cross-remote.sh: the default cross-build runs again (a trailing comment had swallowed the defaults line); the class gets a CI check

The shipper's report: with no arguments, cross-remote.sh died under the Mac's bash 3.2 with 'CARGO_ARGS: unbound variable' and
its chain kept the previous exes. Cause: a comment appended to the defaults line in the box-work merge turned OUT, COMPARE,
TARGET_DIR and CARGO_ARGS=() into comment text, so the array was never declared (the same shape build-remote.sh had at 19:5x).
Fix: the line split, and the default tests use [ -n "${CARGO_ARGS[*]:-}" ] in both tools, safe whether or not the array
exists. Proof: the default cross-build from a fork worktree under /bin/bash 3.2.57 built both exes (igneumd.exe 8f7e2ae3...,
igneum-miner.exe 6f8b49d8...). tools/ci/defaults-line-check.sh fails CI on a line where a comment start is followed by an
assignment list (quoted strings, ${...}, $# and [^#] dropped first); self-test fires on the swallowed shape and passes
${a#b}, sed s#x#y#, $# loops and prose mentions; the tree is clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-josh 2026-10-06 21:55:42 +01:00
parent 0445a51d77
commit 36e4ee7c5a
4 changed files with 70 additions and 8 deletions

View file

@ -95,6 +95,8 @@ jobs:
run: bash tools/ci/commit-string-check.sh --self-test
- name: build server remote checkout self-test (the stale-overlay class of 6 October 2026)
run: bash infra/build-server/remote-run.sh --self-test
- name: no shell assignment hides behind a trailing comment (the swallowed-defaults class of 6 October 2026)
run: bash tools/ci/defaults-line-check.sh --self-test && bash tools/ci/defaults-line-check.sh
- name: no secret file names and no 64-hex secrets in the tree (self-test first, then the tree)
run: bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh
- name: faucet unit tests (validation, the daily limits, the signed transaction; keccak, RLP and secp256k1 vectors)

View file

@ -64,7 +64,7 @@ while [ $# -gt 0 ]; do
esac
done
[ "${CARGO_ARGS[0]:-}" = cargo ] && CARGO_ARGS=("${CARGO_ARGS[@]:1}")
CARGO_ARGS_GIVEN=""; [ "${#CARGO_ARGS[@]}" -gt 0 ] && CARGO_ARGS_GIVEN=1
CARGO_ARGS_GIVEN=""; [ -n "${CARGO_ARGS[*]:-}" ] && CARGO_ARGS_GIVEN=1
bs_host
bs_context
@ -106,16 +106,16 @@ fi
# defaults per crate
case "$BS_KIND:$BS_CRATE_REL" in
node:*)
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features kaspad/igneum-pow)
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features kaspad/igneum-pow)
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneumd $TARGET_DIR/release/igneum-miner" ;;
repo:app/igneum-app)
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release)
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release)
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneum-app $TARGET_DIR/release/igneum-ota-sign $TARGET_DIR/release/igneum-prove-verify" ;;
repo:proving/igneum-prove)
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release)
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release)
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneum-prove-host $TARGET_DIR/release/igneum-prove-export" ;;
*)
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release) ;;
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release) ;;
esac
case "${CARGO_ARGS[0]}" in build) ;; *) [ -n "${ARTEFACTS_SET:-}" ] || { FETCH=0; ARTEFACTS=""; } ;; esac # test, check, clippy: nothing to fetch
[ -n "$OUT" ] || OUT="$BS_CRATE/target-remote"

57
tools/ci/defaults-line-check.sh Executable file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env bash
# The swallowed-defaults class (6 October 2026, twice in one evening): a comment appended to a line of shell assignments
# (`JOBS=...; # ... OUT=""; CARGO_ARGS=()`) turns every assignment after the `#` into comment text; the script still parses,
# bash -n and shellcheck say nothing, and the first use of the undeclared array dies under the Mac's bash 3.2 with
# "unbound variable" (build-remote.sh at 19:5x UTC, cross-remote.sh at 21:xx UTC: the shipper's default cross-build never ran
# and its chain kept the previous exes). Rule: no `#` comment on a line that carries shell assignments after it.
# The scan (python3, which every CI host has) first drops quoted strings, ${...} expansions, $# and [^#] so a `#` inside them
# is not a comment, then flags a line where a comment start (start of line or whitespace, then #) is followed by `NAME=`.
#
# tools/ci/defaults-line-check.sh # exit 1 with file:line on a hit
# tools/ci/defaults-line-check.sh --self-test # fires on the swallowed shape, passes clean ones (including ${a#b} and sed s#x#y#)
set -euo pipefail
cd "$(dirname "$0")/../.."
scan() { # file names as arguments (python3 - takes its script from stdin, so stdin cannot carry the list), file:line per hit, exit 1 if any
python3 - "$@" <<'PY'
import re, sys
strip = [
(re.compile(r"\$\{[^}]*\}"), ""), # ${var#pat}, ${var%pat}, ${!i}
(re.compile(r"\$#"), ""), # the argument count
(re.compile(r"\[\^#\]"), ""), # a bracket expression excluding #
(re.compile(r"'[^']*'"), "''"), # single-quoted strings
(re.compile(r'"(?:[^"\\]|\\.)*"'), '""'), # double-quoted strings
(re.compile(r"\\#"), ""), # an escaped #
]
# the swallowed shape is an assignment LIST after the comment start: `NAME=...;` or `NAME=()`; a prose mention such as
# "(access public|private, private only in NET_MODE=private)" or "OTA_SKIP=1 (the default now)" has neither
hit = re.compile(r"(^|\s)#.*\s[A-Za-z_][A-Za-z0-9_]*=(\(\)|[^;()]*;)")
bad = 0
for path in sys.argv[1:]:
try: lines = open(path, encoding="utf-8", errors="replace").read().split("\n")
except OSError: continue
for n, line in enumerate(lines, 1):
s = line
for rx, rep in strip: s = rx.sub(rep, s)
if s.lstrip().startswith("#"): continue # a whole-line comment may say anything
if hit.search(s):
print(f"defaults-line: {path}:{n}: a comment swallows assignments after it: {line.strip()[:140]}"); bad = 1
sys.exit(bad)
PY
}
if [ "${1:-}" = --self-test ]; then
t=$(mktemp -d); trap 'rm -rf "$t"' EXIT
printf '%s\n' 'JOBS="${JOBS:-}"; # empty = the box decides OUT=""; CARGO_ARGS=()' > "$t/bad.sh"
printf '%s\n' 'JOBS="${JOBS:-}"; OUT=""; CARGO_ARGS=() # one line, nothing assigned after the comment' \
'ver=${tarball#node-}; ver=${ver%-linux-x64.tar.xz}' \
"epoch=\$(sed -n 's/^#define X \"\\(.*\\)\"/\\1/p' \"\$ph\"); day=\$(sed -n 's/^#define Y/x/p')" \
'rel="${a#"$TARGET_DIR"/}"; dest="$OUT/$rel"' \
'for ((i=1;i<=$#;i++)); do fee="${!i}"; done' \
"A=\"\$(find . | sed 's#^\\./##' | sort)\"; B=1" \
'hetzner_create_one() { # name type location [role] [access] (access public|private, private only in NET_MODE=private)' \
'if [ "${OTA_SKIP:-}" = 0 ]; then SIGN=1; fi # the old spelling; OTA_SKIP=1 (the default now) leaves the manifest alone' \
'# a whole-line comment with OUT=""; CARGO_ARGS=() in it' > "$t/good.sh"
if scan "$t/bad.sh" >/dev/null; then echo "defaults-line self-test: the swallowed shape did NOT fire"; exit 1; fi
if scan "$t/good.sh"; then echo "defaults-line self-test: fires on the swallowed shape, passes the clean shapes"; exit 0; else echo "defaults-line self-test: a clean shape fired"; exit 1; fi
fi
# shellcheck disable=SC2046 # paths in this tree carry no spaces
if scan $(git ls-files 'tools/*.sh' 'tools/**/*.sh' 'infra/**/*.sh' 'proto-cuda/**/*.sh' 'packaging/**/*.sh'); then echo "defaults-line: no assignment hides behind a comment"; else exit 1; fi

View file

@ -37,7 +37,10 @@ BS_TOOL=cross-remote
. "$HERE/../infra/build-server/lib.sh"
TARGET=x86_64-pc-windows-gnu
JOBS="${JOBS:-}"; # empty = the box decides: 90 alone, 45 beside another slot holder (remote-run.sh, main's ruling 6 Oct 2026) OUT=""; COMPARE=""; TARGET_DIR="target"; CARGO_ARGS=()
# JOBS empty = the box decides: 90 alone, 45 beside another slot holder (remote-run.sh, main's ruling 6 Oct 2026). One line, no
# trailing comment: a `#` on this line once swallowed every assignment after it, CARGO_ARGS was never declared and the default
# cross-build died with "unbound variable" under the Mac's bash 3.2 (the shipper, 6 Oct 2026, 21:xx UTC)
JOBS="${JOBS:-}"; OUT=""; COMPARE=""; TARGET_DIR="target"; CARGO_ARGS=()
while [ $# -gt 0 ]; do
case "$1" in
--jobs) JOBS="$2"; shift 2 ;;
@ -55,10 +58,10 @@ bs_host
bs_context
case "$BS_KIND:$BS_CRATE_REL" in
node:*)
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features igneum-pow --target "$TARGET")
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features igneum-pow --target "$TARGET")
EXES="igneumd.exe igneum-miner.exe" ;;
repo:app/igneum-app)
[ "${#CARGO_ARGS[@]}" -gt 0 ] || CARGO_ARGS=(build --release --target "$TARGET")
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release --target "$TARGET")
EXES="igneum-app.exe igneum-ota-sign.exe igneum-prove-verify.exe" ;;
*) bs_die "cross-remote builds the fork (run from a vendor/igneum-node* worktree) or app/igneum-app, not $BS_CRATE_REL" ;;
esac