#!/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// 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 --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 /target-remote/ (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-, 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@, 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" # 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 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 ;; --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 bs_host bs_context 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 # 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/release/igneumd $TARGET_DIR/release/igneum-miner" ;; repo:app/igneum-app) [ -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) [ -n "${CARGO_ARGS[*]:-}" ] || CARGO_ARGS=(build --release) [ -n "$ARTEFACTS" ] || ARTEFACTS="$TARGET_DIR/release/igneum-prove-host $TARGET_DIR/release/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 [ -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" 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 ]; 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]}"); BR_COMMAND="cargo ${CARGO_ARGS[*]}"; BR_TARGET=x86_64-unknown-linux-gnu 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 done fi bs_wt_unlock