diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 3e235ee4..a8e7564f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -79,21 +79,9 @@ 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: Build - run: cargo build --target ${{ matrix.target }} --release --all-features - - - name: Strip all debug symbols - run: strip --strip-all target/${{ matrix.target }}/release/pdu + # The container image provides Rust, so no toolchain install step is needed. + - name: Build in pinned container + run: ./reproducible-build.sh ${{ matrix.target }} - name: Attest build provenance uses: actions/attest-build-provenance@v4 @@ -135,12 +123,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 +171,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/CONTRIBUTING.md b/CONTRIBUTING.md index 77ff9238..547a449d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -531,3 +531,14 @@ 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 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`. diff --git a/Cargo.toml b/Cargo.toml index 9dedda03..c34d7cdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,3 +106,7 @@ normalize-path = "0.2.1" pretty_assertions = "1.4.1" rand = "0.10.1" which = "8.0.2" + +[profile.release] +# Strip while linking, so the result does not depend on an external `strip`. +strip = "symbols" 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/ci/github-actions/reproducible-build-env.sh b/ci/github-actions/reproducible-build-env.sh new file mode 100755 index 00000000..e9599f18 --- /dev/null +++ b/ci/github-actions/reproducible-build-env.sh @@ -0,0 +1,34 @@ +#! /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. + +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. 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" + +# 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 + remap="$remap --remap-path-prefix=$rust_src=/rustc/$commit_hash" +fi + +if [[ -n "${RUSTFLAGS:-}" ]]; then + remap="$RUSTFLAGS $remap" +fi + +{ + echo "RUSTFLAGS=$remap" + echo "CARGO_INCREMENTAL=0" + # 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 new file mode 100755 index 00000000..2022b6d3 --- /dev/null +++ b/reproducible-build.sh @@ -0,0 +1,46 @@ +#! /bin/bash +set -o errexit -o pipefail -o nounset + +cd "$(dirname "$0")" + +# 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 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 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 +"$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 /build/ci/build-in-container.sh "$target" + +echo "reproducible-build: built target/$target/release/pdu" >&2 diff --git a/verify-reproducible-build.sh b/verify-reproducible-build.sh new file mode 100755 index 00000000..005611d0 --- /dev/null +++ b/verify-reproducible-build.sh @@ -0,0 +1,11 @@ +#! /bin/bash +set -o errexit -o pipefail -o nounset + +cd "$(dirname "$0")" + +# 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" +sha256sum "target/$target/release/pdu"