diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 255a4fbe..ce3c777a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,8 @@ jobs: run: bash tools/ci/pinned-guests-check.sh - name: root prover playbooks kill the GPU server and unlink its socket (the root-socket class, 5 October 2026) run: bash tools/ci/prover-socket-check.sh + - name: commit-string gate self-test (the empty-commit class of 6 October 2026; the gate itself runs in build-remote.sh, cross-remote.sh and cross-build.sh on every node binary) + run: bash tools/ci/commit-string-check.sh --self-test - 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) diff --git a/app/igneum-app/src/jobbuild.rs b/app/igneum-app/src/jobbuild.rs index b4341acf..204dadd4 100644 --- a/app/igneum-app/src/jobbuild.rs +++ b/app/igneum-app/src/jobbuild.rs @@ -133,6 +133,8 @@ pub struct Manifest { pub created_at: String, pub node_branch: String, pub node_commit: String, + /// the 40-hex commit (manifest node.commit_full, packer of 6 October 2026); empty in older manifests + pub node_commit_full: String, pub node_dirty: bool, pub app_version: String, pub builds: Vec, @@ -159,7 +161,7 @@ pub fn parse_manifest(text: &str) -> Result { let s = |k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").trim().to_string(); let node = v.get("node").cloned().unwrap_or(Value::Null); let ns = |k: &str| node.get(k).and_then(|x| x.as_str()).unwrap_or("").trim().to_string(); - let mut m = Manifest { created_at: s("created_at"), node_branch: ns("branch"), node_commit: ns("commit"), node_dirty: node.get("dirty").and_then(|x| x.as_bool()).unwrap_or(false), app_version: s("app_version"), ..Default::default() }; + let mut m = Manifest { created_at: s("created_at"), node_branch: ns("branch"), node_commit: ns("commit"), node_commit_full: ns("commit_full"), node_dirty: node.get("dirty").and_then(|x| x.as_bool()).unwrap_or(false), app_version: s("app_version"), ..Default::default() }; let builds = v.get("builds").and_then(|b| b.as_array()).ok_or("manifest.json has no \"builds\" list")?; for (i, b) in builds.iter().enumerate() { let dir = b.get("dir").and_then(|x| x.as_str()).unwrap_or("").trim().to_string(); @@ -243,7 +245,7 @@ pub fn windows_env() -> String { s.push_str("export CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++-posix\n"); s.push_str("export AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar\n"); s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc-posix\n"); - s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS=\"-C link-arg=-static -C link-arg=-static-libgcc\"\n"); + s.push_str("export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS=\"-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-Wl,--no-insert-timestamp\"\n"); // no PE timestamp: reproducible exes (6 Oct 2026) s.push_str("export IGNEUM_WINDRES=x86_64-w64-mingw32-windres\n"); s.push_str("LLVM_LIB=$(ls -d /usr/lib/llvm-*/lib 2>/dev/null | sort -V | tail -1)\n"); s.push_str("export LIBCLANG_PATH=\"${LLVM_LIB:-/usr/lib/llvm-18/lib}\"\n"); @@ -293,6 +295,12 @@ pub fn extract_script(p: &BuildParams, job_id: &str, zip_wsl: &str) -> String { // against 0.3.5's consensus-core). The packer stamps too (push-build-inputs.sh); this guard holds if it regresses. s.push_str("find \"$B/src\" -type f -exec touch {} + || { echo \"RESULT extract cannot stamp the sources\"; exit 2; }\n"); s.push_str("[ -f \"$SRC/manifest.json\" ] || { echo \"RESULT extract no manifest.json under $SRC\"; exit 2; }\n"); + // the commit hash (6 October 2026, found on igneum-build-1): the zip has no .git, so kaspa-build-info's build.rs embedded + // nothing in every PC build. It needs .git to be a directory with HEAD a symbolic ref to a branch file holding the hash + // (git rev-parse reads it; its fallback reads the file itself). A minimal .git with the manifest's full commit gives the + // binary its commit string; tools/ci/commit-string-check.sh then passes on the fetched binaries. + s.push_str("ncf=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(\"node\",{}).get(\"commit_full\",\"\"))' \"$SRC/manifest.json\" 2>/dev/null || true)\n"); + s.push_str("if [ -d \"$SRC/node\" ] && printf '%s' \"$ncf\" | grep -qE '^[0-9a-f]{40}$'; then mkdir -p \"$SRC/node/.git/refs/heads\" && printf 'ref: refs/heads/build\\n' > \"$SRC/node/.git/HEAD\" && printf '%s\\n' \"$ncf\" > \"$SRC/node/.git/refs/heads/build\" && echo \"commit $ncf written to node/.git for kaspa-build-info\"; else echo \"no full node commit in the manifest: the node binaries will carry no commit string\"; fi\n"); // the stale-build class (5 October 2026): the target dir persists and cargo rebuilds by mtime, so every extracted // source is stamped now, else a file older than the last build links against the cached crate of the old version s.push_str("find \"$B/src\" -type f -exec touch {} + 2>/dev/null || true\n"); @@ -323,13 +331,23 @@ pub fn build_script(p: &BuildParams, job_id: &str, m: &Manifest, target: &str) - s.push_str(&format!("echo \"STAGE {target} start $(now)\"\n")); s.push_str(&format!("mkdir -p \"$OUT/{target}\"\n")); s.push_str("rc_all=0\n"); + // the node's full commit from the manifest (each stage is its own script; the extract stage read it too) + s.push_str("ncf=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(\"node\",{}).get(\"commit_full\",\"\"))' \"$SRC/manifest.json\" 2>/dev/null || true)\n"); for u in m.builds.iter().filter(|u| u.targets.iter().any(|t| t == target)) { let optional = u.optional_on.iter().any(|t| t == target); let rel = if windows { "x86_64-pc-windows-gnu/release" } else { "release" }; let tflag = if windows { " --target x86_64-pc-windows-gnu" } else { "" }; s.push_str(&format!("echo \"STAGE {target} {dir} start $(now)\"\n", dir = u.dir)); s.push_str(&format!("t0=$(date +%s); rc=1\n")); + if u.dir == "node" { + // kaspa-build-info emits no rerun-if-changed once it found nothing, so the persistent target dir keeps an empty + // hash forever unless that one crate is cleaned when the commit differs from the last one built here (6 Oct 2026) + s.push_str(&format!("if cd \"$SRC/{dir}\"; then [ \"$(cat \"$CARGO_TARGET_DIR/.node-commit-{target}\" 2>/dev/null)\" = \"$ncf\" ] || cargo clean -q --release -p kaspa-build-info{tflag} 2>/dev/null || true; fi\n", dir = u.dir)); + } s.push_str(&format!("if cd \"$SRC/{dir}\"; then $NICE cargo build --release $JOBS{args}{tflag} 2>&1; rc=$?; else echo \"no $SRC/{dir}\"; rc=2; fi\n", dir = u.dir, args = cargo_unit_args(u))); + if u.dir == "node" { + s.push_str(&format!("[ \"$rc\" = 0 ] && printf '%s\\n' \"$ncf\" > \"$CARGO_TARGET_DIR/.node-commit-{target}\"\n")); + } s.push_str(&format!("echo \"RESULT {target} {dir} build exit $rc $(( $(date +%s) - t0 )) s at $(now)\"\n", dir = u.dir)); s.push_str("if [ \"$rc\" = 0 ]; then\n"); for b in &u.bins { diff --git a/docs/plans/build-server.md b/docs/plans/build-server.md index 710650b8..9e8b78dd 100644 --- a/docs/plans/build-server.md +++ b/docs/plans/build-server.md @@ -66,12 +66,12 @@ Also logged for context: the Mac's Linux cross-build with zig (`infra/cross/buil | Incremental 7 s against 2 to 15 min on the Mac (the "8 min under contention") | an edit-build loop of seconds for Linux targets; end to end 10 s because sync is 1 s and the two binaries (57 MB) come back in 2 s | R1; the slot count stays 1 until two agents collide, then 2 with `-j 48` each (`SLOTS=2 run-from-mac.sh` and `--jobs 48`) | | Windows cross 1 min 44 s against 4 min 49 s to 12 min 28 s on the Mac (3 to 7x), 8 s incremental | a Windows exe per commit is cheap enough to build on every push; the PC `build` job (7 min 38 s cold, 5 min 09 s warm for Linux + Windows + tests) stays the second source | R2 proposed | | Mac arm64 binaries: not built here | agents who run nodes on the Mac (local devnets, the DMG) still take Mac slots; the box cannot remove that contention | R3; the real relief is to move test networks to the fleet or to a Devnet 2 seed on this box (its unit, ports and ufw are ready) | -| The commit hash is EMPTY in every Mac worktree build and was empty in the first box builds | `kaspa-build-info` (build-info/build.rs) needs `.git` to be a directory AND HEAD to be a symbolic ref to a loose branch file; a worktree's `.git` is a file and a detached HEAD is not a ref; and once it has found nothing it emits no `rerun-if-changed`, so cargo never runs it again in that target dir (release-0.3.11: `cargo clean -p kaspa-build-info`) | fixed on the box: the remote checkout is `git checkout -B ` and build-remote.sh runs `cargo clean --release -p kaspa-build-info` whenever the commit differs from the last one built in that target dir (`.build-remote-sha-`); verified 17:40 UTC: 2 string hits for 3bfe346f, binary +1,024 B. The release plans' "commit in its strings" checks were passing against builds from the fork's MAIN checkout (a real .git directory on a branch), not from worktrees. Proposed: the PC build job and the Mac's release recipe adopt the same two steps, or the fork's build.rs learns to read a worktree's gitdir file | -| igneumd.exe hash changes build to build with no source change (c424aae0 then bfbff015) while the Linux igneumd stays byte-identical across three builds | the mingw linker writes a timestamp into the PE header; the Linux ELF has none | not changed (the Mac and PC exes have the same property); `-C link-arg=-Wl,--no-insert-timestamp` would make the exe reproducible and is a one-line change to cross-remote.sh, the Mac script and jobbuild.rs together if main wants reproducible Windows builds | -| sccache misses 1,982 of 1,982 | expected for first builds; the cache is 494 MB after three builds, capped at 100 GB | nothing; the hit rate is in every RESULT line and every JSONL line, read it after the first repeat build | +| The commit hash is EMPTY in every Mac worktree build and was empty in the first box builds | `kaspa-build-info` (build-info/build.rs) needs `.git` to be a directory AND HEAD to be a symbolic ref to a loose branch file; a worktree's `.git` is a file and a detached HEAD is not a ref; and once it has found nothing it emits no `rerun-if-changed`, so cargo never runs it again in that target dir (release-0.3.11: `cargo clean -p kaspa-build-info`) | fixed on the box: the remote checkout is `git checkout -B ` and build-remote.sh runs `cargo clean --release -p kaspa-build-info` whenever the commit differs from the last one built in that target dir (`.build-remote-sha-`); verified 17:40 UTC: 2 string hits for 3bfe346f, binary +1,024 B. The release plans' "commit in its strings" checks were passing against builds from the fork's MAIN checkout (a real .git directory on a branch), not from worktrees. DONE (main's decision): the PC job writes a minimal `node/.git` (HEAD -> refs/heads/build holding the manifest's new `commit_full`, written by push-build-inputs.sh) at extract and cleans kaspa-build-info on a new commit (jobbuild.rs, 4 unit tests pass on the box); the Mac's cross-build.sh refuses a worktree and cleans on a new commit; the gate `tools/ci/commit-string-check.sh` runs on every igneumd the three scripts produce (self-test in ci.yml; shown firing on the Mac's worktree-built igneumd and passing on the box's). Only kaspad depends on kaspa-build-info, so igneum-miner is out of the gate's scope | +| igneumd.exe hash changed build to build with no source change (c424aae0 then bfbff015) while the Linux igneumd stayed byte-identical across three builds | the mingw linker wrote a timestamp into the PE header; the Linux ELF has none | DONE (main's decision): `-C link-arg=-Wl,--no-insert-timestamp` in cross-remote.sh, the Mac's cross-build.sh and the PC job (jobbuild.rs); verified 17:48 and 17:49 UTC: two builds, igneumd.exe c38b7570... and igneum-miner.exe c3a0fc2b... identical both times | +| Second worktree's clean build (vendor/igneum-node-v4 from the main checkout, cold target dir, warm sccache): 1 min 18 s, 604 hits of 993 compiles (61 percent), 389 misses | the first build of each of the 123 worktree dirs costs 1 min 18 s to 1 min 27 s, not the Mac's 12 to 18 min; sccache saves 9 s of the 87 because the clean build is dominated by the fork's own 74 crates and the C++ (rocksdb) objects, which differ per tree or do not cache; the cache is 1.0 GB after four builds, capped at 100 GB | nothing; the hit rate is in every RESULT line and every JSONL line | | Disk 3.3 TB free, RAM 125 GB, load peaked at 24 during the Windows build with 96 threads | room for 2 slots and the Devnet 2 seed without contention | nothing now | -## 4. Rules (proposed for CLAUDE.md once the box passes; main decides) +## 4. Rules (ADOPTED by main on 6 October 2026, written into CLAUDE.md "Running agents on this Mac"; R2 narrowed: the PCs keep only GPU and Windows-runtime jobs from now, not two releases) | Rule | Text | |---|---| diff --git a/packaging/windows/push-build-inputs.sh b/packaging/windows/push-build-inputs.sh index da790d66..1cfb2c38 100755 --- a/packaging/windows/push-build-inputs.sh +++ b/packaging/windows/push-build-inputs.sh @@ -80,6 +80,7 @@ fi NODE_BRANCH="$(git -C "$NODE_SRC" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)" NODE_COMMIT="$(git -C "$NODE_SRC" rev-parse --short HEAD 2>/dev/null || echo unknown)" +NODE_COMMIT_FULL="$(git -C "$NODE_SRC" rev-parse HEAD 2>/dev/null || echo unknown)" # the PC job writes it into a .git for kaspa-build-info (6 Oct 2026) NODE_DIRTY=false; [ -z "$(git -C "$NODE_SRC" status --porcelain 2>/dev/null)" ] || NODE_DIRTY=true REPO_BRANCH="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)" REPO_COMMIT="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)" @@ -108,9 +109,9 @@ rsync -a --exclude 'target' --exclude 'target-*' --exclude '.DS_Store' "$ROOT/ig echo "packing proto-cuda (without nvrtc/redist)" rsync -a --exclude 'nvrtc/redist' --exclude '.DS_Store' --exclude '*.exe' --exclude '*.dll' "$ROOT/proto-cuda/" "$STAGE/proto-cuda/" -python3 - "$STAGE/manifest.json" "$NODE_BRANCH" "$NODE_COMMIT" "$NODE_DIRTY" "${NODE_SRC#"$ROOT"/}" "$REPO_BRANCH" "$REPO_COMMIT" "$REPO_DIRTY" "$APP_VERSION" "$WITH_APP" "$NODE_TESTS" "$APP_TESTS" "${WITH_NODE:-1}" <<'PY' +python3 - "$STAGE/manifest.json" "$NODE_BRANCH" "$NODE_COMMIT" "$NODE_DIRTY" "${NODE_SRC#"$ROOT"/}" "$REPO_BRANCH" "$REPO_COMMIT" "$REPO_DIRTY" "$APP_VERSION" "$WITH_APP" "$NODE_TESTS" "$APP_TESTS" "${WITH_NODE:-1}" "$NODE_COMMIT_FULL" <<'PY' import json, sys, datetime -out, nb, nc, nd, ns, rb, rc, rd, av, with_app, node_tests, app_tests, with_node = sys.argv[1:14] +out, nb, nc, nd, ns, rb, rc, rd, av, with_app, node_tests, app_tests, with_node, ncf = sys.argv[1:15] builds = [{"dir": "node", "packages": ["kaspad", "igneum-miner"], "features": ["kaspad/igneum-pow"], "bins": ["igneumd", "igneum-miner"], "targets": ["linux", "windows"]}] if with_node == "1" else [] tests = [] if node_tests.strip() and with_node == "1": @@ -121,7 +122,7 @@ if with_app == "1": tests.append({"dir": "app/igneum-app", "packages": app_tests.split()}) m = { "created_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), - "node": {"branch": nb, "commit": nc, "dirty": nd == "true", "source": ns}, + "node": {"branch": nb, "commit": nc, "commit_full": ncf, "dirty": nd == "true", "source": ns}, "repo": {"branch": rb, "commit": rc, "dirty": rd == "true"}, "app_version": av if with_app == "1" else "", "builds": builds, diff --git a/proto-cuda/windows-node/cross-build.sh b/proto-cuda/windows-node/cross-build.sh index 81d1abc4..209fbe42 100755 --- a/proto-cuda/windows-node/cross-build.sh +++ b/proto-cuda/windows-node/cross-build.sh @@ -36,10 +36,22 @@ export BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu="--target=x86_64-w64-mingw # after that commit imports no mingw DLL and the pairing rule is moot for it; the gate and the DLL copy stay for any # older fork. The PC-built exe (GCC 13) died at its first rocksdb call with the dynamic runtime; see the # release-0.3.6 plan, section 10. -export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-static-libstdc++" +# 6 October 2026 (main's decision): -Wl,--no-insert-timestamp makes the exe reproducible (the PE header timestamp was the one +# byte-level difference between two builds of one tree on igneum-build-1); the box (tools/cross-remote.sh) and the PC job carry it too +export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-static-libstdc++ -C link-arg=-Wl,--no-insert-timestamp" cd "$NODE" +# The commit hash (6 October 2026, found on igneum-build-1): kaspa-build-info's build.rs embeds the commit only when .git is +# a DIRECTORY and HEAD is a symbolic ref to a branch file, and once it has found nothing it emits no rerun-if-changed, so +# cargo never runs it again in this target dir. Two steps: clean that one crate when the commit changed since the last build +# here, and refuse a worktree (.git is a file there: the hash would be empty and tools/ci/commit-string-check.sh would fail +# the release). Build the Windows exes from the fork's main checkout or through tools/cross-remote.sh (the box checks out a branch). +sha=$(git rev-parse HEAD) +[ -d .git ] || { echo "$NODE/.git is not a directory (a worktree): kaspa-build-info would embed no commit; use tools/cross-remote.sh or the fork's main checkout" >&2; exit 1; } +[ "$(cat ".cross-build-sha-$CARGO_TARGET_DIR" 2>/dev/null)" = "$sha" ] || cargo clean -q --release -p kaspa-build-info --target x86_64-pc-windows-gnu 2>/dev/null || true nice -n 19 cargo build --release -j "$JOBS" -p kaspad -p igneum-miner --features igneum-pow --target x86_64-pc-windows-gnu +echo "$sha" > ".cross-build-sha-$CARGO_TARGET_DIR" for exe in igneumd igneum-miner; do f="$CARGO_TARGET_DIR/x86_64-pc-windows-gnu/release/$exe.exe" echo "$f: $(stat -f %z "$f") bytes; DLLs: $(x86_64-w64-mingw32-objdump -p "$f" | grep 'DLL Name' | awk '{print $3}' | sort -u | tr '\n' ' ')" + [ "$exe" = igneumd ] && "$ROOT/tools/ci/commit-string-check.sh" "$f" "$sha" # only kaspad depends on kaspa-build-info done diff --git a/tools/build-remote.sh b/tools/build-remote.sh index 70bfd32c..e1dddffc 100755 --- a/tools/build-remote.sh +++ b/tools/build-remote.sh @@ -106,5 +106,7 @@ if [ "$FETCH" = 1 ] && [ -n "$ARTEFACTS" ]; then rel="${a#"$TARGET_DIR"/}"; dest="$OUT/$rel"; mkdir -p "$(dirname "$dest")" bs_rsync -p "$BS_HOST:$BS_REMOTE_CRATE/$a" "$dest" || bs_die "no $a on the box after the build" 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 diff --git a/tools/ci/commit-string-check.sh b/tools/ci/commit-string-check.sh new file mode 100755 index 00000000..0ef612d3 --- /dev/null +++ b/tools/ci/commit-string-check.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# The empty-commit class (6 October 2026, found on igneum-build-1): the fork's kaspa-build-info embeds the git commit in +# igneumd and igneum-miner only when the source tree's .git is a DIRECTORY whose HEAD is a symbolic ref to a branch file, +# and once its build script has found nothing it emits no rerun-if-changed, so a persistent target dir keeps the empty hash +# for ever. Every Mac worktree build (.git is a file there) and every PC job build (the zip has no .git) shipped with no +# commit string while the release plans' "commit in its strings" line was being ticked from main-checkout builds. +# Rule: an igneumd binary whose strings do not contain its short commit fails (igneum-miner has no kaspa-build-info +# dependency and never carried one; checked 6 October 2026). tools/build-remote.sh, tools/cross-remote.sh and +# proto-cuda/windows-node/cross-build.sh run this on every igneumd they produce; the shipper's preflight should too. +# +# tools/ci/commit-string-check.sh exit 0 when the binary carries the commit +# tools/ci/commit-string-check.sh --self-test fires on a known-bad and passes a known-good case +set -euo pipefail +if [ "${1:-}" = --self-test ]; then + tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT + printf 'igneumd v2.1.0-3bfe346f\0junk\0' > "$tmp/good"; printf 'igneumd v2.1.0\0junk\0' > "$tmp/bad" + "$0" "$tmp/good" 3bfe346f4de0d9a982627270c791b7855abc5af6 >/dev/null || { echo "commit-string self-test: the good case FAILED"; exit 1; } + if "$0" "$tmp/bad" 3bfe346f4de0d9a982627270c791b7855abc5af6 >/dev/null 2>&1; then echo "commit-string self-test: the bad case PASSED (the gate is blind)"; exit 1; fi + echo "commit-string self-test: fires on the empty binary, passes the stamped one"; exit 0 +fi +bin="${1:-}"; sha="${2:-}" +[ -f "$bin" ] && [ -n "$sha" ] || { echo "usage: commit-string-check.sh | --self-test" >&2; exit 2; } +printf '%s' "$sha" | grep -qE '^[0-9a-f]{7,40}$' || { echo "commit-string: '$sha' is not a commit sha" >&2; exit 2; } +short="${sha:0:7}" # build-info's fallback embeds 7 characters; git rev-parse --short gives 7 or more, all starting the same +# grep -c, not grep -q: -q closes the pipe at the first match, strings dies with SIGPIPE and pipefail turns a hit into a miss +# (the first version of this gate failed a stamped binary that way, 6 October 2026) +hits=$(strings "$bin" | grep -c "$short" || true) +if [ "${hits:-0}" -gt 0 ]; then + echo "commit-string: $(basename "$bin") carries $short" +else + echo "commit-string: $(basename "$bin") does NOT carry its commit $short (kaspa-build-info found no .git directory on a branch; see the header)" >&2 + exit 1 +fi diff --git a/tools/cross-remote.sh b/tools/cross-remote.sh index 495e71da..322028f7 100755 --- a/tools/cross-remote.sh +++ b/tools/cross-remote.sh @@ -14,6 +14,9 @@ # list (x86_64-w64-mingw32-objdump -p on the box, as the Mac script prints it). With --compare , the Mac's exe of the same # name is hashed too and the line says identical or differs. # +# Reproducible (main's decision, 6 October 2026): -Wl,--no-insert-timestamp zeroes the PE header timestamp the mingw linker +# writes, so two builds of one tree give one hash (verified 6 Oct: two runs, both exes identical); the Mac's cross-build.sh and +# the PC job (jobbuild.rs) carry the same flag. # Byte identity (6 October 2026): the same rustc (1.99.0 both sides, refused otherwise) and the same flags give the same Rust # code, but two things still differ between the Mac's exe and the box's: the C and C++ objects (rocksdb, snappy, zstd, lz4, # secp256k1, mimalloc) come from Homebrew's mingw gcc on the Mac and Ubuntu's gcc 13 here, and source paths embedded by @@ -72,7 +75,7 @@ bs_log "sources in place after $(( $(date +%s) - t_sync0 )) s" env_block='LLVM_LIB=$(ls -d /usr/lib/llvm-*/lib 2>/dev/null | sort -V | tail -1); [ -n "$LLVM_LIB" ] || { echo "no /usr/lib/llvm-*/lib on the box (apt clang libclang-dev)"; exit 2; } export CC_x86_64_pc_windows_gnu=x86_64-w64-mingw32-gcc-posix CXX_x86_64_pc_windows_gnu=x86_64-w64-mingw32-g++-posix AR_x86_64_pc_windows_gnu=x86_64-w64-mingw32-ar export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc-posix -export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-static-libstdc++" +export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUSTFLAGS="-C link-arg=-static -C link-arg=-static-libgcc -C link-arg=-static-libstdc++ -C link-arg=-Wl,--no-insert-timestamp" export IGNEUM_WINDRES=x86_64-w64-mingw32-windres LIBCLANG_PATH="$LLVM_LIB" export BINDGEN_EXTRA_CLANG_ARGS_x86_64_pc_windows_gnu="--target=x86_64-w64-mingw32 --sysroot=/usr/x86_64-w64-mingw32 -I/usr/x86_64-w64-mingw32/include" echo "cross-remote: $(x86_64-w64-mingw32-gcc-posix --version | head -1); libclang $LLVM_LIB"' @@ -108,6 +111,8 @@ for exe in $EXES; do if [ "$msum" = "$sum" ]; then line="$line; IDENTICAL to $COMPARE/$exe"; else line="$line; differs from $COMPARE/$exe ($(bs_size "$COMPARE/$exe") bytes, sha256 ${msum:0:16}...; expected, see the header)"; fi fi bs_log "$line" + # the commit-string gate (rule of 6 October 2026): a node exe without its commit in its strings fails the run + case "$BS_KIND:$exe" in node:igneumd.exe) "$HERE/ci/commit-string-check.sh" "$dest" "$BS_SHA" || bs_die "commit-string gate failed for $exe" ;; esac # only kaspad depends on kaspa-build-info done # an exe that imports libstdc++-6.dll (a fork before the housekeeping commit that made the C++ runtime static) needs the # three runtime DLLs OF THIS TOOLCHAIN beside it (the PC job does the same; push-inputs.sh takes them from next to the exes)