#!/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. # golangci-lint is installed via `go install` pinned to the same version # the Dockerfile lint stage uses (never "latest"), and is reinstalled # whenever the installed version differs from that pin. The install is # then verified against the binary PATH actually resolves: if the pin is # still not what would run, bootstrap fails instead of reporting # success. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" # Pinned versions, 2026-08-07 (same version as the Dockerfile lint stage). # This is the single source of truth for the linter version: the module # ref below and the version comparison in main() are both derived from # it, so a bump here cannot half-apply. Written without a leading "v", # the way `golangci-lint --version` reports it. GOLANGCI_LINT_VERSION="2.12.2" GOLANGCI_LINT_MODULE="github.com/golangci/golangci-lint/v2/cmd/golangci-lint" GOLANGCI_LINT_REF="$GOLANGCI_LINT_MODULE@v$GOLANGCI_LINT_VERSION" # Seconds to allow `golangci-lint --version` to run. Bootstrap now # executes the binary rather than merely locating it, so a wedged one # must not hang the script. GOLANGCI_LINT_VERSION_TIMEOUT="30" 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 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 } # Echo the installed golangci-lint version, or nothing when the tool is # absent. The binary reports e.g. # golangci-lint has version X.Y.Z built with go1.26.5 from abc1234 ... # so the version is the field after the literal word "version", and it # carries no leading "v" (the module ref does). Some builds do print a # leading "v", so strip one if present and compare bare versions. # # Only stdout is parsed; the binary's stderr is deliberately left # connected to ours so that a present-but-broken linter (missing shared # library, wrong architecture) says why instead of silently yielding the # empty string. The call is bounded by timeout(1) where that exists — # stock macOS has no timeout(1), and there the call runs unbounded, as # it did before this check was version-aware. golangci_lint_version() { command -v golangci-lint >/dev/null 2>&1 || return 0 if command -v timeout >/dev/null 2>&1; then timeout "$GOLANGCI_LINT_VERSION_TIMEOUT" golangci-lint --version else golangci-lint --version fi | awk ' { for (i = 1; i < NF; i++) { if ($i == "version") { v = $(i + 1) sub(/^v/, "", v) print v exit } } } ' } # Confirm that the golangci-lint just installed is the one that will # actually run. `go install` writes into "$(go env GOBIN)" (or # "$(go env GOPATH)/bin"), but `make lint` runs whatever PATH resolves # first. When a wrong-version binary sits ahead of that directory — a # nix profile, apt, brew, apk, or a tarball in /usr/local/bin — the # install lands behind the shadow and changes nothing the gate uses. # Exiting 0 there would leave the local gate linting against a different # ruleset than CI while claiming success, which is the failure this # whole check exists to prevent. Diagnose and stop: naming both paths is # what makes it fixable. Reordering PATH or deleting someone else's # binary is not bootstrap's call. verify_golangci_lint() { # Forget any remembered command locations first: the install may have # created a binary in a directory the shell already searched. hash -r 2>/dev/null || true goinstalldir="$(go env GOBIN)" if [ -z "$goinstalldir" ]; then goinstalldir="$(go env GOPATH)/bin" fi resolved="$(command -v golangci-lint 2>/dev/null || true)" effective="$(golangci_lint_version)" if [ "$effective" != "$GOLANGCI_LINT_VERSION" ]; then echo "bootstrap: installed golangci-lint $GOLANGCI_LINT_VERSION into" \ "$goinstalldir, but the golangci-lint on PATH is" \ "${resolved:-not resolvable} and reports" \ "${effective:-no parseable version}" >&2 echo "bootstrap: the install is shadowed or unreachable; put" \ "$goinstalldir ahead of it on PATH (or remove the shadowing" \ "binary) and re-run" >&2 exit 1 fi } main() { cd "$ROOT" # System tooling, deliberately unpinned: these come from the host # package manager and whatever version it ships is what the host # gets, so a presence check is the right check. The repo pins no # system toolchain versions — the Go language version is governed by # go.mod, and builds that must be reproducible run in the Docker # image, whose base images are pinned by digest. if missing git; then pkg_install git git git git; fi if missing make; then pkg_install gnumake make make make; fi if missing go; then pkg_install go golang go go; fi # Lint tooling, pinned via go install (installs into # "$(go env GOPATH)/bin"; ensure that is on your PATH). Unlike the # system tools above this is version-checked, not presence-checked: # the Dockerfile lint stage runs a digest-pinned linter, so a host # running any other version lints against different rules and # `make check` can go green on a commit CI then rejects. Any version # that is not the pin — older or newer — is reinstalled, and the # install is then verified to be the binary PATH resolves. installed="$(golangci_lint_version)" if [ "$installed" != "$GOLANGCI_LINT_VERSION" ]; then echo "bootstrap: golangci-lint ${installed:-absent or unparseable}," \ "want $GOLANGCI_LINT_VERSION; installing" go install "$GOLANGCI_LINT_REF" verify_golangci_lint fi go mod download echo "bootstrap complete" } main "$@"