Reach the tools this script declares, and fail in seconds when it cannot
Three call sites ran OUTSIDE guix shell and therefore never consulted manifest.scm at all. Declaring a tool and reaching it are different things, and the evidence is that file and jq were ALREADY declared and still died on an azoth runner:
release-build.sh: line 282: file: command not foundWrapped, with the reason at each site:
204 test-cli-exit-contract.sh - needs python3 for check [4]
282 file - asserts artifact type after each cross build
305 generate-release-metadata - needs jq, and runs AFTER all ten cross
builds, so a miss surfaces ~90 minutes inThe metadata site passes its variable through env INSIDE the shell rather than as a prefix outside it. Both forms work for a plain guix shell, which preserves the environment; only this one also survives SIGILRELEASECONTAINER=1, where guix shell -C does not.
Added a preflight over the empirically-established at-risk set. The list is not imagined: it is what remained after taking every command token out of release-build.sh, test-cli-exit-contract.sh, generate-release-metadata.sh and with-zig and testing each against a runner guest's PATH. An earlier bound assembled from a hand-picked list reported one tool when there were four.
THE PREFLIGHT'S FIRST VERSION WAS FALSE-GREEN AND THE SABOTAGE TEST IS THE ONLY REASON THIS IS NOT SHIPPING BROKEN. It probed with the same non-pure shell as the build, so against a manifest containing no python at all it reported OK, having silently resolved the developer's own copy:
python3 -> /home/daviwil/.guix-home/profile/bin/python3The defect it exists to catch, passing its own test. A non-pure probe measures the machine it runs on; the question that matters is whether the MANIFEST supplies the tool, because that is what a runner has and the developer laptop is the one host where the answer is accidentally yes. The probe is now --pure.
Gated both directions, and the RED names the tool while the other five still resolve, so the failure is attributable rather than a probe falling over:
modified manifest -> rc=0, all six from .../yl0dgc6q...-profile
pristine manifest -> rc=1, "not supplied by manifest.scm: python3"KNOWN LIMITATION, stated rather than discovered later: none of this is tested under SIGILRELEASECONTAINER=1. The script already carried that caveat on the registry-anchor gate; wrapping these three adds sites to an existing gap rather than opening a new one. Container mode remains unverified.
Not verified end to end: proving the exit-path gate now runs all 21 checks on a runner needs a full release build there, which additionally needs python in the runner's own package list. That is a separate change requiring a golden-image re-bake.
scripts/release-build.sh | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 92 insertions(+), 3 deletions(-)scripts/release-build.shmodified
GUIX=(guix shell -m manifest.scm --)fi# ---------------------------------------------------------------------------# PREFLIGHT — resolve every at-risk external tool BEFORE doing ninety minutes# of work that will need it.## WHY THIS EXISTS: this script died twice on an azoth runner, each time on a# different missing tool, each time after real work. `jq' is reached at the very# LAST step, after all ten cross builds. Fixing tools one at a time as they# surface costs a full run per tool; enumerating the class costs seconds.## THE TOOL LIST IS EMPIRICAL, NOT IMAGINED. It is the set found by taking every# command token out of release-build.sh, test-cli-exit-contract.sh,# generate-release-metadata.sh and with-zig, keeping those that resolve as real# commands, and testing each against a runner guest's PATH (2026-07-30). An# allowlist assembled from memory answers "are THESE tools present?" and cannot# answer "what is missing?" -- the earlier bound was produced that way and was# wrong, reporting one tool when there were four.## The ambient-only survivors (sha256sum, awk, cp, mkdir, rm, grep) are NOT# listed: they come from %base-packages and were empirically present on the# runner. Only the manifest-supplied set is at risk, and that is what this# checks -- in the wrapped environment, which is where those call sites now run.## `which' IS FIRST ON PURPOSE. It is the instrument, and it is declared in# manifest.scm like everything else. If it is ever absent, every lookup below# fails and the whole list reports MISSING -- which is loud and wrong in the# SAFE direction. A preflight that checks the easy dependencies and skips the# hard one is worse than no preflight, because it manufactures confidence.# ---------------------------------------------------------------------------## *** THE PROBE MUST BE --pure, AND THIS IS THE WHOLE POINT OF IT. ***## The first version of this preflight used "${GUIX[@]}", i.e. the same non-pure# shell as the build. It reported a confident OK against a manifest with no# python in it, because a non-pure `guix shell' inherits the ambient PATH and it# had quietly resolved the developer's own copy:## python3 -> /home/daviwil/.guix-home/profile/bin/python3## That is the exact defect this preflight exists to catch, passing its own test.# A non-pure probe measures THE MACHINE IT RUNS ON; the question that matters is# whether the MANIFEST supplies the tool, because that is what a runner guest# will have and the developer laptop is the one host where the answer is# accidentally yes. Caught only by deliberately sabotaging the manifest and# watching the gate stay green -- it was not visible by reading it.PREFLIGHT_GUIX=(guix shell --pure -m manifest.scm --)info "Preflight: resolving external tools in the build environment"preflight_missing=""for tool in which python3 file jq curl minisign; do if resolved=$("${PREFLIGHT_GUIX[@]}" which "$tool" 2>/dev/null); then printf ' %-9s -> %s\n' "$tool" "$resolved" else printf ' %-9s -> MISSING\n' "$tool" preflight_missing="$preflight_missing $tool" fidone[[ -z "$preflight_missing" ]] \ || die "preflight: not supplied by manifest.scm:$preflight_missing"echo " preflight: OK"# ---------------------------------------------------------------------------# Bootstrap (mirrors the CI bootstrap step)# ---------------------------------------------------------------------------# topics/sigil-cli-exit-path-contract.# ---------------------------------------------------------------------------info "Exit-path contract gate (static release binary)""$SRC/test/integration/test-cli-exit-contract.sh" "$STATIC_BIN" \# WRAPPED, and the absence of this wrapper was the bug. Check [4] of this gate# shells out to python3 to hold a port open. Unwrapped, python3 resolved from the# AMBIENT PATH -- which a developer laptop happens to carry and a runner guest# does not. Measured on an azoth runner 2026-07-30:## test-cli-exit-contract.sh: line 356: python3: command not found# exit-path contract: 1 of 18 checks FAILED## and note the denominator MOVED: 18 attempted there against 21 with python3# present, because the three serve-on-an-already-bound-port assertions never ran# at all. "1 of 18 FAILED" reads as "17 fine"; the truth was three checks# silently missing from the count."${GUIX[@]}" "$SRC/test/integration/test-cli-exit-contract.sh" "$STATIC_BIN" \ || die "exit-path contract gate FAILED on the release binary"echo " exit-path contract: OK" [[ "$config" == windows-* ]] && src_bin="$src_bin.exe" [[ -f "$src_bin" ]] || die "expected binary missing: $src_bin" cp "$src_bin" "$OUT/$(artifact_name "$config")" file "$OUT/$(artifact_name "$config")" # WRAPPED for the same reason as the exit-path gate above. `file' IS declared # in manifest.scm and STILL died here on the azoth runner -- # "release-build.sh: line 282: file: command not found" -- because an # unwrapped call site never consults the manifest at all. Declaring a tool and # reaching it are two different things. "${GUIX[@]}" file "$OUT/$(artifact_name "$config")"done# The glibc linux-amd64 binary can run on an FHS glibc x86_64 host. Skipinfo "Generating checksums and release metadata"(cd "$OUT" && sha256sum sigil-* > SHA256SUMS)RELEASE_REPO_URL="https://codeberg.org/sigil/sigil" \# WRAPPED: generate-release-metadata.sh uses `jq', which is declared in# manifest.scm and was still unreachable here for the same reason `file' was.## The variable is passed through `env' INSIDE the shell rather than as a# `VAR=x "${GUIX[@]}" ...' prefix outside it. Both work for a plain `guix shell',# which preserves the environment; only this one also survives# SIGIL_RELEASE_CONTAINER=1, where `guix shell -C' does not. Removing the# ambiguity beats getting it right for one of the two modes.## ORDERING NOTE, because it cost most of a run once: this is the LAST tool the# script needs and it runs AFTER all ten cross builds. A missing tool here# surfaces roughly ninety minutes in. That is the argument for the preflight# below rather than for fixing tools one at a time as they fail."${GUIX[@]}" env RELEASE_REPO_URL="https://codeberg.org/sigil/sigil" \ "$SRC/scripts/generate-release-metadata.sh" "$VERSION" "$OUT" > "$OUT/v$VERSION.json"cp "$OUT/v$VERSION.json" "$OUT/latest.json"