Files
netwatch/script/bootstrap
T
clawbot bbcc7d921d
check / check (push) Successful in 12s
build: one image, nginx in front of the backend on loopback (closes #52)
The root Dockerfile builds the only image; Dockerfile.backend is gone.
Its stages: lint, a Go stage that runs the tests and builds
netwatch-server, the node stage, and an nginx runtime. nginx serves
dist/ on 8080 and proxies /api/ and /.well-known/healthcheck to the
backend on 127.0.0.1:8081. bin/entrypoint.sh starts both, turns TERM or
INT into a stop of both, and exits non-zero when either exits on its
own. The backend runs as user netwatch and keeps reports on the /data
volume. New setting BIND_ADDRESS (empty: every interface). STOPSIGNAL is
SIGTERM, since the nginx image's SIGQUIT would miss the entrypoint.
script/docker is the org model verbatim.

Model: opus-5-5
2026-09-29 02:59:33 +02:00

259 lines
8.8 KiB
Bash
Executable File

#!/bin/sh
# script/bootstrap: install all dependencies needed to build and develop
# this repo. Idempotent: every install is guarded by a check so already
# installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes nothing is present. Node is
# used directly if installed; otherwise it is installed at a pinned
# version via nvm (installing nvm itself first, from a hash-verified
# release archive, never curl | sh). Go, with its gofmt, is used
# directly if it is at least the version backend/go.mod asks for;
# otherwise the pinned Go release is installed from its hash-verified
# archive.
#
# What this script installs outside the system package manager lives
# under $HOME and is linked into ~/.local/bin, where make and the git
# hook find it once that directory is on PATH. Nothing in ~/.local/bin
# that this script did not create is ever replaced.
#
# golangci-lint is not installed: make lint runs it in Docker, which
# this script does not install either.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Pinned versions, 2026-07-07
NODE_VERSION="22.17.0"
NVM_VERSION="0.40.3"
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
YARN_VERSION="1.22.22"
# The Go inside the golang:1.25-alpine image Dockerfile builds the
# backend with, 2026-08-09. The archive hashes are in ensure_go.
GO_VERSION="1.25.7"
BIN_DIR="$HOME/.local/bin"
TOOLCHAIN="$HOME/.local/share/$("$ROOT/script/projectname")/toolchain"
PKGMGR=""
SUDO=""
APT_UPDATED=""
detect_pkgmgr() {
[ -n "$PKGMGR" ] && return 0
if command -v nix-env >/dev/null 2>&1; then
PKGMGR="nix"
elif command -v apt-get >/dev/null 2>&1; then
PKGMGR="apt"
elif command -v brew >/dev/null 2>&1; then
PKGMGR="brew"
elif command -v apk >/dev/null 2>&1; then
PKGMGR="apk"
else
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
exit 1
fi
if [ "$PKGMGR" = "apt" ]; then
export DEBIAN_FRONTEND=noninteractive
if [ "$(id -u)" != "0" ]; then
SUDO="sudo"
fi
fi
}
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
pkg_install() {
detect_pkgmgr
case "$PKGMGR" in
nix) nix-env -iA "nixpkgs.$1" ;;
apt)
if [ -z "$APT_UPDATED" ]; then
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
APT_UPDATED=1
fi
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
;;
brew) brew install "$3" ;;
apk) apk add --no-cache "$4" ;;
esac
}
missing() {
! command -v "$1" >/dev/null 2>&1
}
# verify_sha256 <file> <expected-hash>
verify_sha256() {
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$1" | cut -d' ' -f1)"
else
actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
fi
if [ "$actual" != "$2" ]; then
echo "bootstrap: sha256 mismatch for $1" >&2
echo " expected: $2" >&2
echo " actual: $actual" >&2
exit 1
fi
}
# link_bin <target> <name>: make an installed tool reachable as
# $BIN_DIR/<name>. Only a symlink this script made, one pointing into
# $TOOLCHAIN or ~/.nvm, is ever replaced; if anything else is already
# there, bootstrap stops.
link_bin() {
link="$BIN_DIR/$2"
if [ -L "$link" ] || [ -e "$link" ]; then
case "$(readlink "$link" || true)" in
"$TOOLCHAIN"/* | "$HOME"/.nvm/*) ;;
*)
echo "bootstrap: $link was not created by this script;" >&2
echo " remove or rename it, then re-run bootstrap" >&2
exit 1
;;
esac
fi
mkdir -p "$BIN_DIR"
ln -sf "$1" "$link"
}
# nvm is a bash script; run a command in a bash with nvm loaded
nvm_sh() {
bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
}
ensure_nvm() {
[ -s "$HOME/.nvm/nvm.sh" ] && return 0
# nvm prerequisites; nvm itself requires bash
if missing bash; then pkg_install bash bash bash bash; fi
if missing curl; then pkg_install curl curl curl curl; fi
if missing git; then pkg_install git git git git; fi
tmp="$(mktemp -d)"
curl -fsSL -o "$tmp/nvm.tar.gz" \
"https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz"
verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256"
mkdir -p "$HOME/.nvm"
tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
rm -rf "$tmp"
}
ensure_node() {
if ! missing node; then return 0; fi
ensure_nvm
nvm_sh "nvm install $NODE_VERSION"
link_bin "$HOME/.nvm/versions/node/v$NODE_VERSION/bin/node" node
}
# ensure_yarn: corepack writes its shims (pnpm and yarnpkg as well as
# yarn) into $TOOLCHAIN rather than next to itself, and the npm fallback
# installs there too; only yarn is linked.
ensure_yarn() {
if ! missing yarn; then return 0; fi
shims="$TOOLCHAIN/corepack-shims"
mkdir -p "$shims"
if ! missing corepack; then
corepack enable --install-directory "$shims"
corepack prepare "yarn@$YARN_VERSION" --activate
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
nvm_sh "nvm use $NODE_VERSION >/dev/null && \
corepack enable --install-directory \"$shims\" && \
corepack prepare yarn@$YARN_VERSION --activate"
else
npm install -g --prefix "$TOOLCHAIN/npm-global" "yarn@$YARN_VERSION"
shims="$TOOLCHAIN/npm-global/bin"
fi
link_bin "$shims/yarn" yarn
}
# go_ok: the go on PATH has its gofmt beside it (a Go release ships the
# two together) and is at least the version backend/go.mod asks for.
# GOTOOLCHAIN=local makes an older go fail here instead of fetching a
# newer toolchain for itself.
go_ok() {
if missing go; then return 1; fi
[ -x "$(dirname "$(command -v go)")/gofmt" ] || return 1
(cd "$ROOT/backend" && GOTOOLCHAIN=local go list -m >/dev/null 2>&1)
}
# ensure_go: unless go_ok, install GO_VERSION and link its go and gofmt.
# They are linked on every run that needs them, so a deleted link is put
# back, and the archive is unpacked again if either binary is missing.
ensure_go() {
if go_ok; then return 0; fi
go_dir="$TOOLCHAIN/go-$GO_VERSION"
if [ ! -x "$go_dir/bin/go" ] || [ ! -x "$go_dir/bin/gofmt" ]; then
# sha256 of each archive, from https://go.dev/dl/?mode=json
case "$(uname -s)-$(uname -m)" in
Linux-x86_64)
plat="linux-amd64"
sha="12e6d6a191091ae27dc31f6efc630e3a3b8ba409baf3573d955b196fdf086005"
;;
Linux-aarch64)
plat="linux-arm64"
sha="ba611a53534135a81067240eff9508cd7e256c560edd5d8c2fef54f083c07129"
;;
Darwin-x86_64)
plat="darwin-amd64"
sha="bf5050a2152f4053837b886e8d9640c829dbacbc3370f913351eb0904cb706f5"
;;
Darwin-arm64)
plat="darwin-arm64"
sha="ff18369ffad05c57d5bed888b660b31385f3c913670a83ef557cdfd98ea9ae1b"
;;
*)
echo "bootstrap: no pinned Go release for this platform" >&2
exit 1
;;
esac
if missing curl; then pkg_install curl curl curl curl; fi
mkdir -p "$TOOLCHAIN"
curl -fsSL -o "$go_dir.tar.gz" \
"https://go.dev/dl/go$GO_VERSION.$plat.tar.gz"
verify_sha256 "$go_dir.tar.gz" "$sha"
# Unpacked beside its final place and then moved there, so an
# interrupted run never leaves a partial Go that looks complete.
rm -rf "$go_dir.partial"
mkdir "$go_dir.partial"
tar -xzf "$go_dir.tar.gz" -C "$go_dir.partial" --strip-components=1
rm -rf "$go_dir" "$go_dir.tar.gz"
mv "$go_dir.partial" "$go_dir"
fi
link_bin "$go_dir/bin/go" go
link_bin "$go_dir/bin/gofmt" gofmt
}
main() {
cd "$ROOT"
# Tools linked on an earlier run count as installed, and tools linked
# on this run are found by the steps after it.
path_hint=""
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*) path_hint=yes ;;
esac
PATH="$BIN_DIR:$PATH"
if missing make; then pkg_install gnumake make make make; fi
if missing git; then pkg_install git git git git; fi
ensure_node
ensure_yarn
yarn install --frozen-lockfile
ensure_go
(cd "$ROOT/backend" && go mod download)
if missing docker; then
echo "bootstrap: docker not found; make lint, and so make check" >&2
echo " and the pre-commit hook, need it to run the Go linter" >&2
fi
if [ -n "$path_hint" ] && [ -d "$BIN_DIR" ]; then
echo "bootstrap: add $BIN_DIR to the front of your PATH, e.g." >&2
echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" >&2
fi
echo "bootstrap complete"
}
main "$@"