ci: take bash and tools from the .#ci dev shell; stop policing versions

This follows the user's rulings: CI should not care which bash or which
standard utilities it runs, and jobs take what the ci dev shell provides.

- checkers.sh: remove find_modern_bash, the 5.1 floor (improvise-fd0j) and
  exit 3. Suites run under $BASH with PATH unchanged.
- checkers-test.sh: remove sections 2, 5 and 5b (the fake bashes and utilbin).
  They existed only to pin the floor.
- scripts/kb: drop the BSD `stat -f` fallback. kb needs the dev shell for tpl,
  and that shell's stat is GNU.
- ci.yml: remove every `shell: bash` override, so every step runs in the dev
  shell. Remove the web-toolchain version probe and the Playwright
  version/browser probe. The shell's PLAYWRIGHT_BROWSERS_PATH pins the
  browsers. npm ci stays because @playwright/test is not nix-provided.
- flake.nix: update the comments that promised those CI checks.

Reasoning: context/code/tooling.md section 5. Evidence:
context/test-evidence/tooling.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
Edward Langley
2026-09-23 23:32:54 -07:00
parent 68f32a8a55
commit 754add31e2
7 changed files with 88 additions and 173 deletions
+6 -47
View File
@@ -53,29 +53,6 @@ jobs:
# below, so it fails under this job's name rather than another's. # below, so it fails under this job's name rather than another's.
- name: The stamp gate refuses what it claims to - name: The stamp gate refuses what it claims to
run: ./scripts/checkers.sh assemble-stamp run: ./scripts/checkers.sh assemble-stamp
# scripts/web-toolchain-versions.env stays the one home for the pins, so
# the release capture (scripts/build-web-release.sh, improvise-oujb)
# provisions the same versions. CI no longer installs them — the dev
# shell supplies wasm-pack and node — so this checks the shell against
# the pins instead: a nixpkgs bump that moves either fails here, by name.
# wasm-pack runs its own bundled `wasm-opt`, which is why
# improvise-wasm-client's Cargo.toml names `--enable-bulk-memory`.
- name: The dev shell supplies the pinned web toolchain
run: |
. scripts/web-toolchain-versions.env
test "$(wasm-pack --version)" = "wasm-pack ${WASM_PACK_VERSION#v}"
test "$(node -p 'process.versions.node.split(".")[0]')" = "$NODE_VERSION"
wasm-pack --version
node --version
# wasm-pack uses the shell's wasm-bindgen only at Cargo.lock's exact
# version; otherwise it cargo-installs one (71s) — so a lock bump
# must move flake.nix's wasmBindgenCli too, and this says so.
lock="$(awk '/^name = "wasm-bindgen"$/ {getline; gsub(/[^0-9.]/, ""); print}' Cargo.lock)"
test "$(wasm-bindgen --version)" = "wasm-bindgen $lock" || {
echo "flake.nix wasmBindgenCli is $(wasm-bindgen --version); Cargo.lock wants $lock" >&2
exit 1
}
# Built by Nix (packages.web and packages.site in nix/ci.nix), which runs # Built by Nix (packages.web and packages.site in nix/ci.nix), which runs
# the same harness/build.sh and web/assemble.sh a developer runs, so there # the same harness/build.sh and web/assemble.sh a developer runs, so there
# is still one answer to "how is the bundle built". `web` is the three # is still one answer to "how is the bundle built". `web` is the three
@@ -84,14 +61,12 @@ jobs:
# the harness tree is stamped `harness`. Dependencies are their own # the harness tree is stamped `harness`. Dependencies are their own
# derivations, substituted from titan's cache when Cargo.lock is unchanged. # derivations, substituted from titan's cache when Cargo.lock is unchanged.
- name: Build the bundles, the harness tree and the site - name: Build the bundles, the harness tree and the site
shell: bash
run: | run: |
nix build --print-build-logs --out-link result-web .#web nix build --print-build-logs --out-link result-web .#web
nix build --print-build-logs --out-link result-site .#site nix build --print-build-logs --out-link result-site .#site
# The upload steps below read these paths. Copied rather than linked: # The upload steps below read these paths. Copied rather than linked:
# an artifact of a symlink into /nix/store is a dangling link elsewhere. # an artifact of a symlink into /nix/store is a dangling link elsewhere.
- name: Lay the outputs out where the uploads read them - name: Lay the outputs out where the uploads read them
shell: bash
run: | run: |
rm -rf harness/serve web/pkg web/worker-pkg site rm -rf harness/serve web/pkg web/worker-pkg site
cp -rL result-web/harness-serve harness/serve cp -rL result-web/harness-serve harness/serve
@@ -143,12 +118,10 @@ jobs:
# checks.clippy and checks.test in nix/ci.nix: the same cargo commands, # checks.clippy and checks.test in nix/ci.nix: the same cargo commands,
# on the dev/test profiles, with dependencies as a cached derivation. # on the dev/test profiles, with dependencies as a cached derivation.
- name: cargo clippy --workspace --all-targets -- -D warnings - name: cargo clippy --workspace --all-targets -- -D warnings
shell: bash
run: nix build --print-build-logs .#checks.x86_64-linux.clippy run: nix build --print-build-logs .#checks.x86_64-linux.clippy
# Also runs the test-evidence gate (check-comment-runs.py # Also runs the test-evidence gate (check-comment-runs.py
# --check-evidence), which lists the tests this derivation compiled. # --check-evidence), which lists the tests this derivation compiled.
- name: cargo test --workspace, and test-evidence headings name real tests - name: cargo test --workspace, and test-evidence headings name real tests
shell: bash
run: nix build --print-build-logs .#checks.x86_64-linux.test run: nix build --print-build-logs .#checks.x86_64-linux.test
# The comment cap and the test-evidence keys (improvise-9c8c); without # The comment cap and the test-evidence keys (improvise-9c8c); without
# this step the rule is advisory. See context/design-principles.md §6. # this step the rule is advisory. See context/design-principles.md §6.
@@ -174,9 +147,7 @@ jobs:
runs-on: [self-hosted, nixos, microvm] runs-on: [self-hosted, nixos, microvm]
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
# Plain bash, not the dev shell: this step needs nix and nothing else.
- name: nix build .#improvise - name: nix build .#improvise
shell: bash
run: nix build --print-build-logs .#improvise run: nix build --print-build-logs .#improvise
wasm: wasm:
@@ -190,7 +161,6 @@ jobs:
# depends on crossterm, which does not build for wasm — that is correct, # depends on crossterm, which does not build for wasm — that is correct,
# not a gap (improvise-ywd). The log names each crate as it is checked. # not a gap (improvise-ywd). The log names each crate as it is checked.
- name: cargo check --target wasm32-unknown-unknown, per bundle crate - name: cargo check --target wasm32-unknown-unknown, per bundle crate
shell: bash
run: nix build --print-build-logs .#checks.x86_64-linux.wasm-check run: nix build --print-build-logs .#checks.x86_64-linux.wasm-check
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -235,9 +205,9 @@ jobs:
# bypass also found that nothing ran this suite. # bypass also found that nothing ran this suite.
- name: The kb tool refuses what it claims to - name: The kb tool refuses what it claims to
run: ./scripts/checkers.sh kb run: ./scripts/checkers.sh kb
# Every suite goes through scripts/checkers.sh, which pins a bash >= 5 # Every suite goes through scripts/checkers.sh, which scrubs the
# and scrubs the environment, so the shell and the variables a suite # environment, so the variables a suite passes under here are the ones it
# passes under here are the ones it passes under locally (improvise-f9qf). # passes under locally (improvise-f9qf). Its bash is the dev shell's.
- name: The checker runner refuses what it claims to - name: The checker runner refuses what it claims to
run: ./scripts/checkers.sh --self-test run: ./scripts/checkers.sh --self-test
# pr-watch is driven entirely from fixtures. The release preflight is the # pr-watch is driven entirely from fixtures. The release preflight is the
@@ -273,7 +243,6 @@ jobs:
# checks.harness-lint in nix/ci.nix, run from inside harness/opfs-contract: # checks.harness-lint in nix/ci.nix, run from inside harness/opfs-contract:
# `cargo fmt -p opfs-contract -- --check` and clippy for wasm32. # `cargo fmt -p opfs-contract -- --check` and clippy for wasm32.
- name: cargo fmt -p opfs-contract and clippy --target wasm32 - name: cargo fmt -p opfs-contract and clippy --target wasm32
shell: bash
run: nix build --print-build-logs .#checks.x86_64-linux.harness-lint run: nix build --print-build-logs .#checks.x86_64-linux.harness-lint
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -301,19 +270,9 @@ jobs:
- name: Install the harness - name: Install the harness
working-directory: harness working-directory: harness
run: npm ci run: npm ci
# No browser install step: the dev shell points PLAYWRIGHT_BROWSERS_PATH # No browser install step and no version probe: the dev shell sets
# at nixpkgs' Chromium, Firefox and WebKit, since Playwright's own # PLAYWRIGHT_BROWSERS_PATH to nixpkgs' browsers, which pin them. `npm ci`
# downloads cannot run on NixOS. All three, because improvise-xf1's # stays because @playwright/test itself is not nix-provided.
# acceptance criterion names Chrome, Firefox and Safari and
# `createSyncAccessHandle` is implemented separately by each engine.
# WebKit's OPFS is absent today; the spec skips on a runtime probe.
# @playwright/test must equal nixpkgs' playwright-driver version, or it
# looks for browser builds that are not there; this says which.
- name: Playwright matches the dev shell's browsers
working-directory: harness
run: |
npx playwright --version
ls "$PLAYWRIGHT_BROWSERS_PATH"
# The tree `build` assembled, not one this job rebuilds. Same bytes as # The tree `build` assembled, not one this job rebuilds. Same bytes as
# the Pages artifact, modulo the cache-bust token `web/assemble.sh` # the Pages artifact, modulo the cache-bust token `web/assemble.sh`
# stamps — the harness tree is stamped `harness` and the site the commit # stamps — the harness tree is stamped `harness` and the site the commit
+30 -15
View File
@@ -660,32 +660,47 @@ sixteen scripts, and the review cost is the whole diff rather than the shebang
line. The runner also does not ship zsh, so every job would need an `apt-get`. line. The runner also does not ship zsh, so every job would need an `apt-get`.
Declined. Declined.
What was adopted instead pins the *interpreter and the environment together*, in What was adopted instead fixes the *environment* a suite runs in, in
one place, with no per-invocation nix cost: one place, with no per-invocation nix cost:
**`scripts/checkers.sh` is the one entry point for every check_the_checker.** CI **`scripts/checkers.sh` is the one entry point for every check_the_checker.** CI
calls it and the local gate calls it, so there is no second invocation to drift. calls it and the local gate calls it, so there is no second invocation to drift.
It does three things a bare `./scripts/X-test.sh` does not: It runs every suite under the bash that runs it: in CI and under direnv, that
is the dev shell's bash. It does two things a bare `./scripts/X-test.sh` does
not:
1. **Pins a bash ≥ 5.1.** The ambient one is tried first, so a runner that 1. **Scrubs the environment to an allowlist** (`env -i` plus `PATH`, `HOME`,
already has bash 5.2 pays nothing; nix supplies one only when the ambient
bash is older, which on macOS outside the dev shell it is. No bash ≥ 5.1 and
no nix is **exit 3** — a refusal, never a quiet run under 3.2. The floor is
5.1 and not 5 because `pr-reviews-test.sh` uses `wait -n -p`, and `-p`
arrived in 5.1. Under 5.0 it is an invalid option (improvise-fd0j).
2. **Scrubs the environment to an allowlist** (`env -i` plus `PATH`, `HOME`,
`TMPDIR`, the `NIX_*` and `GITHUB_*` names, extensible through `TMPDIR`, the `NIX_*` and `GITHUB_*` names, extensible through
`CHECKERS_KEEP`). This is the part that closes the direnv hole above, and it `CHECKERS_KEEP`). This is the part that closes the direnv hole above, and it
is a *positive* list on purpose: the failure mode of a missing name is a loud is a *positive* list on purpose: the failure mode of a missing name is a loud
"command not found", where the failure mode of an extra name is the silence "command not found", where the failure mode of an extra name is the silence
this section is about. this section is about.
3. **Enforces the shebang rule**, so the `sh`/`bash` split cannot come back. 2. **Enforces the shebang rule**, so the `sh`/`bash` split cannot come back.
The pinned bash's directory goes on the front of `PATH`, so a script the suite **The runner no longer polices the bash version or where tools live.** It used
invokes through its *own* `#!/usr/bin/env bash` — or through `sh`, which to pin a bash of at least 5.1 (`find_modern_bash`: try the ambient bash, fall
nixpkgs' bash also provides — gets the pinned one too. Pinning only the outer back to `nix shell nixpkgs#bash`, and exit 3 if neither was available). The
shell would have left every inner one on the system bash, which is where the floor was 5.1 because `pr-reviews-test.sh` uses `wait -n -p` (improvise-fd0j).
fault actually lived. All of that was removed on the user's ruling, when CI moved to the NixOS
runners: "we shouldn't care which version of bash and other standard utilities
are running in CI", and "most jobs should just take what comes in the ci dev
shell".
So every CI step runs inside `nix develop .#ci`, which provides the bash and the
GNU coreutils. The runner calls suites with `$BASH` and an unchanged `PATH`. A
script that needs a newer bash feature fails where it uses it, not at a version
gate.
Two things went with the floor:
- **Its self-tests.** `checkers-test.sh` sections 2, 5 and 5b faked old
bashes, or stripped `PATH` down to hand-picked utilities, to prove the floor
held. Those were the tests that broke on the NixOS runners, where `/usr/bin`
and `/bin` hold only `env` and `sh` (#296, improvise-3uow).
- **The hard-coded tool paths** in the suites (`/bin/sleep` and `/bin/date` in
`pr-reviews-test.sh`) and `scripts/kb`'s BSD `stat -f` fallback. `scripts/kb`
already needs the dev shell for `tpl`, so GNU `stat` is always present when it
runs.
**A suite is `X-test.sh` where `X` or `X.sh` is present beside it.** That rule **A suite is `X-test.sh` where `X` or `X.sh` is present beside it.** That rule
exists because `scripts/node-test.sh` matches `*-test.sh` and is a test exists because `scripts/node-test.sh` matches `*-test.sh` and is a test
+33 -3
View File
@@ -1106,6 +1106,36 @@ clock is a separate script.
**Scope sweep.** `grep -rnE '/(usr/)?bin/[a-z]'` over `scripts/`, `web/*.sh`, **Scope sweep.** `grep -rnE '/(usr/)?bin/[a-z]'` over `scripts/`, `web/*.sh`,
`harness/*.sh`, `harness/tests` and `.github/workflows` finds only `#!/bin/sh` `harness/*.sh`, `harness/tests` and `.github/workflows` finds only `#!/bin/sh`
shebangs, and `/bin/sh` exists on NixOS. It also finds the shebangs, and `/bin/sh` exists on NixOS. It also found a
`${PATH:-/usr/bin:/bin}` fallback in `checkers.sh`, which is used only when `${PATH:-/usr/bin:/bin}` fallback in `checkers.sh`, which is now removed (next
PATH is unset. entry).
### The bash floor and its self-tests are retired — `scripts/checkers.sh`
**The ruling.** The user ruled that CI should not care which version of bash or
of the standard utilities it runs, and that CI jobs should use whatever the
`.#ci` dev shell provides. So `checkers.sh` no longer pins a bash:
`find_modern_bash`, the 5.1 floor (improvise-fd0j) and exit 3 are gone. Suites
run under `$BASH`, the bash running the runner, with `PATH` unchanged. The
reasoning is in `context/code/tooling.md` §5.
**Tests removed.** Three sections of `checkers-test.sh` were removed. They
existed only to pin the floor, and the entries above that cite them describe
cases that no longer exist:
- Section 2, "a bash-3.2-only construct fails under the runner's pinned bash".
It asserted a bash-5 behaviour, so its result depended on the bash version.
- Section 5, "an old bash with no nix is exit 3".
- Section 5b, "a bash 5.0 with no nix is exit 3, not 5.x" and "a bash 5.1 is
accepted", together with the `utilbin` that #296 added for them.
The suite keeps 10 cases: environment scrubbing (with its direct-run pair), the
shebang rule (with its control), fail and pass, unknown suite, and `--list`.
**Kept deliberately.** The environment allowlist. That is what the
`set -u`/`$name` evidence above earns, and it does not depend on any version.
**Also removed with the ruling.** `scripts/kb`'s `stat -f` fallback: `kb`
already requires the dev shell for `tpl`, and the dev shell's `stat` is GNU.
From `ci.yml`, the toolchain version probes ("the dev shell supplies the pinned
web toolchain", "Playwright matches the dev shell's browsers") and every
`shell: bash` override, so every step runs in `nix develop .#ci`.
+4 -5
View File
@@ -43,7 +43,7 @@
# behind the runners' TLS-auditing proxy those downloads fail (binaryen # behind the runners' TLS-auditing proxy those downloads fail (binaryen
# with no fallback). On PATH, it downloads nothing. wasm-pack only takes # with no fallback). On PATH, it downloads nothing. wasm-pack only takes
# a PATH wasm-bindgen whose version equals Cargo.lock's `wasm-bindgen`, # a PATH wasm-bindgen whose version equals Cargo.lock's `wasm-bindgen`,
# so this pin moves with the lock; CI checks that it does. # so this pin moves with the lock: on a mismatch the sandboxed build fails.
wasmBindgenCli = pkgs.buildWasmBindgenCli rec { wasmBindgenCli = pkgs.buildWasmBindgenCli rec {
src = pkgs.fetchCrate { src = pkgs.fetchCrate {
pname = "wasm-bindgen-cli"; pname = "wasm-bindgen-cli";
@@ -73,14 +73,13 @@
# `web/build.sh` — and so the Pages job's local dry run — needs this. # `web/build.sh` — and so the Pages job's local dry run — needs this.
# The `wasm` CI job only runs `cargo check`, which does not, so the # The `wasm` CI job only runs `cargo check`, which does not, so the
# shell was missing the one tool that turns a passing check into a # shell was missing the one tool that turns a passing check into a
# loadable page (improvise-d31). Must match WASM_PACK_VERSION in # loadable page (improvise-d31). CI takes whatever version this is.
# scripts/web-toolchain-versions.env; CI checks that it does.
pkgs.wasm-pack pkgs.wasm-pack
wasmBindgenCli wasmBindgenCli
pkgs.binaryen pkgs.binaryen
# NODE_VERSION's major, same check. The js and browser jobs ran 22 # The js and browser jobs once ran 22 while `build` ran 24; one shell
# while `build` ran the pinned 24; one shell means one node. # means one node.
pkgs.nodejs_24 pkgs.nodejs_24
pkgs.python3 pkgs.python3
pkgs.jq pkgs.jq
+4 -63
View File
@@ -67,21 +67,7 @@ direct=$?
unset name unset name
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 2. The bash-5 `local` semantics. A multi-assignment `local` does not read # 2. The shebang rule. `#!/bin/sh` is dash in CI and bash-in-sh-mode on macOS.
# left to right on bash 5, so this suite passes on macOS bash 3.2 and dies
# under `set -u` on bash 5 -- PR #133's 19/19 local against 2/19 in CI.
d="$(sandbox bash5)"
suite "$d" vers <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
f() { local a="$1" b="$a/x"; echo "$b"; }
f one
EOF
expect "a bash-3.2-only construct fails under the runner's pinned bash" \
1 "$d/checkers.sh" vers
# ---------------------------------------------------------------------------
# 3. The shebang rule. `#!/bin/sh` is dash in CI and bash-in-sh-mode on macOS.
d="$(sandbox shebang)" d="$(sandbox shebang)"
suite "$d" fine <<'EOF' suite "$d" fine <<'EOF'
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -101,7 +87,7 @@ rm -f "$d/legacy.sh"
expect "and the same sandbox passes once it is removed" 0 "$d/checkers.sh" fine expect "and the same sandbox passes once it is removed" 0 "$d/checkers.sh" fine
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 4. A failing suite is reported, and a passing one is not. # 3. A failing suite is reported, and a passing one is not.
d="$(sandbox rc)" d="$(sandbox rc)"
suite "$d" red <<'EOF' suite "$d" red <<'EOF'
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -117,52 +103,7 @@ EOF
expect "a passing suite passes the runner" 0 "$d/checkers.sh" blue expect "a passing suite passes the runner" 0 "$d/checkers.sh" blue
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 5. No bash >= 5 and no nix is exit 3, not a silent run under whatever is # 4. A named suite that does not exist is a usage error, not a silent no-op.
# there. This is the arm that would have refused the macOS system bash.
d="$(sandbox oldbash)"
suite "$d" any <<'EOF'
#!/usr/bin/env bash
exit 0
EOF
mkdir -p "$work/fakebin"
cat > "$work/fakebin/bash" <<'EOF'
#!/bin/sh
case "$*" in *BASH_VERSINFO*) echo 3; exit 0 ;; esac
exit 0
EOF
chmod +x "$work/fakebin/bash"
# The runner is invoked through a REAL bash, so the fake one below is only
# what its own `command -v bash` probe finds -- not its interpreter.
# utilbin carries only the utilities the runner itself needs, and no nix, so
# the fake bash is the only candidate and the nix fallback is absent. Linked
# one by one rather than taking /usr/bin:/bin, which on NixOS hold only `env`
# and `sh` -- the runner died on `dirname` there, exit 1 instead of 3. `head`
# and `env` are for 5b's 5.1 control, which runs past the version check.
mkdir -p "$work/utilbin"
for tool in dirname head env; do
ln -sf "$(command -v "$tool")" "$work/utilbin/$tool"
done
env PATH="$work/fakebin:$work/utilbin" HOME="$HOME" \
"${BASH:-bash}" "$d/checkers.sh" any > "$work/out" 2>&1
got=$?
[ "$got" = 3 ] && ok "an old bash with no nix is exit 3" \
|| bad "an old bash with no nix is exit 3" "exit $got, wanted 3"
# 5b. The floor is 5.1, not 5: `wait -n -p` is 5.1 (improvise-fd0j). The 5.1
# control proves the 5.0 refusal is the version and not the fake itself.
for v in "5 0:3:a bash 5.0 with no nix is exit 3, not 5.x" \
"5 1:0:a bash 5.1 is accepted"; do
printf '#!/bin/sh\ncase "$*" in *BASH_VERSINFO*) echo %s; exit 0 ;; esac\nexit 0\n' \
"${v%%:*}" > "$work/fakebin/bash"
want="${v#*:}"; name="${want#*:}"; want="${want%%:*}"
env PATH="$work/fakebin:$work/utilbin" HOME="$HOME" \
"${BASH:-bash}" "$d/checkers.sh" any > "$work/out" 2>&1
got=$?
[ "$got" = "$want" ] && ok "$name" || bad "$name" "exit $got, wanted $want"
done
# ---------------------------------------------------------------------------
# 6. A named suite that does not exist is a usage error, not a silent no-op.
d="$(sandbox missing)" d="$(sandbox missing)"
suite "$d" present <<'EOF' suite "$d" present <<'EOF'
#!/usr/bin/env bash #!/usr/bin/env bash
@@ -171,7 +112,7 @@ EOF
expect "an unknown suite name is exit 4" 4 "$d/checkers.sh" absent expect "an unknown suite name is exit 4" 4 "$d/checkers.sh" absent
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 7. --list names every suite in the directory and nothing else. # 5. --list names every suite in the directory and nothing else.
d="$(sandbox listing)" d="$(sandbox listing)"
suite "$d" alpha <<'EOF' suite "$d" alpha <<'EOF'
#!/usr/bin/env bash #!/usr/bin/env bash
+10 -39
View File
@@ -3,15 +3,14 @@
# and so does the local gate, so a suite cannot be green under one shell and # and so does the local gate, so a suite cannot be green under one shell and
# red under the other. Reasoning: context/code/tooling.md section 3. # red under the other. Reasoning: context/code/tooling.md section 3.
# #
# It does three things a bare `./scripts/X-test.sh` does not: # It does two things a bare `./scripts/X-test.sh` does not. It runs every
# suite under the bash running it (the dev shell's), and it:
# #
# 1. pins a bash >= 5.1, taken from nix when the ambient one is older, so the # 1. scrubs the environment down to an allowlist, because `nix develop`
# macOS system bash 3.2 can never be the shell a suite passes under;
# 2. scrubs the environment down to an allowlist, because `nix develop`
# exports `name`, `out`, `system`, `shell`, `builder` and two dozen more # exports `name`, `out`, `system`, `shell`, `builder` and two dozen more
# generically-named variables that CI does not have -- a `set -u` fault # generically-named variables that CI does not have -- a `set -u` fault
# reading one of them is SILENT inside the dev shell and fatal in CI; # reading one of them is SILENT inside the dev shell and fatal in CI;
# 3. refuses a shell script in scripts/ whose shebang is not # 2. refuses a shell script in scripts/ whose shebang is not
# `#!/usr/bin/env bash`, so the sh/bash split cannot come back. # `#!/usr/bin/env bash`, so the sh/bash split cannot come back.
set -euo pipefail set -euo pipefail
@@ -30,7 +29,6 @@ usage: checkers.sh [--list] [--self-test] [name ...]
exit 0 every suite passed exit 0 every suite passed
exit 1 a suite failed, or a shebang is not `#!/usr/bin/env bash` exit 1 a suite failed, or a shebang is not `#!/usr/bin/env bash`
exit 3 no bash >= 5.1 could be found and nix could not supply one
exit 4 usage error exit 4 usage error
THIS SCRIPT'S `$?` MUST BE THE ONE YOU READ. Nothing may follow it in the same THIS SCRIPT'S `$?` MUST BE THE ONE YOU READ. Nothing may follow it in the same
@@ -46,22 +44,6 @@ ALLOW="HOME TMPDIR TMP TEMP TERM LANG LC_ALL USER LOGNAME
NIX_PATH NIX_REMOTE NIX_SSL_CERT_FILE SSL_CERT_FILE NIX_PATH NIX_REMOTE NIX_SSL_CERT_FILE SSL_CERT_FILE
XDG_CACHE_HOME XDG_CONFIG_HOME XDG_DATA_HOME" XDG_CACHE_HOME XDG_CONFIG_HOME XDG_DATA_HOME"
# Prints the absolute path of a bash >= 5.1, or nothing. The ambient one is tried
# first so a machine that already has a modern bash pays no nix startup at all.
find_modern_bash() {
local candidate major minor
candidate="$(command -v bash || true)"
if [ -n "$candidate" ]; then
read -r major minor < <("$candidate" -c 'echo "${BASH_VERSINFO[0]} ${BASH_VERSINFO[1]}"' \
2>/dev/null || echo 0 0)
if [ "$(( ${major:-0} * 100 + ${minor:-0} ))" -ge 501 ] 2>/dev/null; then
echo "$candidate"; return 0
fi
fi
command -v nix >/dev/null 2>&1 || return 0
nix shell nixpkgs#bash --command bash -c 'command -v bash' 2>/dev/null || true
}
# Every shell script in scripts/ says `#!/usr/bin/env bash` and nothing else. # Every shell script in scripts/ says `#!/usr/bin/env bash` and nothing else.
# `#!/bin/sh` is dash in CI and bash 3.2 in sh-mode on macOS: a second platform # `#!/bin/sh` is dash in CI and bash 3.2 in sh-mode on macOS: a second platform
# split, undetected for the same reason the first one was. # split, undetected for the same reason the first one was.
@@ -94,19 +76,16 @@ suites() {
done done
} }
# $1 the pinned bash, $2 the suite file. The suite runs with the pinned bash # $1 the suite file, run by the bash running this script, with PATH unchanged.
# FIRST on PATH, so a script it invokes through its own `#!/usr/bin/env bash`
# -- or through `sh` -- gets the pinned one too, not the system 3.2.
run_suite() { run_suite() {
local pinned="$1" file="$2" local file="$1"
local bindir; bindir="$(dirname "$pinned")"
local -a env_args=(-i) local -a env_args=(-i)
local v local v
for v in $ALLOW ${CHECKERS_KEEP:-}; do for v in $ALLOW ${CHECKERS_KEEP:-}; do
if [ -n "${!v+set}" ]; then env_args+=("$v=${!v}"); fi if [ -n "${!v+set}" ]; then env_args+=("$v=${!v}"); fi
done done
env_args+=("PATH=$bindir:${PATH:-/usr/bin:/bin}") env_args+=("PATH=$PATH")
env "${env_args[@]}" "$pinned" "$file" env "${env_args[@]}" "$BASH" "$file"
} }
main() { main() {
@@ -123,15 +102,7 @@ main() {
shift shift
done done
local pinned; pinned="$(find_modern_bash)" printf 'checkers: bash %s at %s\n' "$BASH_VERSION" "$BASH"
if [ -z "$pinned" ]; then
echo 'checkers: no bash >= 5.1 on PATH and nix cannot supply one.' >&2
echo ' Enter the dev shell (direnv allow / nix develop) and retry.' >&2
return 3
fi
printf 'checkers: bash %s at %s\n' \
"$("$pinned" -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"')" "$pinned"
local rc=0 local rc=0
check_shebangs || rc=1 check_shebangs || rc=1
@@ -149,7 +120,7 @@ main() {
rc=4; continue rc=4; continue
fi fi
printf '\n=== %s ===\n' "$n" printf '\n=== %s ===\n' "$n"
run_suite "$pinned" "$file" || rc=1 run_suite "$file" || rc=1
done done
return $rc return $rc
} }
+1 -1
View File
@@ -87,7 +87,7 @@ ledger_path() {
# first line; bytes alone are not enough, since a restore can reproduce them. # first line; bytes alone are not enough, since a restore can reproduce them.
file_stamp() { file_stamp() {
printf '%s %s' "$(stamp_of "$1")" \ printf '%s %s' "$(stamp_of "$1")" \
"$(stat -c '%i %.9Y' "$1" 2>/dev/null || stat -f '%i %Fm' "$1")" "$(stat -c '%i %.9Y' "$1")"
} }
ledger_holds() { [ -f "$1" ] && [ "$(head -n 1 "$1")" = "% kb-stamp $(file_stamp "$2")" ]; } ledger_holds() { [ -f "$1" ] && [ "$(head -n 1 "$1")" = "% kb-stamp $(file_stamp "$2")" ]; }