diff --git a/Dockerfile b/Dockerfile index b5c26f9..b087878 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ COPY . . # Go half of fmt-check only: this image has no node, so no prettier. The # markdown half runs in the mdfmt stage below. The image has no gofumpt -# either; script/gofumpt builds the version it pins with `go run`. +# either; script/gofumpt builds the version tools/go.mod requires. RUN script/gofumpt --check # The linter directly, not `make lint`: script/lint builds this stage, and # there is no docker inside this build. @@ -31,7 +31,7 @@ COPY . . RUN script/prettier --check # Build stage — tests and compilation -# golang:1.23 (2026-03-14) +# golang:1.23.12, 2026-03-14 FROM golang@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 AS builder # Force BuildKit to run the lint and mdfmt stages by creating stage dependencies diff --git a/README.md b/README.md index 65f89ed..2a352eb 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,10 @@ provide: - `script/fmt` — format all code and docs (writes): `script/gofumpt --write` and `script/prettier --write` - `script/gofumpt` — run `gofumpt` over every Go file in the repository in the - given mode, `--write` or `--check`, at the one version it pins (built on - demand by `go run`, so nothing installs it); `script/fmt`, `script/fmt-check` - and the Docker lint stage all go through it, so they cannot disagree about Go + given mode, `--write` or `--check`, at the version `tools/go.mod` requires + (built on demand by `go tool` from source checked against the hashes in + `tools/go.sum`, so nothing installs it); `script/fmt`, `script/fmt-check` and + the Docker lint stage all go through it, so they cannot disagree about Go formatting - `script/prettier` — run prettier over the repository's canonical file set (Markdown and JSON, minus `.prettierignore`) in the given mode, `--write` or diff --git a/package.json b/package.json index cca61f6..1a23c56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "mfer", - "version": "0.1.0", "private": true, "description": "Development tooling for the mfer repository: prettier, used by script/fmt and script/fmt-check to format and verify Markdown and JSON.", "license": "WTFPL", diff --git a/script/bootstrap b/script/bootstrap index 1ee831f..5a5f9c9 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -132,15 +132,18 @@ main() { # ---- JS / docs repos ---- # This is a Go repo, but node and yarn are required anyway: prettier # formats the Markdown and JSON, and script/fmt-check verifies it. - # The version is pinned by package.json/yarn.lock, whose integrity - # hashes --frozen-lockfile enforces. + # The version is pinned by package.json/yarn.lock: yarn checks every + # package it fetches against its yarn.lock integrity hash, and + # --frozen-lockfile fails instead of rewriting a yarn.lock that no + # longer matches package.json. ensure_node ensure_yarn install_js_deps # ---- Go repos ---- if missing go; then pkg_install go golang go go; fi - # No golangci-lint: script/lint runs it in Docker only. + # No golangci-lint: script/lint runs it in Docker only. No gofumpt: + # script/gofumpt builds the version tools/go.mod requires. go mod download # ---- Python repos ---- diff --git a/script/gofumpt b/script/gofumpt index 1622d46..a9fc585 100755 --- a/script/gofumpt +++ b/script/gofumpt @@ -10,10 +10,9 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" -# gofumpt v0.12.0, 2026-10-04. `go run` fetches and builds exactly this -# version, so neither a developer machine nor the lint image needs -# gofumpt installed. -GOFUMPT="mvdan.cc/gofumpt@v0.12.0" +# gofumpt v0.12.0, 2026-10-04, required by tools/go.mod. `go tool` run in +# tools/ builds it from source checked against the hashes in tools/go.sum, +# so neither a developer machine nor the lint image needs it installed. usage() { echo "usage: script/gofumpt --write|--check" >&2 @@ -22,15 +21,15 @@ usage() { main() { [ "$#" -eq 1 ] || usage - cd "$ROOT" - # Every Go file in the repo. gofumpt holds generated files, such as - # mfer/mf.pb.go, to gofmt's rules only. + cd "$ROOT/tools" + # Every Go file in the repo, from $ROOT down. gofumpt holds generated + # files, such as mfer/mf.pb.go, to gofmt's rules only. case "$1" in - --write) go run "$GOFUMPT" -l -w . ;; + --write) go tool gofumpt -l -w "$ROOT" ;; --check) # Own line: a failing command inside `[ -n "$(...)" ]` does # not trip `set -e`, so a gofumpt that never ran would pass. - unformatted="$(go run "$GOFUMPT" -l .)" + unformatted="$(go tool gofumpt -l "$ROOT")" if [ -n "$unformatted" ]; then echo "gofumpt: files need formatting (run make fmt):" >&2 echo "$unformatted" >&2 diff --git a/script/prettier b/script/prettier index b2f66a3..3519278 100755 --- a/script/prettier +++ b/script/prettier @@ -18,24 +18,9 @@ usage() { exit 2 } -# Prefer the version pinned by package.json/yarn.lock so that CI and -# developer machines format identically. Fall back to a prettier on PATH, -# but say so, because a different version formats differently. -find_prettier() { - if [ -x "$ROOT/node_modules/.bin/prettier" ]; then - printf '%s\n' "$ROOT/node_modules/.bin/prettier" - return 0 - fi - if command -v prettier >/dev/null 2>&1; then - echo "prettier: node_modules/.bin/prettier is absent; using the" \ - "prettier on PATH, which may be a different version than the" \ - "one pinned in package.json. Run script/bootstrap to install" \ - "the pinned version." >&2 - command -v prettier - return 0 - fi - return 1 -} +# Only the prettier yarn installed from yarn.lock, never one on PATH: a +# different version formats differently. +PRETTIER="$ROOT/node_modules/.bin/prettier" main() { [ "$#" -eq 1 ] || usage @@ -46,10 +31,17 @@ main() { cd "$ROOT" - if ! prettier_bin="$(find_prettier)"; then - echo "prettier: not found." >&2 - echo " Install it with: script/bootstrap" >&2 - echo " (installs the version pinned in package.json/yarn.lock)" >&2 + # node_modules keeps the old prettier after package.json moves to a new + # one, until script/bootstrap runs again, so compare the two. A prettier + # that runs also means node is on PATH, which reading package.json needs. + if ! installed="$("$PRETTIER" --version 2>/dev/null)"; then + echo "prettier: not installed; run script/bootstrap" >&2 + exit 1 + fi + pinned="$(node -p 'require("./package.json").devDependencies.prettier')" + if [ "$installed" != "$pinned" ]; then + echo "prettier: package.json pins $pinned but $installed is" \ + "installed; run script/bootstrap" >&2 exit 1 fi @@ -62,8 +54,8 @@ main() { # patterns always match at least one tracked file (README.md, # package.json), so an empty match means the glob broke, and prettier # erroring out is exactly what we want rather than a vacuous pass. - "$prettier_bin" "$mode" "**/*.md" - "$prettier_bin" "$mode" "**/*.json" + "$PRETTIER" "$mode" "**/*.md" + "$PRETTIER" "$mode" "**/*.json" } main "$@" diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 0000000..8d0bd50 --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,15 @@ +// The developer tools this repo runs with `go tool`, kept out of the mfer +// module so they add nothing to what mfer's users download. `go tool` builds +// exactly the source whose hashes go.sum here records. +module sneak.berlin/go/mfer/tools + +go 1.26.0 + +tool mvdan.cc/gofumpt + +require ( + golang.org/x/mod v0.40.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/tools v0.49.0 // indirect + mvdan.cc/gofumpt v0.12.0 // indirect +) diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 0000000..34c5e53 --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,20 @@ +github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= +github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho= +mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=