igneum/tools/build-remote.sh
igneum-labs d60d27525b Merge remote-tracking branch 'box/master' into scrub
# Conflicts:
#	CLAUDE.md
#	docs/plans/counter-asic-3-status.md
#	docs/plans/release-0.3.21.md
#	docs/plans/release-0.3.22.md
#	tools/ci/merge-to-master.sh
#	tools/ci/pre-push.sh
#	tools/ci/red-watch.mjs
2026-10-07 19:16:14 +00:00

267 lines
23 KiB
Bash
Executable file

#!/usr/bin/env bash
# Build on igneum-build-1 instead of this Mac. Run from any crate directory of any worktree on the Mac (a fork worktree under
# vendor/, igneum-pow, app/igneum-app, proving/igneum-prove): the sources go to /srv/builds/<worktree>/<same relative path> on
# the box (HEAD through the bare mirror, uncommitted changes by rsync, changed files re-stamped with touch in lib.sh's
# bs_overlay_dir: the copied-sources rule), the cargo command runs there
# with sccache and -j 90 under one of the box's build slots, and the artefacts come back into target-remote/ here.
#
# tools/build-remote.sh the default command for this crate (below)
# tools/build-remote.sh -- build --release -p kaspad --features kaspad/igneum-pow
# tools/build-remote.sh -- test --release -p kaspa-consensus-core --lib
# tools/build-remote.sh --artefacts "target/release/igneumd" --out /tmp/x -- build --release -p kaspad --features kaspad/igneum-pow
# tools/build-remote.sh --jobs 48 -- check
# tools/build-remote.sh --target-dir target-exp -- build --release another persistent target dir on the box
# tools/build-remote.sh --no-fetch -- clippy --all-targets nothing comes back (tests, check, clippy)
# tools/build-remote.sh --ship [hive|rig|seed|linux] [--glibc X.Y] anything that SHIPS: cargo zigbuild for
# x86_64-unknown-linux-gnu.<glibc> where the glibc comes
# from the class (hive/rig 2.31: HiveOS is Ubuntu 20.04
# based; seed/linux 2.35: Debian 12 and Ubuntu 22.04;
# default seed; --glibc overrides)
# (zig as the C/C++ toolchain, as the Mac's
# infra/cross/build-linux.sh), artefacts from
# target/x86_64-unknown-linux-gnu/release, each checked by
# tools/ci/glibc-ceiling-check.sh (need at most <glibc>).
# A plain build is native glibc 2.39: the box, the fleet's
# Ubuntu 24.04 hosts, never a seed or a rig (7 Oct 2026:
# a seed took 14 restarts on a 2.39 binary).
# tools/build-remote.sh --priority gate -- test ... a RELEASE GATE (the app gate, the canary cut, the pre-push
# self-tests): nice 0, the full core set, a slot ahead of
# queued suites and benches
# tools/build-remote.sh --plan [--priority gate] -- <cargo args> print the resolved class (kind, nice, cores, jobs) and stop;
# no box, no crate needed (the CI check uses it)
#
# Scheduling (main, 7 October 2026, after a load of 190 on 96 threads: a release join bench and the 0.3.19 app gate starving each
# other and "builds" of 16 minutes): every SUITE (cargo test) and BENCH (cargo bench) runs under nice 10 on the last 32 cores with
# -j 32 unless the caller passes --priority gate; builds keep the box's own jobs rule (90 alone, 45 beside another slot holder); a
# gate runs at nice 0 on the full set and takes a slot ahead of queued suites and benches (remote-run.sh gate-pending marker). The
# class travels in the slot label ("; kind=suite nice=10 cores=32") and the JSONL line, so the dashboard's job card shows why a
# job is slow. A call without a priority flag defaults to the bounded class for suites and benches: tools/ci/build-kind-default-check.sh.
# tools/build-remote.sh --self-test-repro [--full] from a fork worktree: igneum-miner built twice a minute
# apart without sccache into one target dir must give one
# sha256, and a per-run target path must not (prost's
# OUT_DIR); --full adds kaspad with libmimalloc-sys
# recompiled between the builds (mimalloc's __DATE__), with
# and without SOURCE_DATE_EPOCH. Takes 2 to 6 min on the box.
#
# Sources: HEAD through the bare mirror, uncommitted changes by rsync --checksum, every written file re-stamped with touch in
# lib.sh's bs_overlay_dir (the copied-sources rule; tools/ci/copied-sources-check.sh reads this file for that word).
# Reproducible: every command runs with SOURCE_DATE_EPOCH = the commit's author time and TZ=UTC, into ONE fixed target directory per
# target (lib.sh bs_repro_env; main's rule of 6 October 2026 from the 0.3.14 repro: prost embeds OUT_DIR, mimalloc embeds the date).
#
# Defaults by crate: a fork worktree builds `-p kaspad -p igneum-miner --features kaspad/igneum-pow` in release and fetches
# target/release/{igneumd,igneum-miner} (what packaging/README-ship.md and infra/cross expect); app/igneum-app builds release
# and fetches igneum-app, igneum-ota-sign, igneum-prove-verify; proving/igneum-prove fetches igneum-prove-host and
# igneum-prove-export; any other crate builds release and fetches nothing unless --artefacts names files.
#
# Artefacts land in <crate>/target-remote/<path without the leading target/> (target-remote/release/igneumd), NEVER in
# target/: the box builds x86_64 Linux ELF binaries (glibc 2.39, Ubuntu 24.04), which do not run on this Mac. Each one is
# reported with size and sha256. For Windows exes use tools/cross-remote.sh.
#
# Slots: the box has its own slot files (/srv/builds/_locks/build-<k>, count in /srv/builds/_locks/slots, default 1); this
# script takes one of THOSE, never the Mac's ~/.config/igneum/build-slots or tools/lock/with-lock.sh, so a remote build does
# not hold a Mac slot. A build waits up to 2 h for a remote slot, as with-lock.sh does.
#
# Needs: ~/.config/igneum/build-server (build@<ip>, written by infra/build-server/run-from-mac.sh), ~/.ssh/igneum_ed25519,
# the same rustc version on both sides (refused otherwise; IGNEUM_TOOLCHAIN_MISMATCH=ok overrides). IGNEUM_AGENT names the
# agent in the slot-file label and the box's JSONL log (/srv/builds/_log/builds.jsonl); default the worktree name.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC2034 # shared with the scripts that source lib.sh
BS_TOOL=build-remote
# shellcheck source=../infra/build-server/lib.sh
. "$HERE/../infra/build-server/lib.sh"
{ # whole-body: bash parses this block entirely before running a line of it, so an edit to this file while a run is in
# flight cannot reach the running copy (7 Oct 2026: build-remote.sh was edited mid-run and died on shifted bytes after a 4-min build)
# JOBS empty = the box decides: 90 alone, 45 beside another slot holder (remote-run.sh, main's ruling 6 Oct 2026)
JOBS="${JOBS:-}"; OUT=""; ARTEFACTS=""; TARGET_DIR="target"; FETCH=1; CARGO_ARGS=(); SELFTEST=0; FULL=0; SHIP=0; SHIP_CLASS="${SHIP_CLASS:-seed}"; PRIORITY="${PRIORITY:-normal}"; PLAN=0; BOX="${BOX:-}"; GLIBC="${GLIBC:-}"
while [ $# -gt 0 ]; do
case "$1" in
--jobs) JOBS="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--artefacts) ARTEFACTS="$2"; ARTEFACTS_SET=1; shift 2 ;;
--target-dir) TARGET_DIR="$2"; shift 2 ;;
--no-fetch) FETCH=0; shift ;;
--self-test-repro) SELFTEST=1; shift ;;
--ship) SHIP=1; case "${2:-}" in hive|rig|seed|linux|native) SHIP_CLASS="$2"; shift 2 ;; *) shift ;; esac ;;
--priority) PRIORITY="$2"; shift 2 ;;
--box) BOX="$2"; BOX_GIVEN=1; shift 2 ;;
--gate) PRIORITY=gate; shift ;;
--plan) PLAN=1; shift ;;
--glibc) GLIBC="$2"; shift 2 ;;
--full) FULL=1; shift ;;
--) shift; CARGO_ARGS=("$@"); break ;;
-h|--help) sed -n '2,32p' "$0"; exit 0 ;;
*) CARGO_ARGS=("$@"); break ;;
esac
done
[ "${CARGO_ARGS[0]:-}" = cargo ] && CARGO_ARGS=("${CARGO_ARGS[@]:1}")
CARGO_ARGS_GIVEN=""; [ -n "${CARGO_ARGS[*]:-}" ] && CARGO_ARGS_GIVEN=1
# the scheduling class, from the cargo subcommand and the priority flag alone (no box, no crate): BR_NICE, BR_CORES (the last N of the
# box's 96; 0 = the full set), BR_JOBS_CAP (0 = the box's rule), BR_PRIORITY; the kind for the log is refined after bs_context
BR_NICE=0; BR_CORES=0; BR_JOBS_CAP=0; BR_PRIORITY="$PRIORITY"; SCHED_CLASS=build
case "$PRIORITY" in gate|normal) ;; *) bs_die "--priority takes gate or normal, not '$PRIORITY'" ;; esac
case "${CARGO_ARGS[0]:-build}" in
test) SCHED_CLASS=suite ;;
bench) SCHED_CLASS=bench ;;
check|clippy) SCHED_CLASS=check ;;
*) SCHED_CLASS=build ;;
esac
if [ "$PRIORITY" = gate ]; then BR_NICE=0; BR_CORES=0; BR_JOBS_CAP=0
elif [ "$SCHED_CLASS" = suite ] || [ "$SCHED_CLASS" = bench ]; then BR_NICE=10; BR_CORES="${IGNEUM_BOUND_CORES:-88}"; BR_JOBS_CAP="${IGNEUM_BOUND_CORES:-88}"; fi # the founder, 7 Oct 2026 19:4x BST: load both boxes to near max; 88 of 96, 8 reserved
# an explicit --jobs above the bounded class's cap is clamped (main's rule: bounded unless a priority flag; 7 Oct 2026: two suites
# ran at -j 90 on a box at load 190 because their callers passed --jobs 90)
if [ "$BR_JOBS_CAP" -gt 0 ] && [ -n "$JOBS" ] && [ "$JOBS" -gt "$BR_JOBS_CAP" ]; then bs_log "--jobs $JOBS clamped to $BR_JOBS_CAP for a $SCHED_CLASS (pass --priority gate for the full set)"; JOBS=$BR_JOBS_CAP; fi
export BR_NICE BR_CORES BR_JOBS_CAP BR_PRIORITY
if [ "$PLAN" = 1 ]; then
k="$SCHED_CLASS"; [ "$PRIORITY" = gate ] && k=gate
printf 'kind=%s nice=%s cores=%s jobs=%s priority=%s\n' "$k" "$BR_NICE" "$( [ "$BR_CORES" = 0 ] && echo 96 || echo "$BR_CORES")" "$( [ "$BR_JOBS_CAP" = 0 ] && echo box || echo "$BR_JOBS_CAP")" "$PRIORITY"
exit 0
fi
# the box: --box N pins it; else the class's PREFERRED box with spill-over (lib.sh bs_route_spill, the founder 7 Oct 2026: a build or gate
# prefers box 1, a suite or bench box 2, a proving crate box 3; a preferred box with no free slot or a 1-minute load above 64 hands
# the job to the other box when that one qualifies; the decision line is printed here and lands in the JSONL row as "route")
ROUTE_CLASS="$SCHED_CLASS"; [ "$PRIORITY" = gate ] && ROUTE_CLASS=gate
if [ -z "$BOX" ]; then bs_route_spill "$ROUTE_CLASS"; BOX=$BS_ROUTE_BOX; else BR_ROUTE_PREF=$BOX BR_ROUTE_BOX=$BOX BR_ROUTE_SPILLED=0 BR_ROUTE_REASON="box $BOX by --box"; export BR_ROUTE_PREF BR_ROUTE_BOX BR_ROUTE_SPILLED BR_ROUTE_REASON; fi
bs_host "$BOX"
bs_context
# a proving crate prefers box 3 unless the caller chose (its builds and suites alike; the same spill-over)
if [ "$BS_CRATE_REL" = proving/igneum-prove ] && [ -z "${BOX_GIVEN:-}" ] && [ "$PRIORITY" != gate ]; then bs_route_spill prove; [ "$BS_ROUTE_BOX" != "$BOX" ] && { BOX=$BS_ROUTE_BOX; bs_host "$BOX"; }; fi
# box 2 keeps the suites' numbers: everything that lands there runs at the bounded class (nice 10, a 32-core band, -j 32), builds
# and gates included (main, 7 Oct 2026); a gate still takes its slot ahead of queued suites
if [ "$BOX" = 2 ] && [ "$BR_CORES" = 0 ]; then BR_NICE=10; BR_CORES="${IGNEUM_BOUND_CORES:-88}"; BR_JOBS_CAP="${IGNEUM_BOUND_CORES:-88}"; export BR_NICE BR_CORES BR_JOBS_CAP; bs_log "bounded on box 2 (nice 10, the ${BR_CORES}-core band, -j $BR_JOBS_CAP) so a suite beside it keeps its number"; fi
if [ "$BR_JOBS_CAP" -gt 0 ] && [ -n "$JOBS" ] && [ "$JOBS" -gt "$BR_JOBS_CAP" ]; then bs_log "--jobs $JOBS clamped to $BR_JOBS_CAP on box $BOX"; JOBS=$BR_JOBS_CAP; fi
bs_log "box $BOX ($BS_HOST) for class $SCHED_CLASS, priority $PRIORITY$( [ "${BR_ROUTE_SPILLED:-0}" = 1 ] && echo ", SPILLED from box $BR_ROUTE_PREF")"
if [ "$SELFTEST" = 1 ]; then
[ "$BS_KIND" = node ] || bs_die "--self-test-repro runs from a fork worktree (igneum-miner and kaspad live there)"
bs_toolchain_check; bs_sync_sources; bs_log "sources in place (changed files re-stamped with touch by bs_overlay_dir)"
# the remote script: no sccache (RUSTC_WRAPPER empty overrides the box's cargo config), the env from bs_repro_env, one fixed target
# dir `target-repro`; between the two builds the generated-code crate (prost, OUT_DIR) is cleaned so it is regenerated, and the
# clock is let past the next minute boundary so a __DATE__/__TIME__ stamp would differ
# RUSTC_WRAPPER=/usr/bin/env is a true pass-through: cargo treats an EMPTY value as unset and would fall back to the box's
# configured sccache, which is what hid both classes in the 0.3.14 repro
cmd="$(bs_repro_env)export RUSTC_WRAPPER=/usr/bin/env; set -u
sha() { sha256sum \"\$1\" | cut -c1-64; }
wait_minute() { local m0; m0=\$(date +%M); while [ \"\$(date +%M)\" = \"\$m0\" ]; do sleep 2; done; }
build() { local f=\"\"; [ \"\$2\" = kaspad ] && f=\"--features kaspad/igneum-pow\"; CARGO_TARGET_DIR=\"\$1\" cargo build --release -p \"\$2\" \$f > /tmp/repro-build.log 2>&1 || { echo \"self-test: cargo build -p \$2 into \$1 FAILED:\"; tail -5 /tmp/repro-build.log; exit 1; }; }
rc=0
echo \"self-test: SOURCE_DATE_EPOCH=\$SOURCE_DATE_EPOCH TZ=\$TZ, RUSTC_WRAPPER=\$RUSTC_WRAPPER (sccache off), \$(rustc --version)\"
build target-repro igneum-miner; a=\$(sha target-repro/release/igneum-miner)
wait_minute; CARGO_TARGET_DIR=target-repro cargo clean -q --release -p kaspa-grpc-core -p igneum-miner; build target-repro igneum-miner; b=\$(sha target-repro/release/igneum-miner)
if [ \"\$a\" = \"\$b\" ]; then echo \"self-test: igneum-miner twice a minute apart, one target dir: MATCH \$a\"; else echo \"self-test: igneum-miner DIFFERS across a minute: \$a vs \$b\"; rc=1; fi
build target-repro-\$\$ igneum-miner; c=\$(sha target-repro-\$\$/release/igneum-miner); rm -rf target-repro-\$\$
if [ \"\$a\" != \"\$c\" ]; then echo \"self-test: a per-run target path gives a different igneum-miner (prost OUT_DIR), as expected: \$c\"; else echo \"self-test: a per-run target path gave the SAME bytes; the OUT_DIR class no longer reproduces (the rule still stands)\"; fi
if [ $FULL = 1 ]; then
build target-repro kaspad; d=\$(sha target-repro/release/igneumd)
wait_minute; CARGO_TARGET_DIR=target-repro cargo clean -q --release -p libmimalloc-sys -p kaspad; build target-repro kaspad; e=\$(sha target-repro/release/igneumd)
if [ \"\$d\" = \"\$e\" ]; then echo \"self-test: kaspad with mimalloc recompiled a minute later: MATCH \$d\"; else echo \"self-test: kaspad DIFFERS with mimalloc recompiled: \$d vs \$e\"; rc=1; fi
unset SOURCE_DATE_EPOCH; wait_minute; CARGO_TARGET_DIR=target-repro cargo clean -q --release -p libmimalloc-sys -p kaspad; build target-repro kaspad; f=\$(sha target-repro/release/igneumd)
if [ \"\$d\" != \"\$f\" ]; then echo \"self-test: without SOURCE_DATE_EPOCH kaspad differs a minute later (mimalloc __DATE__), as expected: \$f\"; else echo \"self-test: without SOURCE_DATE_EPOCH kaspad was still identical (mimalloc's stamp did not move this time)\"; fi
fi
( exit \$rc )"
BR_KIND=check BR_COMMAND="self-test-repro" BR_TARGET=x86_64-unknown-linux-gnu BR_ARTEFACTS=""; export BR_KIND BR_COMMAND BR_TARGET BR_ARTEFACTS
set +e; bs_remote_run "$BS_REMOTE_CRATE" "$BS_WT/$BS_CRATE_REL self-test-repro" "$cmd" 2>&1 | grep -E '^self-test:|RESULT' | sed 's/^/ /' >&2; rc=${PIPESTATUS[0]}; set -e
bs_wt_unlock
[ "$rc" = 0 ] && bs_log "self-test-repro passed" || bs_die "self-test-repro FAILED (rc $rc)"
exit 0
fi
# --ship: the zig path and the target triple dir for the artefacts
SHIP_TARGET=x86_64-unknown-linux-gnu
if [ "$SHIP" = 1 ]; then
[ -n "$GLIBC" ] || GLIBC=$("$HERE/ci/glibc-ceiling-check.sh" --ceiling-of "$SHIP_CLASS") || bs_die "unknown ship class $SHIP_CLASS"
[ "$GLIBC" != native ] || bs_die "--ship native is a plain build: drop --ship"
TARGET_SUB="$SHIP_TARGET/release"
else TARGET_SUB="release"; fi
# defaults per crate
case "$BS_KIND:$BS_CRATE_REL" in
node:*)
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release -p kaspad -p igneum-miner --features kaspad/igneum-pow)
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/$TARGET_SUB/igneumd $TARGET_DIR/$TARGET_SUB/igneum-miner" ;;
repo:app/igneum-app)
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release)
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/$TARGET_SUB/igneum-app $TARGET_DIR/$TARGET_SUB/igneum-ota-sign $TARGET_DIR/$TARGET_SUB/igneum-prove-verify" ;;
repo:proving/igneum-prove)
[ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release)
[ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/$TARGET_SUB/igneum-prove-host $TARGET_DIR/$TARGET_SUB/igneum-prove-export" ;;
*)
[ -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
if [ "$SHIP" = 1 ]; then
[ "${CARGO_ARGS[0]}" = build ] || bs_die "--ship is for cargo build"
CARGO_ARGS=(zigbuild "${CARGO_ARGS[@]:1}" --target "$SHIP_TARGET.$GLIBC")
BR_TARGET_SHIP="$SHIP_TARGET.$GLIBC"
fi
[ -n "$OUT" ] || OUT="$BS_CRATE/target-remote"
bs_log "$BS_KIND crate $BS_WT/$BS_CRATE_REL at $BS_SHA ($BS_BRANCH) -> $BS_HOST:$BS_REMOTE_CRATE; cargo ${CARGO_ARGS[*]} -j ${JOBS:-auto}; target dir $TARGET_DIR"
# a fork build pairs with the igneum-pow of the igneum worktree it sits in (the fork's path dependency ../../../../igneum-pow), so the
# pairing is said on the first line and recorded (7 Oct 2026, 0.3.17: a fork at 12153428 under a master worktree failed in kaspa-pow
# four minutes in, "no associated function chain_program_shadow", because master's igneum-pow predates the release branch's)
if [ "$BS_KIND" = node ]; then
pair_branch=$(git -C "$BS_WT_ROOT" branch --show-current 2>/dev/null || true); pair_dirty=$(git -C "$BS_WT_ROOT" status --porcelain -- igneum-pow 2>/dev/null || true)
# no `[ ... ] && echo` inside an assignment's $( ): its false status is the assignment's status and set -e ends the script (7 Oct 2026, 03:0x UTC)
PAIR="igneum $(git -C "$BS_WT_ROOT" rev-parse --short HEAD) (${pair_branch:-detached})${pair_dirty:+ with uncommitted igneum-pow changes}"
bs_log "pairs with $PAIR: igneum-pow $(grep -m1 '^version' "$BS_WT_ROOT/igneum-pow/Cargo.toml" | sed 's/.*"\(.*\)".*/\1/')"
export BR_PAIRS_WITH="$PAIR"
fi
bs_toolchain_check
t_sync0=$(date +%s)
bs_sync_sources
bs_log "sources in place after $(( $(date +%s) - t_sync0 )) s (every changed file re-stamped with touch by bs_overlay_dir)"
# the fork's kaspa-build-info embeds the commit through a build script that, having once found no branch, emits no
# rerun-if-changed and is never run again by cargo (release-0.3.11 plan: `cargo clean -p kaspa-build-info` first); so when
# the commit the box builds differs from the last one built in this target dir, that one crate is cleaned (a relink, seconds)
pre=""
if [ "$BS_KIND" = node ] && { [ "${CARGO_ARGS[0]}" = build ] || [ "${CARGO_ARGS[0]}" = zigbuild ]; }; then
pre="[ \"\$(cat '.build-remote-sha-$TARGET_DIR' 2>/dev/null)\" = '$BS_SHA' ] || CARGO_TARGET_DIR='$TARGET_DIR' cargo clean -q --release -p kaspa-build-info 2>/dev/null; "
fi
cmd="$(bs_repro_env)${pre}CARGO_TARGET_DIR='$TARGET_DIR' cargo $(printf '%q ' "${CARGO_ARGS[@]}")${JOBS:+-j $JOBS} 2>&1 | tee -a '$BS_REMOTE_WT/.build-remote.log'; rc=\${PIPESTATUS[0]}; [ \$rc = 0 ] && echo '$BS_SHA' > '.build-remote-sha-$TARGET_DIR'; ( exit \$rc )" # a subshell exit: the runner reads \$? and still prints its RESULT line
label="$BS_WT/$BS_CRATE_REL cargo ${CARGO_ARGS[*]}"
BR_KIND=$(bs_kind build-remote "$( [ "${CARGO_ARGS[0]}" = zigbuild ] && echo build || echo "${CARGO_ARGS[0]}")"); BR_COMMAND="cargo ${CARGO_ARGS[*]}"; BR_TARGET="${BR_TARGET_SHIP:-x86_64-unknown-linux-gnu}"
[ "$SCHED_CLASS" = bench ] && BR_KIND=bench
[ "$PRIORITY" = gate ] && BR_KIND=gate
label="$label; kind=$BR_KIND nice=$BR_NICE cores=$( [ "$BR_CORES" = 0 ] && echo 96 || echo "$BR_CORES")"
[ "${BR_ROUTE_SPILLED:-0}" = 1 ] && label="$label; spilled from box $BR_ROUTE_PREF"
for ((i = 0; i < ${#CARGO_ARGS[@]}; i++)); do [ "${CARGO_ARGS[$i]}" = --target ] && BR_TARGET="${CARGO_ARGS[$((i + 1))]:-}"; done
BR_ARTEFACTS=""; [ "$FETCH" = 1 ] && BR_ARTEFACTS="$ARTEFACTS"
export BR_KIND BR_COMMAND BR_TARGET BR_ARTEFACTS
t0=$(date +%s)
set +e
bs_remote_run "$BS_REMOTE_CRATE" "$label" "$cmd" 2>&1 | tee "/tmp/build-remote-$$.log"
rc=${PIPESTATUS[0]}
set -e
secs=$(( $(date +%s) - t0 ))
result=$(grep -m1 '^build-remote: RESULT' "/tmp/build-remote-$$.log" || true); rm -f "/tmp/build-remote-$$.log"
if [ "$rc" != 0 ]; then bs_die "remote cargo failed (rc $rc) after $(bs_fmt_secs "$secs"); $result"; fi
bs_log "remote cargo ${CARGO_ARGS[0]} done in $(bs_fmt_secs "$secs") wall from the Mac; ${result#build-remote: RESULT }"
if [ "$FETCH" = 1 ] && [ -n "$ARTEFACTS" ]; then
mkdir -p "$OUT"
for a in $ARTEFACTS; do
rel="${a#"$TARGET_DIR"/}"; dest="$OUT/$rel"; mkdir -p "$(dirname "$dest")"
if ! bs_rsync -p "$BS_HOST:$BS_REMOTE_CRATE/$a" "$dest" 2>/dev/null; then
# a default artefact the caller's own `-p` selection did not build is noted, not fatal (6 Oct 2026: `-p igneum-prove-host`
# alone left no igneum-prove-export); a missing artefact the caller NAMED with --artefacts is fatal
if [ -n "${ARTEFACTS_SET:-}" ] || [ -z "${CARGO_ARGS_GIVEN:-}" ]; then bs_die "no $a on the box after the build"; fi
bs_log "no $a on the box (not built by cargo ${CARGO_ARGS[*]}); skipped"; continue
fi
bs_log "artefact $dest: $(bs_size "$dest") bytes, sha256 $(bs_sha256 "$dest"), $(file -b "$dest" | cut -c1-60)"
# the commit-string gate (rule of 6 October 2026): a node binary without its commit in its strings fails the run
case "$BS_KIND:$(basename "$dest")" in node:igneumd) "$HERE/ci/commit-string-check.sh" "$dest" "$BS_SHA" || bs_die "commit-string gate failed for $a" ;; esac # only kaspad depends on kaspa-build-info
# the engine gate (7 Oct 2026, the Devnet 3 start): igneumd and igneum-miner must carry igneum-pow/src/ paths; a commit string
# alone does not prove the engine (a stub-engine 21d8f454 rejected every mined block on the fleet's hub)
case "$BS_KIND:$(basename "$dest")" in node:igneumd|node:igneum-miner) [ "${IGNEUM_ALLOW_STUB:-}" = 1 ] || "$HERE/ci/engine-check.sh" "$dest" || bs_die "engine gate failed for $a (IGNEUM_ALLOW_STUB=1 to fetch a stub-engine binary on purpose)" ;; esac
# the glibc ceiling of anything that ships (main, 7 Oct 2026): a seed or a rig refuses a binary needing more than 2.36
if [ "$SHIP" = 1 ]; then "$HERE/ci/glibc-ceiling-check.sh" "$dest" "$GLIBC" || bs_die "glibc ceiling gate failed for $a"; fi
done
fi
bs_wt_unlock
exit 0
}