#!/usr/bin/env bash # verify-reproducible-build.sh # # Builds the Triangles daemon (trianglesd) twice from the same source tree # into two separate build directories, then compares the resulting # SHA256 hashes. Exits 0 if the two builds produce byte-identical binaries, # non-zero otherwise. # # Usage: # scripts/verify-reproducible-build.sh # default: trianglesd, Release # BUILD_TYPE=Debug scripts/verify-reproducible-build.sh # override build type # TARGET=triangles-qt scripts/verify-reproducible-build.sh # build Qt wallet instead # # What "reproducible" means here: # Given identical source tree, identical compiler toolchain, identical # build flags, identical SOURCE_DATE_EPOCH (if set) -- the resulting # binary must hash identically across separate build directories. # # This script does NOT enforce compiler version pinning. Two different # GCC versions will legitimately produce different binaries even with # identical flags. The verification is "same source + same toolchain = # same binary." # # Pass criteria: # 1. Both builds succeed # 2. Both binaries exist # 3. SHA256 of the two binaries is equal # # On failure: prints the two SHA256s and the diff in size so a reviewer # can investigate. Common causes of non-determinism: # - __DATE__/__TIME__ embedded (we eliminate this in CMakeLists.txt) # - absolute paths in __FILE__ (mitigated by -ffile-prefix-map) # - uninitialized stack/heap contents (should not affect final binary) # - linker adds random base addresses (PIE; deterministic if compiled # with -fno-pie) set -euo pipefail # ── Config ───────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="${SOURCE_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}" BUILD_TYPE="${BUILD_TYPE:-Release}" TARGET="${TARGET:-trianglesd}" # Skip Qt by default -- it's slow and adds CI noise. Override with TARGET=triangles-qt : "${BUILD_QT:=OFF}" BUILD_DIR_A="${BUILD_DIR_A:-/tmp/triangles-repro-A}" BUILD_DIR_B="${BUILD_DIR_B:-/tmp/triangles-repro-B}" LOG_A="${LOG_A:-/tmp/triangles-repro-A.log}" LOG_B="${LOG_B:-/tmp/triangles-repro-B.log}" # ── Preflight ────────────────────────────────────────────────────────────── command -v cmake >/dev/null || { echo "ERROR: cmake not found" >&2; exit 2; } command -v ninja >/dev/null || { echo "ERROR: ninja not found (apt install ninja-build)" >&2; exit 2; } command -v sha256sum >/dev/null || { echo "ERROR: sha256sum not found" >&2; exit 2; } if [ ! -d "$SOURCE_DIR" ]; then echo "ERROR: source dir not found: $SOURCE_DIR" >&2 exit 2 fi # Warn if tracked files are dirty -- git describe --dirty (used by build.h) # ignores untracked files, but includes modified/staged tracked files in the # version string. Untracked notes/build outputs are safe and should not scare # release builders. if [ -n "$(cd "$SOURCE_DIR" && git status --porcelain --untracked-files=no 2>/dev/null)" ]; then echo "WARNING: tracked working tree changes detected." >&2 echo " build.h will include a '-dirty' suffix, so the binary will not" >&2 echo " match a clean checkout/tag. Commit or stash tracked changes first." >&2 fi # ── Embedded sub-libraries (Tor, I2P) ───────────────────────────────────── # The daemon statically links libtor.a and libi2pd*.a; both must exist # before cmake's link step. On a fresh checkout they need to be built from # the embedded submodules. CI does this in build-all.yml before the main # build; this script does the same so a local `scripts/verify-reproducible-build.sh` # works out of the box. TOR_LIB="$SOURCE_DIR/src/tor/tor-src/libtor.a" I2P_LIBS=( "$SOURCE_DIR/src/i2p/i2pd-src/libi2pd.a" "$SOURCE_DIR/src/i2p/i2pd-src/libi2pdclient.a" "$SOURCE_DIR/src/i2p/i2pd-src/libi2pdlang.a" ) NEED_TOR_BUILD=0 NEED_I2P_BUILD=0 [ -f "$TOR_LIB" ] || NEED_TOR_BUILD=1 for lib in "${I2P_LIBS[@]}"; do [ -f "$lib" ] || NEED_I2P_BUILD=1; done if [ "$NEED_TOR_BUILD" = "1" ]; then echo "Building libtor.a (one-time, ~5 min)..." >&2 # CI passes /usr paths for native Linux; defaults in build-libtor.sh # are MINGW64 cross-compile paths. LIBEVENT_DIR=/usr OPENSSL_DIR=/usr ZLIB_DIR=/usr \ bash "$SOURCE_DIR/src/tor/build-libtor.sh" \ > /tmp/triangles-build-libtor.log 2>&1 \ || { echo "ERROR: libtor build failed; see /tmp/triangles-build-libtor.log" >&2; exit 5; } fi if [ "$NEED_I2P_BUILD" = "1" ]; then echo "Building libi2pd*.a (one-time, ~3 min)..." >&2 bash "$SOURCE_DIR/src/i2p/build-libi2pd.sh" \ > /tmp/triangles-build-libi2pd.log 2>&1 \ || { echo "ERROR: libi2pd build failed; see /tmp/triangles-build-libi2pd.log" >&2; exit 5; } fi # ── Helpers ──────────────────────────────────────────────────────────────── build_one() { local dir="$1" log="$2" rm -rf "$dir" mkdir -p "$dir" echo " configuring in $dir (BUILD_TYPE=$BUILD_TYPE BUILD_QT=$BUILD_QT)..." >&2 cmake -S "$SOURCE_DIR" -B "$dir" \ -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ -DBUILD_QT="$BUILD_QT" \ > "$log" 2>&1 || { echo " configure failed; see $log" >&2; tail -30 "$log" >&2; exit 3; } echo " building target $TARGET..." >&2 cmake --build "$dir" --target "$TARGET" -j "$(nproc)" \ >> "$log" 2>&1 || { echo " build failed; see $log" >&2; tail -30 "$log" >&2; exit 3; } # ONLY stdout of the find goes to the caller. Progress logs above # were redirected to stderr so they don't pollute the captured path. find "$dir" -name "$TARGET" -type f -executable | head -1 } # ── Build twice ──────────────────────────────────────────────────────────── echo "Building $TARGET ($BUILD_TYPE) twice from $SOURCE_DIR..." echo "" BIN_A="$(build_one "$BUILD_DIR_A" "$LOG_A")" BIN_B="$(build_one "$BUILD_DIR_B" "$LOG_B")" if [ -z "$BIN_A" ] || [ -z "$BIN_B" ]; then echo "ERROR: could not find built binary" >&2 echo " A: '$BIN_A'" >&2 echo " B: '$BIN_B'" >&2 exit 4 fi # ── Compare ──────────────────────────────────────────────────────────────── HASH_A="$(sha256sum "$BIN_A" | awk '{print $1}')" HASH_B="$(sha256sum "$BIN_B" | awk '{print $1}')" SIZE_A="$(stat -c%s "$BIN_A" 2>/dev/null || stat -f%z "$BIN_A")" SIZE_B="$(stat -c%s "$BIN_B" 2>/dev/null || stat -f%z "$BIN_B")" echo "" echo "Binary A: $BIN_A" echo " sha256: $HASH_A" echo " size: $SIZE_A bytes" echo "Binary B: $BIN_B" echo " sha256: $HASH_B" echo " size: $SIZE_B bytes" echo "" if [ "$HASH_A" = "$HASH_B" ]; then echo "✓ REPRODUCIBLE: both builds produced identical SHA256" exit 0 else echo "✗ NOT REPRODUCIBLE: hashes differ" echo "" echo "Likely causes:" echo " - __DATE__/__TIME__ embedded (check src/version.cpp)" echo " - absolute build paths in __FILE__ (check CMakeLists.txt for -ffile-prefix-map)" echo " - dirty git tree (commit/stash and rerun)" echo " - PIE base randomization (compile with -fno-pie -no-pie for testing)" echo " - non-deterministic linker output (linker version mismatch)" exit 1 fi