#!/usr/bin/env bash # scripts/quality/reproducible-build.sh # # Build the project twice with the same toolchain + flags + sources, and # verify the two outputs are byte-identical (after stripping ABI noise). # # Why: conformallab++ is header-only, so the binaries we ship are just # the test executables. If the same source + same toolchain produces # different bytes, something non-deterministic snuck in: # * a `__DATE__` / `__TIME__` macro in the code # * an absolute path leaked into a string literal # * iteration over an unordered container of items # * a parallel build with non-deterministic linking order # # Local-only. Two full builds at ~3 min each ≈ 6 min wall time. # # Usage: # bash scripts/quality/reproducible-build.sh # # Exit codes: # 0 the two builds produce byte-identical executables # 1 there is at least one differing byte; prints which executable(s) # 2 prerequisite missing set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT" DIR_A="build-repro-A" DIR_B="build-repro-B" # Identical-input check: nuke any state from a previous run. rm -rf "$DIR_A" "$DIR_B" # Use a fixed timezone + SOURCE_DATE_EPOCH so any time-based macros # yield identical strings in both builds. Without this, even a perfect # build pipeline disagrees if __TIME__ slips in. export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-1700000000}" export TZ=UTC export LC_ALL=C echo "========================================" echo " Reproducible-build check" echo " build A: $DIR_A" echo " build B: $DIR_B" echo " SOURCE_DATE_EPOCH: $SOURCE_DATE_EPOCH" echo "========================================" echo build_once () { local dir="$1" cmake -S code -B "$dir" -DCMAKE_BUILD_TYPE=Release -Wno-dev >/dev/null # Single-threaded build → deterministic link order. cmake --build "$dir" --target conformallab_tests -j1 >"$dir/build.log" 2>&1 } echo "── Build A ─────────────────────────────────" build_once "$DIR_A" echo " done." echo "── Build B ─────────────────────────────────" build_once "$DIR_B" echo " done." # ── Compare ───────────────────────────────────────────────────────────────── # Test executables live at: # build-repro-*/conformallab_tests (single combined fast test) # build-repro-*/test_* (older per-suite executables) # Compare every regular file under each build dir that ends in # `_tests` or starts with `test_`. echo echo "── Diff ────────────────────────────────────" mismatches="" for path_a in "$DIR_A"/conformallab_tests "$DIR_A"/test_*; do [ -f "$path_a" ] || continue rel="${path_a#${DIR_A}/}" path_b="$DIR_B/$rel" if [ ! -f "$path_b" ]; then echo " MISS $rel (only in build A)" mismatches="$mismatches $rel (missing in build B) " continue fi if cmp -s "$path_a" "$path_b"; then echo " OK $rel" else a_sha=$(shasum -a 256 "$path_a" | cut -d' ' -f1) b_sha=$(shasum -a 256 "$path_b" | cut -d' ' -f1) echo " DIFF $rel" echo " A $a_sha" echo " B $b_sha" mismatches="$mismatches $rel " fi done echo if [ -n "$mismatches" ]; then echo "FAIL: the build is not byte-reproducible." echo echo "Differing files:" printf "%s" "$mismatches" echo echo "Common causes:" echo " * __DATE__ / __TIME__ macros baked into the binary" echo " * absolute build path embedded in a debug-info string" echo " * a parallel-link race (-j > 1) — but we already use -j1 here" echo " * a header generated from a non-deterministic source" echo echo "Debug recipe:" echo " diff <(strings $DIR_A/$rel) <(strings $DIR_B/$rel) | head -20" exit 1 fi echo "OK: every test executable is byte-identical between the two builds." exit 0