From 324d9299dec9fcedc324a768d1d2de27da494df8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 20:23:37 +0000 Subject: [PATCH 01/10] ci: make release binaries reproducible across hosts The release binary must be determined by the source code and the pinned toolchain alone, not by the host that performs the build. Two host-specific inputs previously leaked into the artifact. First, the dependency source paths from the cargo home were embedded verbatim into the binary through the `file!()` macro, so the absolute registry path of the build machine became part of the output. A new `ci/github-actions/reproducible-build-env.sh` script, run before each build in the three deployment build jobs, sets `--remap-path-prefix` for the cargo home and the workspace. This replaces those prefixes with fixed placeholders and is the stable equivalent of the still-unstable `-Z trim-paths` option. The script also pins `CARGO_INCREMENTAL=0` and provides `SOURCE_DATE_EPOCH`. Second, the binary was stripped by a separate `strip` step whose output depends on the version of the host `strip` program. The release profile now sets `strip = "symbols"`, so the toolchain strips the binary while linking, and the redundant strip steps are removed. This also strips the Windows binaries, which the removed steps never covered. Parallel code generation is kept. On a fixed toolchain it is already deterministic, verified by two independent builds producing byte-identical binaries, so `codegen-units` stays at its default rather than being serialized to 1. The value is pinned to preserve that parallelism against a future change to the default. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- .github/workflows/deploy.yaml | 17 ++++++---- Cargo.toml | 14 ++++++++ ci/github-actions/reproducible-build-env.sh | 36 +++++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100755 ci/github-actions/reproducible-build-env.sh diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 3e235ee4..bbba7fec 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -89,12 +89,13 @@ jobs: - name: Install compilation target run: rustup target add ${{ matrix.target }} + - name: Configure reproducible build environment + shell: bash + run: ./ci/github-actions/reproducible-build-env.sh + - name: Build run: cargo build --target ${{ matrix.target }} --release --all-features - - name: Strip all debug symbols - run: strip --strip-all target/${{ matrix.target }}/release/pdu - - name: Attest build provenance uses: actions/attest-build-provenance@v4 with: @@ -135,12 +136,13 @@ jobs: - name: Install compilation target run: rustup target add ${{ matrix.target }} + - name: Configure reproducible build environment + shell: bash + run: ./ci/github-actions/reproducible-build-env.sh + - name: Build run: cargo build --target ${{ matrix.target }} --release --all-features - - name: Strip all debug symbols - run: strip target/${{ matrix.target }}/release/pdu - - name: Attest build provenance uses: actions/attest-build-provenance@v4 with: @@ -182,6 +184,9 @@ jobs: - name: Install compilation target run: rustup target add ${{ matrix.target }} + - name: Configure reproducible build environment + shell: bash + run: ./ci/github-actions/reproducible-build-env.sh - name: Build run: cargo build --target ${{ matrix.target }} --release --all-features diff --git a/Cargo.toml b/Cargo.toml index 9dedda03..69716ad7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,3 +106,17 @@ normalize-path = "0.2.1" pretty_assertions = "1.4.1" rand = "0.10.1" which = "8.0.2" + +# The release profile is tuned for reproducible builds. The compiled binary +# should be determined solely by the source code and the pinned toolchain, +# never by the host that happens to compile it. +[profile.release] +# Strip symbols as part of linking, so the toolchain itself produces the final +# artifact. This keeps stripping deterministic and identical on every host, +# instead of depending on the version of an external `strip` program. +strip = "symbols" +# Keep the default parallel code generation. On a fixed toolchain the output of +# parallel codegen is already deterministic, so reproducibility does not require +# serializing compilation with `codegen-units = 1`. Pinning the value preserves +# this parallelism against any future change to the default. +codegen-units = 16 diff --git a/ci/github-actions/reproducible-build-env.sh b/ci/github-actions/reproducible-build-env.sh new file mode 100755 index 00000000..56ca4a1b --- /dev/null +++ b/ci/github-actions/reproducible-build-env.sh @@ -0,0 +1,36 @@ +#! /bin/bash +set -o errexit -o pipefail -o nounset + +# Emit the environment variables that make the release binary reproducible into +# "$GITHUB_ENV", so that every later step in the job inherits them. The goal is +# for the compiled artifact to depend only on the source code and the pinned +# toolchain, never on the host that performs the build. +# +# None of these variables reduce parallelism. Parallel code generation is +# already deterministic on a fixed toolchain, so the build stays multi-threaded. + +cargo_home="${CARGO_HOME:-$HOME/.cargo}" +workspace="${GITHUB_WORKSPACE:-$PWD}" + +# Replace the machine-specific absolute prefixes that the compiler would +# otherwise embed into the binary. The dependency sources live under the cargo +# home, and the crate itself is built from the workspace directory. Both paths +# differ from one machine to the next. This is the stable equivalent of the +# still-unstable "-Z trim-paths" option, and it does not affect codegen. +remap="--remap-path-prefix=$cargo_home=/cargo" +remap="$remap --remap-path-prefix=$workspace=/build" +if [[ -n "${RUSTFLAGS:-}" ]]; then + remap="$RUSTFLAGS $remap" +fi + +{ + echo "RUSTFLAGS=$remap" + # Incremental compilation caches carry host-specific state and are already + # disabled for the release profile. Pin the value so a stray environment + # setting cannot turn it back on. + echo "CARGO_INCREMENTAL=0" + # Provide a fixed timestamp for any build script that would otherwise read the + # wall clock. The current sources embed no timestamp, so this is a safeguard + # rather than a fix for a present problem. + echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" +} >>"$GITHUB_ENV" From 3a18a1fb8ee1e71355f0e0423863c821fb239a4e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 20:23:43 +0000 Subject: [PATCH 02/10] docs(contributing): document reproducible builds Explain that release binaries are reproducible, that parallel code generation is kept because it is deterministic on a fixed toolchain, and how the `strip = "symbols"` profile setting and the `--remap-path-prefix` script achieve this. Include the commands to reproduce a binary locally and compare its checksum. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- CONTRIBUTING.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77ff9238..3c42abbe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -531,3 +531,37 @@ FMT=true LINT=true BUILD=true TEST=true DOC=true DYLINT=true ./test.sh > [!NOTE] > Some tests may fail with a hint about `TEST_SKIP`. Follow the hint and rerun with the suggested variable. + +## Reproducible Builds + +The release binaries are built so that their exact bytes, including the +checksums, follow from the source code and the pinned toolchain alone. A build +performed on a different host, with a different number of threads, or at a +different time yields the same artifact. + +Reproducibility does not cost compilation speed. Parallel code generation is +deterministic on a fixed toolchain, so the release profile keeps the default +`codegen-units` instead of serializing the build with `codegen-units = 1`. + +Two mechanisms remove the host-specific information that would otherwise differ +between machines: + +- The `[profile.release]` section of `Cargo.toml` sets `strip = "symbols"`, so + the toolchain strips the binary while linking. The result no longer depends on + the version of an external `strip` program, which was previously invoked as a + separate step and produced host-dependent output. +- The `ci/github-actions/reproducible-build-env.sh` script, run by the + deployment workflow before each build, adds `--remap-path-prefix` entries that + replace the absolute paths of the cargo home and the workspace with fixed + placeholders. This is the stable counterpart of the still-unstable + `-Z trim-paths` option, and it leaves code generation untouched. + +To reproduce a release binary locally and compare its checksum, replay the same +environment: + +```sh +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="--remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=/cargo --remap-path-prefix=$PWD=/build" +cargo build --release --all-features +sha256sum target/release/pdu +``` From 6e76ac51aaec8fefe719ae802032a4e2e73a10d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 21:02:21 +0000 Subject: [PATCH 03/10] ci(deploy): drop unnecessary codegen-units pin The pin was not required for reproducibility. Because the toolchain is pinned, the release build uses the same default codegen-units on every host, and that default is already deterministic. Verified by building the same sources with the pin removed at `-j1`, `-j16`, and `-j64`, and under a restricted CPU affinity, each producing an identical binary. The default also keeps parallel code generation, so removing the pin does not serialize the build. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- Cargo.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 69716ad7..272a9f1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,8 +115,3 @@ which = "8.0.2" # artifact. This keeps stripping deterministic and identical on every host, # instead of depending on the version of an external `strip` program. strip = "symbols" -# Keep the default parallel code generation. On a fixed toolchain the output of -# parallel codegen is already deterministic, so reproducibility does not require -# serializing compilation with `codegen-units = 1`. Pinning the value preserves -# this parallelism against any future change to the default. -codegen-units = 16 From 0bb82402baa475e709fe9069f38aa362084baecf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 21:02:36 +0000 Subject: [PATCH 04/10] docs(contributing): trim redundant reproducible-builds prose Remove the paragraph and the bullet list that restated what the `Cargo.toml` profile and the reproducible-build script already document in their own comments. The section now states the reproducibility guarantee and the way to verify it, without duplicating the mechanism details. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- CONTRIBUTING.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c42abbe..089fa9df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -539,23 +539,6 @@ checksums, follow from the source code and the pinned toolchain alone. A build performed on a different host, with a different number of threads, or at a different time yields the same artifact. -Reproducibility does not cost compilation speed. Parallel code generation is -deterministic on a fixed toolchain, so the release profile keeps the default -`codegen-units` instead of serializing the build with `codegen-units = 1`. - -Two mechanisms remove the host-specific information that would otherwise differ -between machines: - -- The `[profile.release]` section of `Cargo.toml` sets `strip = "symbols"`, so - the toolchain strips the binary while linking. The result no longer depends on - the version of an external `strip` program, which was previously invoked as a - separate step and produced host-dependent output. -- The `ci/github-actions/reproducible-build-env.sh` script, run by the - deployment workflow before each build, adds `--remap-path-prefix` entries that - replace the absolute paths of the cargo home and the workspace with fixed - placeholders. This is the stable counterpart of the still-unstable - `-Z trim-paths` option, and it leaves code generation untouched. - To reproduce a release binary locally and compare its checksum, replay the same environment: From d94b149523f1bdd22a490fae3433e1980804c6a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 21:05:25 +0000 Subject: [PATCH 05/10] ci(deploy): add script to reproduce and check a release binary Move the local reproduction commands out of `CONTRIBUTING.md` and into `verify-reproducible-build.sh`, alongside `run.sh`, `test.sh`, and `generate-completions.sh`. The script rebuilds a release binary with the reproducible environment, mirroring the deployment workflow including the explicit target, and prints its checksum for comparison against a published release. The target defaults to the primary Linux target and can be overridden. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- CONTRIBUTING.md | 11 ++--------- verify-reproducible-build.sh | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) create mode 100755 verify-reproducible-build.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 089fa9df..103e710a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -539,12 +539,5 @@ checksums, follow from the source code and the pinned toolchain alone. A build performed on a different host, with a different number of threads, or at a different time yields the same artifact. -To reproduce a release binary locally and compare its checksum, replay the same -environment: - -```sh -export CARGO_INCREMENTAL=0 -export RUSTFLAGS="--remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=/cargo --remap-path-prefix=$PWD=/build" -cargo build --release --all-features -sha256sum target/release/pdu -``` +To reproduce a release binary locally and compare its checksum against a +published release, run `./verify-reproducible-build.sh`. diff --git a/verify-reproducible-build.sh b/verify-reproducible-build.sh new file mode 100755 index 00000000..eddf2619 --- /dev/null +++ b/verify-reproducible-build.sh @@ -0,0 +1,20 @@ +#! /bin/bash +set -o errexit -o pipefail -o nounset + +cd "$(dirname "$0")" + +# Rebuild a release binary with the environment that makes it reproducible, then +# print its checksum. Compare the checksum against the matching published release +# to confirm that the release follows from this source and this toolchain. +# +# The build mirrors the deployment workflow, including the explicit target, so +# the result matches the released artifact for that target. The target defaults +# to the primary Linux target and may be overridden, for example with +# x86_64-unknown-linux-musl. +target="${1:-x86_64-unknown-linux-gnu}" + +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="--remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=/cargo --remap-path-prefix=$PWD=/build" + +cargo build --release --all-features --target "$target" +sha256sum "target/$target/release/pdu" From e2bb0081302959ae2029f68cd25915ccb7eb7ce9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 12:23:08 +0000 Subject: [PATCH 06/10] ci(deploy): remap the toolchain sysroot for reproducibility The standard library embeds its own source paths into the binary through panic locations. They normally read `/rustc//...`, but when the `rust-src` component is installed the compiler rewrites them to the local toolchain directory under the sysroot, which differs from one machine to the next. The reproducible-build environment remapped the cargo home and the workspace but not the sysroot, so a build on a machine with `rust-src` installed produced a different binary from one without it. Add a third `--remap-path-prefix` that maps the sysroot rust source directory back to the canonical `/rustc/` form, in both the deployment environment script and `verify-reproducible-build.sh`. Verified by building with `rust-src` installed: the result is now byte-identical to the build without it. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- ci/github-actions/reproducible-build-env.sh | 13 +++++++++++++ verify-reproducible-build.sh | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/ci/github-actions/reproducible-build-env.sh b/ci/github-actions/reproducible-build-env.sh index 56ca4a1b..0ad8a3b4 100755 --- a/ci/github-actions/reproducible-build-env.sh +++ b/ci/github-actions/reproducible-build-env.sh @@ -19,6 +19,19 @@ workspace="${GITHUB_WORKSPACE:-$PWD}" # still-unstable "-Z trim-paths" option, and it does not affect codegen. remap="--remap-path-prefix=$cargo_home=/cargo" remap="$remap --remap-path-prefix=$workspace=/build" + +# The standard library's own source paths are baked into the binary through +# panic locations. They normally read "/rustc//...", but when the +# "rust-src" component is installed the compiler rewrites them to the local +# toolchain directory, which differs from one machine to the next. Remap that +# directory back to the canonical form, so the result no longer depends on +# whether "rust-src" happens to be installed. +commit_hash="$(rustc --version --verbose | awk '/^commit-hash:/ { print $2 }')" +rust_src="$(rustc --print sysroot)/lib/rustlib/src/rust" +if [[ -n "$commit_hash" ]]; then + remap="$remap --remap-path-prefix=$rust_src=/rustc/$commit_hash" +fi + if [[ -n "${RUSTFLAGS:-}" ]]; then remap="$RUSTFLAGS $remap" fi diff --git a/verify-reproducible-build.sh b/verify-reproducible-build.sh index eddf2619..987bd132 100755 --- a/verify-reproducible-build.sh +++ b/verify-reproducible-build.sh @@ -13,8 +13,19 @@ cd "$(dirname "$0")" # x86_64-unknown-linux-musl. target="${1:-x86_64-unknown-linux-gnu}" +# Remap the machine-specific absolute prefixes that the compiler would otherwise +# embed into the binary: the dependency sources under the cargo home, the crate +# built from this directory, and the standard library sources under the toolchain +# sysroot. The last one matters when the "rust-src" component is installed, +# because the compiler then rewrites the standard library's "/rustc/" +# paths to the local toolchain directory. +commit_hash="$(rustc --version --verbose | awk '/^commit-hash:/ { print $2 }')" +rust_src="$(rustc --print sysroot)/lib/rustlib/src/rust" export CARGO_INCREMENTAL=0 export RUSTFLAGS="--remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=/cargo --remap-path-prefix=$PWD=/build" +if [[ -n "$commit_hash" ]]; then + export RUSTFLAGS="$RUSTFLAGS --remap-path-prefix=$rust_src=/rustc/$commit_hash" +fi cargo build --release --all-features --target "$target" sha256sum "target/$target/release/pdu" From 49d6d3c6a2e02744a95c1858ea4009ff0fb5c9b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 15:05:01 +0000 Subject: [PATCH 07/10] ci(deploy): build linux binaries in a pinned container The GNU target links its C runtime, the glibc startup objects and the GCC version string, from the build environment rather than from the Rust toolchain. A bare host build therefore varies between distributions even after paths are remapped, so two hosts produce different binaries. Add `reproducible-build.sh`, which builds a release binary inside a container image pinned by digest. The image ships rustc 1.97.1 on Debian bullseye, so the GCC and glibc are fixed and the result is identical on every host. Bullseye's glibc 2.31 keeps the required glibc version low for broad compatibility, and the binary remains an ordinary dynamically linked ELF rather than being tied to any special filesystem. The script detects Docker or Podman, forwards proxy variables for networks that need them, and mounts the workspace read-only. The `build_linux` job now calls this script instead of installing a toolchain and building on the runner, and `verify-reproducible-build.sh` delegates to it so a local checksum matches the released artifact. The macOS and Windows jobs are unchanged, since a Linux container cannot build them. Verified in a container for both targets: `x86_64-unknown-linux-gnu` and `x86_64-unknown-linux-musl` each build byte-identically across repeated runs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- .github/workflows/deploy.yaml | 22 +++-------- reproducible-build.sh | 70 +++++++++++++++++++++++++++++++++++ verify-reproducible-build.sh | 27 +++----------- 3 files changed, 81 insertions(+), 38 deletions(-) create mode 100755 reproducible-build.sh diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index bbba7fec..e4d1cbf4 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -79,22 +79,12 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Install Rust - shell: bash - run: | - installer=$(mktemp -d)/install-rustup - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > $installer - bash $installer --default-toolchain $(cat rust-toolchain) -y - - - name: Install compilation target - run: rustup target add ${{ matrix.target }} - - - name: Configure reproducible build environment - shell: bash - run: ./ci/github-actions/reproducible-build-env.sh - - - name: Build - run: cargo build --target ${{ matrix.target }} --release --all-features + # The Linux binaries are built inside a digest-pinned container image so + # that their C runtime, and therefore the exact bytes, are identical on + # every host. The image provides Rust, so no separate toolchain + # installation step is required. Docker is preinstalled on the runner. + - name: Build in pinned container + run: ./reproducible-build.sh ${{ matrix.target }} - name: Attest build provenance uses: actions/attest-build-provenance@v4 diff --git a/reproducible-build.sh b/reproducible-build.sh new file mode 100755 index 00000000..393551f2 --- /dev/null +++ b/reproducible-build.sh @@ -0,0 +1,70 @@ +#! /bin/bash +set -o errexit -o pipefail -o nounset + +cd "$(dirname "$0")" + +# Build a release binary inside a pinned container image, so the result is +# identical no matter which host runs the build. The GNU target links its C +# runtime (the glibc startup objects and the GCC version string) from the build +# environment rather than from the Rust toolchain, so a bare host build varies +# between distributions. Fixing the image fixes that C runtime. +# +# The image is pinned by digest, not by tag, so the toolchain, GCC, and glibc +# can never drift. It ships rustc 1.97.1 on Debian bullseye, whose glibc 2.31 +# keeps the required glibc version low for broad compatibility. +target="${1:-x86_64-unknown-linux-gnu}" +image="rust:1.97.1-slim-bullseye@sha256:dd159e8e44aaa84dffbb7f24a742a96b8d0a26920bd3b46ec2b1778be0da1cdf" + +# Detect a container runtime. PDU_CONTAINER_RUNTIME overrides the choice. +runtime="${PDU_CONTAINER_RUNTIME:-}" +if [[ -z "$runtime" ]]; then + for candidate in docker podman; do + if command -v "$candidate" >/dev/null; then + runtime="$candidate" + break + fi + done +fi +if [[ -z "$runtime" ]]; then + echo "error: no container runtime found; install docker or podman, or set PDU_CONTAINER_RUNTIME" >&2 + exit 1 +fi +echo "reproducible-build: using $runtime with $image" >&2 + +mkdir -p target + +# The workspace is mounted read-only so the build cannot modify the source, and +# only the target directory is writable. Proxy variables are forwarded for +# networks that require them, and they are absent on an ordinary network. +# PDU_CONTAINER_RUN_ARGS passes extra runtime flags for unusual environments, +# such as a custom CA certificate mount. +# shellcheck disable=SC2086 +"$runtime" run --rm --interactive \ + --env HTTPS_PROXY --env HTTP_PROXY --env NO_PROXY \ + --env https_proxy --env http_proxy --env no_proxy \ + --volume "$PWD":/build:ro \ + --volume "$PWD/target":/build/target \ + ${PDU_CONTAINER_RUN_ARGS:-} \ + "$image" \ + bash -s -- "$target" <<'INNER' +set -o errexit -o pipefail -o nounset +target="$1" +cd /build + +# The chosen target is not always the image's host target, so make sure its +# standard library is present. +rustup target add "$target" >/dev/null + +# Remap the machine-independent but still absolute paths that the compiler +# embeds, exactly as an ordinary reproducible build would. Inside the pinned +# image these prefixes are already fixed, so this mainly keeps the embedded +# paths canonical and matches the non-container build environment. +commit_hash="$(rustc --version --verbose | awk '/^commit-hash:/ { print $2 }')" +rust_src="$(rustc --print sysroot)/lib/rustlib/src/rust" +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="--remap-path-prefix=${CARGO_HOME:-/usr/local/cargo}=/cargo --remap-path-prefix=$rust_src=/rustc/$commit_hash" + +cargo build --release --all-features --target "$target" +INNER + +echo "reproducible-build: built target/$target/release/pdu" >&2 diff --git a/verify-reproducible-build.sh b/verify-reproducible-build.sh index 987bd132..2d13e06f 100755 --- a/verify-reproducible-build.sh +++ b/verify-reproducible-build.sh @@ -3,29 +3,12 @@ set -o errexit -o pipefail -o nounset cd "$(dirname "$0")" -# Rebuild a release binary with the environment that makes it reproducible, then -# print its checksum. Compare the checksum against the matching published release -# to confirm that the release follows from this source and this toolchain. -# -# The build mirrors the deployment workflow, including the explicit target, so -# the result matches the released artifact for that target. The target defaults -# to the primary Linux target and may be overridden, for example with +# Rebuild a release binary inside the pinned container and print its checksum. +# Building inside the container makes the result independent of the host, so the +# checksum can be compared against a published release. The target defaults to +# the primary Linux target and may be overridden, for example with # x86_64-unknown-linux-musl. target="${1:-x86_64-unknown-linux-gnu}" -# Remap the machine-specific absolute prefixes that the compiler would otherwise -# embed into the binary: the dependency sources under the cargo home, the crate -# built from this directory, and the standard library sources under the toolchain -# sysroot. The last one matters when the "rust-src" component is installed, -# because the compiler then rewrites the standard library's "/rustc/" -# paths to the local toolchain directory. -commit_hash="$(rustc --version --verbose | awk '/^commit-hash:/ { print $2 }')" -rust_src="$(rustc --print sysroot)/lib/rustlib/src/rust" -export CARGO_INCREMENTAL=0 -export RUSTFLAGS="--remap-path-prefix=${CARGO_HOME:-$HOME/.cargo}=/cargo --remap-path-prefix=$PWD=/build" -if [[ -n "$commit_hash" ]]; then - export RUSTFLAGS="$RUSTFLAGS --remap-path-prefix=$rust_src=/rustc/$commit_hash" -fi - -cargo build --release --all-features --target "$target" +./reproducible-build.sh "$target" sha256sum "target/$target/release/pdu" From a3f9d9fcff888471b8511278fabd5509819ad269 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 15:05:01 +0000 Subject: [PATCH 08/10] docs(contributing): note the containerized reproducible build Update the reproducible-builds section to describe the pinned container image as the source of host independence, and note that the verification script needs Docker or Podman and accepts a target argument. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- CONTRIBUTING.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 103e710a..547a449d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -534,10 +534,11 @@ FMT=true LINT=true BUILD=true TEST=true DOC=true DYLINT=true ./test.sh ## Reproducible Builds -The release binaries are built so that their exact bytes, including the -checksums, follow from the source code and the pinned toolchain alone. A build -performed on a different host, with a different number of threads, or at a -different time yields the same artifact. - -To reproduce a release binary locally and compare its checksum against a -published release, run `./verify-reproducible-build.sh`. +The Linux release binaries are built inside a container image pinned by digest, +so their exact bytes are identical on every host, independent of the host's +distribution, thread count, or clock. + +To rebuild a release binary and compare its checksum against a published +release, run `./verify-reproducible-build.sh` (requires Docker or Podman). The +target defaults to `x86_64-unknown-linux-gnu`; pass another as the first +argument, such as `x86_64-unknown-linux-musl`. From 1feaec8e9acdab9d94cb7ff5f06cf0a839b1dee1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 18:07:21 +0000 Subject: [PATCH 09/10] ci(deploy): extract the in-container build into a script The commands that run inside the container lived in a heredoc, where they were invisible to `bash -n` and to shellcheck, and awkward to edit. Move them to `ci/build-in-container.sh`, which the container runs from the mounted workspace. The resulting binaries are unchanged, verified by rebuilding both Linux targets and comparing their checksums against the previous ones. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- ci/build-in-container.sh | 19 +++++++++++++++++++ reproducible-build.sh | 21 +-------------------- 2 files changed, 20 insertions(+), 20 deletions(-) create mode 100755 ci/build-in-container.sh diff --git a/ci/build-in-container.sh b/ci/build-in-container.sh new file mode 100755 index 00000000..89b3e2b9 --- /dev/null +++ b/ci/build-in-container.sh @@ -0,0 +1,19 @@ +#! /bin/bash +set -o errexit -o pipefail -o nounset + +# This script runs inside the container started by reproducible-build.sh, where +# the workspace is mounted at /build. It is not meant to be run on the host. +target="$1" +cd /build + +# The chosen target is not always the image's host target. +rustup target add "$target" >/dev/null + +# Keep the paths that the compiler embeds canonical, matching the remapping that +# a build outside the container performs. +commit_hash="$(rustc --version --verbose | awk '/^commit-hash:/ { print $2 }')" +rust_src="$(rustc --print sysroot)/lib/rustlib/src/rust" +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="--remap-path-prefix=${CARGO_HOME:-/usr/local/cargo}=/cargo --remap-path-prefix=$rust_src=/rustc/$commit_hash" + +cargo build --release --all-features --target "$target" diff --git a/reproducible-build.sh b/reproducible-build.sh index 393551f2..1c405eed 100755 --- a/reproducible-build.sh +++ b/reproducible-build.sh @@ -46,25 +46,6 @@ mkdir -p target --volume "$PWD/target":/build/target \ ${PDU_CONTAINER_RUN_ARGS:-} \ "$image" \ - bash -s -- "$target" <<'INNER' -set -o errexit -o pipefail -o nounset -target="$1" -cd /build - -# The chosen target is not always the image's host target, so make sure its -# standard library is present. -rustup target add "$target" >/dev/null - -# Remap the machine-independent but still absolute paths that the compiler -# embeds, exactly as an ordinary reproducible build would. Inside the pinned -# image these prefixes are already fixed, so this mainly keeps the embedded -# paths canonical and matches the non-container build environment. -commit_hash="$(rustc --version --verbose | awk '/^commit-hash:/ { print $2 }')" -rust_src="$(rustc --print sysroot)/lib/rustlib/src/rust" -export CARGO_INCREMENTAL=0 -export RUSTFLAGS="--remap-path-prefix=${CARGO_HOME:-/usr/local/cargo}=/cargo --remap-path-prefix=$rust_src=/rustc/$commit_hash" - -cargo build --release --all-features --target "$target" -INNER + bash /build/ci/build-in-container.sh "$target" echo "reproducible-build: built target/$target/release/pdu" >&2 From bf01fe55c864f2431920294f13d3c2f4dd82777b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 18:10:57 +0000 Subject: [PATCH 10/10] docs: trim the reproducible-build comments Several comments explained more than a reader needs, restating what the adjacent line already says or recording rationale from the review discussion. Keep the parts that carry a fact which is not evident from the code, such as the pinned toolchain versions, the reason the GNU target depends on the build environment, and the effect of the `rust-src` component on embedded paths. Remove or shorten the rest. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UjrvAEQT3ryDqfspssN3g --- .github/workflows/deploy.yaml | 5 +--- Cargo.toml | 7 +---- ci/github-actions/reproducible-build-env.sh | 29 +++++---------------- reproducible-build.sh | 17 +++++------- verify-reproducible-build.sh | 7 ++--- 5 files changed, 17 insertions(+), 48 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e4d1cbf4..a8e7564f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -79,10 +79,7 @@ jobs: steps: - uses: actions/checkout@v7 - # The Linux binaries are built inside a digest-pinned container image so - # that their C runtime, and therefore the exact bytes, are identical on - # every host. The image provides Rust, so no separate toolchain - # installation step is required. Docker is preinstalled on the runner. + # The container image provides Rust, so no toolchain install step is needed. - name: Build in pinned container run: ./reproducible-build.sh ${{ matrix.target }} diff --git a/Cargo.toml b/Cargo.toml index 272a9f1b..c34d7cdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,11 +107,6 @@ pretty_assertions = "1.4.1" rand = "0.10.1" which = "8.0.2" -# The release profile is tuned for reproducible builds. The compiled binary -# should be determined solely by the source code and the pinned toolchain, -# never by the host that happens to compile it. [profile.release] -# Strip symbols as part of linking, so the toolchain itself produces the final -# artifact. This keeps stripping deterministic and identical on every host, -# instead of depending on the version of an external `strip` program. +# Strip while linking, so the result does not depend on an external `strip`. strip = "symbols" diff --git a/ci/github-actions/reproducible-build-env.sh b/ci/github-actions/reproducible-build-env.sh index 0ad8a3b4..e9599f18 100755 --- a/ci/github-actions/reproducible-build-env.sh +++ b/ci/github-actions/reproducible-build-env.sh @@ -2,30 +2,20 @@ set -o errexit -o pipefail -o nounset # Emit the environment variables that make the release binary reproducible into -# "$GITHUB_ENV", so that every later step in the job inherits them. The goal is -# for the compiled artifact to depend only on the source code and the pinned -# toolchain, never on the host that performs the build. -# -# None of these variables reduce parallelism. Parallel code generation is -# already deterministic on a fixed toolchain, so the build stays multi-threaded. +# "$GITHUB_ENV", so that every later step in the job inherits them. cargo_home="${CARGO_HOME:-$HOME/.cargo}" workspace="${GITHUB_WORKSPACE:-$PWD}" # Replace the machine-specific absolute prefixes that the compiler would -# otherwise embed into the binary. The dependency sources live under the cargo -# home, and the crate itself is built from the workspace directory. Both paths -# differ from one machine to the next. This is the stable equivalent of the -# still-unstable "-Z trim-paths" option, and it does not affect codegen. +# otherwise embed into the binary. This is the stable equivalent of the +# still-unstable "-Z trim-paths" option. remap="--remap-path-prefix=$cargo_home=/cargo" remap="$remap --remap-path-prefix=$workspace=/build" -# The standard library's own source paths are baked into the binary through -# panic locations. They normally read "/rustc//...", but when the -# "rust-src" component is installed the compiler rewrites them to the local -# toolchain directory, which differs from one machine to the next. Remap that -# directory back to the canonical form, so the result no longer depends on -# whether "rust-src" happens to be installed. +# When the "rust-src" component is installed, the compiler rewrites the standard +# library's "/rustc//..." paths to the local toolchain directory. +# Remap them back, so the result does not depend on whether it is installed. commit_hash="$(rustc --version --verbose | awk '/^commit-hash:/ { print $2 }')" rust_src="$(rustc --print sysroot)/lib/rustlib/src/rust" if [[ -n "$commit_hash" ]]; then @@ -38,12 +28,7 @@ fi { echo "RUSTFLAGS=$remap" - # Incremental compilation caches carry host-specific state and are already - # disabled for the release profile. Pin the value so a stray environment - # setting cannot turn it back on. echo "CARGO_INCREMENTAL=0" - # Provide a fixed timestamp for any build script that would otherwise read the - # wall clock. The current sources embed no timestamp, so this is a safeguard - # rather than a fix for a present problem. + # A fixed timestamp for any build script that would otherwise read the clock. echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" } >>"$GITHUB_ENV" diff --git a/reproducible-build.sh b/reproducible-build.sh index 1c405eed..2022b6d3 100755 --- a/reproducible-build.sh +++ b/reproducible-build.sh @@ -3,15 +3,12 @@ set -o errexit -o pipefail -o nounset cd "$(dirname "$0")" -# Build a release binary inside a pinned container image, so the result is -# identical no matter which host runs the build. The GNU target links its C -# runtime (the glibc startup objects and the GCC version string) from the build -# environment rather than from the Rust toolchain, so a bare host build varies -# between distributions. Fixing the image fixes that C runtime. +# Build a release binary inside a pinned container image. The GNU target takes +# its C runtime from the build environment rather than from the Rust toolchain, +# so pinning the image is what makes the output identical on every host. # -# The image is pinned by digest, not by tag, so the toolchain, GCC, and glibc -# can never drift. It ships rustc 1.97.1 on Debian bullseye, whose glibc 2.31 -# keeps the required glibc version low for broad compatibility. +# The image ships rustc 1.97.1 on Debian bullseye, whose glibc 2.31 keeps the +# required glibc version low for broad compatibility. target="${1:-x86_64-unknown-linux-gnu}" image="rust:1.97.1-slim-bullseye@sha256:dd159e8e44aaa84dffbb7f24a742a96b8d0a26920bd3b46ec2b1778be0da1cdf" @@ -33,9 +30,7 @@ echo "reproducible-build: using $runtime with $image" >&2 mkdir -p target -# The workspace is mounted read-only so the build cannot modify the source, and -# only the target directory is writable. Proxy variables are forwarded for -# networks that require them, and they are absent on an ordinary network. +# The workspace is mounted read-only, so only the target directory is writable. # PDU_CONTAINER_RUN_ARGS passes extra runtime flags for unusual environments, # such as a custom CA certificate mount. # shellcheck disable=SC2086 diff --git a/verify-reproducible-build.sh b/verify-reproducible-build.sh index 2d13e06f..005611d0 100755 --- a/verify-reproducible-build.sh +++ b/verify-reproducible-build.sh @@ -3,11 +3,8 @@ set -o errexit -o pipefail -o nounset cd "$(dirname "$0")" -# Rebuild a release binary inside the pinned container and print its checksum. -# Building inside the container makes the result independent of the host, so the -# checksum can be compared against a published release. The target defaults to -# the primary Linux target and may be overridden, for example with -# x86_64-unknown-linux-musl. +# Rebuild a release binary and print its checksum for comparison against a +# published release. target="${1:-x86_64-unknown-linux-gnu}" ./reproducible-build.sh "$target"