#!/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. 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" 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 2.12.2 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. golangci_lint_version() { command -v golangci-lint >/dev/null 2>&1 || return 0 golangci-lint --version 2>/dev/null | awk ' { for (i = 1; i < NF; i++) { if ($i == "version") { v = $(i + 1) sub(/^v/, "", v) print v exit } } } ' } 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. 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" fi go mod download echo "bootstrap complete" } main "$@"