Files
fips/testing/repro-tarball-gate.sh
T
Johnathan Corgan 6446305516 Land the reproducible-tarball gate in the source tree
Release gate 11 builds the systemd install tarball twice and compares
the two byte for byte. packaging/systemd/build-tarball.sh already
carries the tar-level determinism work; what never existed in the tree
was the wrapper that actually builds twice and compares. The gate ran
for the first time at v0.4.2 and passed, but it ran from outside the
repository, so the release could not run its own gate.

The logic is unchanged from the version that passed at v0.4.2. Only its
assumptions about living outside the tree are fixed: the source repo
now defaults to the script's own checkout instead of a sibling path,
output lands under the gitignored target/ rather than a lab-runs
directory, and the checkout test uses `git rev-parse --git-dir` so a
run from a linked worktree, whose .git is a file, is not rejected.

(cherry picked from commit c3d75639aa0ce45500fab5d32faeae58369b4763)
2026-08-25 20:46:54 +01:00

102 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Gate 11: build the systemd install tarball twice and compare the two byte for byte.
#
# packaging/systemd/build-tarball.sh already carries the tar-level determinism work
# (SOURCE_DATE_EPOCH, --mtime, --sort=name, --numeric-owner --owner=0 --group=0).
# What has never existed at any FIPS release is a wrapper that actually builds twice
# and compares, so this supplies only that.
#
# Builds run in throwaway worktrees, never in a working checkout, so no existing
# target/ cache is destroyed and no checkout is left dirty.
#
# A and B same path, built twice -- this is the gate
# C a different path -- probe only, never gating: there is no
# [profile.release], no .cargo/config.toml and no --remap-path-prefix,
# so an absolute build path can reach the binaries
#
# Usage: testing/repro-tarball-gate.sh <ref> [src-repo] [out-dir]
# Exit: 0 if A and B are identical, 1 if they differ, 2 on a setup failure.
set -euo pipefail
REF="${1:?usage: repro-tarball-gate.sh <ref> [src-repo] [out-dir]}"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SRC_REPO="${2:-${REPO_ROOT}}"
# Under target/, which is gitignored, so a gate run never dirties the checkout
# it was launched from.
OUT_DIR="${3:-${REPO_ROOT}/target/repro-tarball}"
# `--git-dir` rather than a test for a .git directory: a linked worktree carries
# a .git file, and the release runs this out of whatever checkout is to hand.
git -C "${SRC_REPO}" rev-parse --git-dir >/dev/null 2>&1 \
|| { echo "no source checkout at ${SRC_REPO}" >&2; exit 2; }
SHA="$(git -C "${SRC_REPO}" rev-parse "${REF}")"
# Pin explicitly rather than letting build-tarball.sh derive it, so all three
# builds share one value even if they are cut from different worktrees.
SOURCE_DATE_EPOCH="$(git -C "${SRC_REPO}" log -1 --format=%ct "${SHA}")"
export SOURCE_DATE_EPOCH
WORK="$(mktemp -d -t repro-gate-XXXXXX)"
mkdir -p "${OUT_DIR}"
trap 'for w in "${WORK}"/*; do [ -d "$w" ] && git -C "${SRC_REPO}" worktree remove --force "$w" 2>/dev/null || true; done; rm -rf "${WORK}"' EXIT
echo "ref ${REF} (${SHA})"
echo "SOURCE_DATE_EPOCH ${SOURCE_DATE_EPOCH}"
echo "work ${WORK}"
echo
# Build one tarball in a fresh worktree at $1, leaving it at $2.
build() {
local dir="$1" dest="$2" label="$3"
echo "=== ${label}: ${dir}"
git -C "${SRC_REPO}" worktree add --detach --quiet "${dir}" "${SHA}"
( cd "${dir}" && ./packaging/systemd/build-tarball.sh ) >"${OUT_DIR}/${label}.log" 2>&1 || {
echo "${label}: build failed, see ${OUT_DIR}/${label}.log" >&2
tail -20 "${OUT_DIR}/${label}.log" >&2
exit 2
}
local tb
tb="$(ls "${dir}"/deploy/*.tar.gz)"
cp "${tb}" "${dest}"
echo "${label}: $(sha256sum "${dest}" | cut -d' ' -f1) $(basename "${tb}")"
git -C "${SRC_REPO}" worktree remove --force "${dir}"
}
# A and B share one path, so the second reuses nothing: the worktree is removed
# and recreated between them, which is what makes this a real rebuild.
build "${WORK}/same" "${OUT_DIR}/A.tar.gz" A
build "${WORK}/same" "${OUT_DIR}/B.tar.gz" B
build "${WORK}/other-path-for-the-probe" "${OUT_DIR}/C.tar.gz" C
echo
rc=0
if cmp -s "${OUT_DIR}/A.tar.gz" "${OUT_DIR}/B.tar.gz"; then
echo "GATE PASS: A and B are byte-identical"
else
echo "GATE FAIL: A and B differ"
rc=1
fi
if cmp -s "${OUT_DIR}/A.tar.gz" "${OUT_DIR}/C.tar.gz"; then
echo "PROBE: a different build path changes nothing"
else
echo "PROBE: a different build path changes the tarball (not gating)"
fi
# Localize any difference to the file level, for both the gate and the probe.
for pair in A:B A:C; do
l="${pair%%:*}"; r="${pair##*:}"
cmp -s "${OUT_DIR}/${l}.tar.gz" "${OUT_DIR}/${r}.tar.gz" && continue
echo
echo "--- per-member digests, ${l} vs ${r}"
for s in "${l}" "${r}"; do
rm -rf "${WORK}/x-${s}"; mkdir -p "${WORK}/x-${s}"
tar -xzf "${OUT_DIR}/${s}.tar.gz" -C "${WORK}/x-${s}"
( cd "${WORK}/x-${s}" && find . -type f | sort | xargs sha256sum ) >"${WORK}/d-${s}.txt"
done
diff "${WORK}/d-${l}.txt" "${WORK}/d-${r}.txt" || true
done
exit "${rc}"