130 lines
6.1 KiB
Bash
Executable File
130 lines
6.1 KiB
Bash
Executable File
#!/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
|
|
|
|
# ── 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 |