Files
netwatch/script/bootstrap
T
clawbot f423768975
check / check (push) Successful in 2m13s
cibuild: the org model, which runs every check uncached (closes #37)
script/cibuild was a plain docker build ., so on a tree Docker had
seen before every check step came from the build cache and the build
still passed. It is now the org model from sneak/prompts, byte for
byte: script/bootstrap, script/check, then docker build --no-cache
with the git describe version as the VERSION build argument.

The workflow puts ~/.local/bin, where bootstrap links what it
installs, on the step's PATH. Bootstrap now installs its pinned node
when the installed one is older than 22.12.0, the oldest the
frontend's dependencies accept (puppeteer-core's engines field), as
it already does for Go against backend/go.mod.

Model: opus-5-5
2026-09-29 11:55:52 +02:00

277 lines
9.5 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 it is at least NODE_MIN_VERSION; 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"
# The oldest node the frontend's dependencies accept: the "engines"
# field of puppeteer-core 25.5.0, the most demanding of them, asks for
# 22.12.0 or newer, 2026-09-29. An older installed node is not used.
NODE_MIN_VERSION="22.12.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"
}
# node_ok: the node on PATH is at least NODE_MIN_VERSION. node itself
# compares the two: major, then minor, then patch.
node_ok() {
if missing node; then return 1; fi
node -e '
const have = process.versions.node.split(".").map(Number);
const want = process.argv[1].split(".").map(Number);
for (let i = 0; i < 3; i++) {
if (have[i] !== want[i]) process.exit(have[i] > want[i] ? 0 : 1);
}
' "$NODE_MIN_VERSION"
}
# ensure_node: unless node_ok, install NODE_VERSION and link its node.
ensure_node() {
if node_ok; 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 "$@"