reproducible-builds

v2026.09.24

Reproducible builds — bit-for-bit identical artifacts from the same source, independently verifiable. Covers the reproducible-builds.org methodology, Bitcoin Core's Guix-based reproducible builds (the gold standard for cryptocurrency software), Nix Flakes for deterministic environments, source-date-epoch (SOURCE_DATE_EPOCH), build flag normalization (file ordering, locale, paths), .reproducible-builds.org diff tooling (diffoscope), and how to apply this to Rust + Gradle + mobile builds. USE WHEN: user mentions "reproducible builds", "deterministic builds", "bit-for-bit", "Guix builds", "diffoscope", "SOURCE_DATE_EPOCH", "Bitcoin Core build", "Nix flake build", "verify binary identical", "supply-chain attestation" DO NOT USE FOR: Artifact signing - use `security/sigstore-cosign` DO NOT USE FOR: Cross-compile mechanics - use `build-tools/rust-cross-compile` DO NOT USE FOR: General Gradle - use `build-tools/gradle-kmp`

GitHub
Install command
npx skhub add claude-dev-suite/reproducible-builds
Markdown
SKILL.md

Reproducible Builds

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: reproducible-builds.

What & Why

A reproducible build produces bit-for-bit identical binaries from the same source code, independent of who runs the build, when, or where. Essential for:

  • Trust: anyone can verify that a published binary actually matches the source
  • Supply-chain security: detect compromised build infrastructure
  • Wallet/crypto software: users verify binaries match audited source — no hidden changes
  • Regulatory compliance: provable provenance

Bitcoin Core has used Gitian (deprecated) and now Guix for reproducible builds since 2013. BHODL-style wallet apps should follow.

Sources of Non-Determinism

SourceExampleFix
Timestamps in archivestar records mtimeSOURCE_DATE_EPOCH env
Random IDsUUIDs in metadataPin via build script
Build paths/home/alice vs /buildStrip via --remap-path-prefix (Rust) / -fdebug-prefix-map (gcc/clang)
Locale-dependent sortLC_ALL differencesLC_ALL=C
Parallel compilation orderOutput depends on raceSort outputs deterministically
File ordering in archivesglob filesystem orderSort before adding
Compiler versionsgcc 13 vs 14 produces different codePin compiler version
ABI / linkerdynamic vs static, link orderPin linker, use static when possible
Filesystem encodingUTF-8 NFC vs NFDNormalize source paths
Username, hostname embeddedBuild hostname in binaryPatch out via build flags
Kernel/OS version (rare)Some compilers branch on unameContainerize the build

SOURCE_DATE_EPOCH

The standard env var for fixing build timestamps. Most modern build tools honor it.

export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)        # last commit time
# or fixed value
export SOURCE_DATE_EPOCH=1700000000

# Most tools auto-honor:
tar --mtime=@$SOURCE_DATE_EPOCH ...
zip ...                                                     # zip 3.0+
gcc -frecord-gcc-switches ...                              # debug info uses SOURCE_DATE_EPOCH
sphinx-build ...                                            # docs

# Build with normalized timestamps
make

For Rust, use --remap-path-prefix:

# Cargo.toml
[profile.release]
opt-level = 3
strip = true
panic = "abort"

[build]
rustflags = ["--remap-path-prefix", "/home/builder/src=src"]

Bitcoin Core Approach (Guix)

Bitcoin Core uses Guix — a functional package manager + build system that captures every dependency, compiler, and config bit. Builders independently produce identical binaries; signed attestations posted to bitcoin-core/guix.sigs.

# Inside Bitcoin Core repo
./contrib/guix/guix-build

# Output (signed by builder):
guix-build-<commit>/output/<host>/bitcoin-<version>-<host>.tar.gz
guix-build-<commit>/output/<host>/SHA256SUMS.part

# Each builder runs same command, all produce identical SHA256SUMS
# Multiple builders sign attestation file → published as multi-sig proof

Why Guix specifically:

  • Hermetic builds (no host system leakage)
  • Bit-perfect dependency pinning via content-addressed store
  • Cross-compile from one host to many targets
  • Reproducible across Linux distros, including Arch/Debian/NixOS

Trade-offs:

  • Steep learning curve
  • Big initial download (Guix store ~5-10 GB)
  • Mostly Linux-focused (macOS/Windows targets cross-compiled from Linux host)

Nix Flakes (Modern Alternative)

For non-Guix users, Nix Flakes offers similar guarantees with broader ecosystem:

# flake.nix
{
    description = "BHODL reproducible build";
    inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    inputs.flake-utils.url = "github:numtide/flake-utils";
    inputs.rust-overlay.url = "github:oxalica/rust-overlay";

    outputs = { self, nixpkgs, flake-utils, rust-overlay }:
        flake-utils.lib.eachDefaultSystem (system:
            let
                pkgs = import nixpkgs {
                    inherit system;
                    overlays = [ rust-overlay.overlays.default ];
                };
                rust = pkgs.rust-bin.stable."1.85.0".default;
            in {
                packages.default = pkgs.rustPlatform.buildRustPackage {
                    pname = "bhodl-ffi";
                    version = "1.0.0";
                    src = ./.;
                    cargoLock.lockFile = ./Cargo.lock;
                    nativeBuildInputs = [ rust ];
                    SOURCE_DATE_EPOCH = "1735689600";   # fixed
                };
            }
        );
}
nix build
# Output deterministic: result/bin/bhodl-ffi
sha256sum result/bin/bhodl-ffi

flake.lock pins all transitive dependencies by content hash. Two users running same nix build get bit-identical output.

Rust Reproducibility

# .cargo/config.toml
[build]
rustflags = [
    "--remap-path-prefix", "/home/builder=src",
    "--remap-path-prefix", "/Users/builder=src",
]

[profile.release]
strip = true                                    # remove non-deterministic debug paths
panic = "abort"                                 # less variability than unwind
codegen-units = 1                               # deterministic codegen order

[net]
git-fetch-with-cli = false
# Pin Rust toolchain
echo "1.85.0" > rust-toolchain.toml
# Or full TOML:
cat > rust-toolchain.toml <<EOF
[toolchain]
channel = "1.85.0"
components = ["rustfmt", "clippy"]
targets = ["aarch64-apple-ios", "aarch64-linux-android"]
profile = "minimal"
EOF

Cargo.lock must be committed for libraries that need reproducible builds (it's optional for libraries by default; required for binaries).

Verifying Reproducibility — diffoscope

When two builds produce different binaries, find why:

# Install
brew install diffoscope        # or apt
pip install diffoscope          # has many optional formats

# Compare
diffoscope build1/output build2/output
diffoscope build1/binary build2/binary --html report.html

Outputs hierarchical diff: archive members, binary sections, debug info, recursive into nested archives.

For mobile APK comparison:

diffoscope app-1.apk app-2.apk --html-dir report/

Common findings: META-INF order, classes.dex opt order (D8 nondeterminism), .so build paths, asset compression metadata.

Android APK Reproducibility

APK is a .zip with embedded DEX, native libs, resources. Sources of nondeterminism:

SourceFix
Build timestamp in AndroidManifest.xmlNone — accept or strip post-build
META-INF/*.RSA/*.SF orderapksigner v3+ deterministic
classes.dex D8 codegenUse --release mode + pinned D8 version
Resource compression metadataUse -Z store for aapt2 if needed
File ordering in zipapksigner rewrites in fixed order
ProGuard/R8 minificationSet -printseeds, -printusage, -printmapping for diff visibility

Modern AGP (8+) with R8 produces increasingly deterministic output. Pin AGP version, JDK version, NDK version.

// app/build.gradle.kts
android {
    compileSdk = 35
    ndkVersion = "27.1.12297006"               // pin
    defaultConfig {
        vectorDrawables.useSupportLibrary = true
    }
    packaging {
        resources.excludes += setOf(
            "META-INF/MANIFEST.MF",
            "META-INF/build-data.properties",
        )
    }
    signingConfigs {
        create("release") {
            storeFile = file("release.keystore")
            storePassword = System.getenv("KEYSTORE_PASS")
            keyAlias = "bhodl"
            keyPassword = System.getenv("KEY_PASS")
        }
    }
}

Users verify by re-running build, comparing SHA-256 of unsigned APK (app-release-unsigned.apk).

iOS / Xcode Reproducibility

Hardest target. Xcode's build process embeds many timestamps and host-specific paths.

SourceFix
Info.plist CFBundleVersionPin via build script
Compiler timestampsSOURCE_DATE_EPOCH (limited support in Xcode)
Code signingSame cert + provisioning profile
Build server hostnameStrip via dwarfutil --remove-build-machine
dSYM UUIDBest-effort match

Pragmatic approach: provide unsigned .framework reproducibly (Rust XCFramework can be deterministic), then user signs themselves with their own Apple certificate.

For BHODL: ship XCFramework reproducibly + accept that Xcode app build is not fully reproducible. Users verify the framework, sign their own app build.

CI Pattern: Reproducibility Verification

# .github/workflows/reproducible.yml
name: Verify reproducibility

on: [pull_request]

jobs:
  build-twice:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Pin toolchain
        run: rustup show

      - name: Build #1
        run: |
            export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
            cargo build --release --target aarch64-apple-ios
            cp target/aarch64-apple-ios/release/libbhodl_ffi.a /tmp/build1.a
            cargo clean

      - name: Build #2
        run: |
            export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
            cargo build --release --target aarch64-apple-ios
            cp target/aarch64-apple-ios/release/libbhodl_ffi.a /tmp/build2.a

      - name: Compare
        run: |
            sha256sum /tmp/build1.a /tmp/build2.a
            cmp /tmp/build1.a /tmp/build2.a || (
                diffoscope /tmp/build1.a /tmp/build2.a --html /tmp/report.html
                exit 1
            )

If diff found, CI fails and uploads HTML diff for inspection.

Distribution Pattern (Bitcoin Core Style)

For BHODL-style wallet apps:

  1. CI builds reproducibly → produces bhodl-1.0.0.tar.gz + SHA256SUMS
  2. Multiple builders independently rebuild from same source tag → each produces own SHA256SUMS
  3. Builders sign their SHA256SUMS with their PGP/Sigstore key, push to bhodl/build-attestations
  4. Users download binary, fetch attestations, verify N-of-M builders agree on hash, verify signatures
  5. Optional: cosign sign-blob for supply chain attestation (see security/sigstore-cosign)
# User verification flow
wget https://bhodl.app/release/bhodl-1.0.0.tar.gz
wget https://bhodl.app/release/SHA256SUMS
wget https://bhodl.app/release/SHA256SUMS.asc

# Verify multi-sig
gpg --verify SHA256SUMS.asc SHA256SUMS         # checks all sigs
sha256sum -c SHA256SUMS                         # verifies download matches

Rust Specifics — Build Pinning

# rust-toolchain.toml — pin compiler
[toolchain]
channel = "1.85.0"

# Cargo.lock — pin all dependencies (commit it!)

# .cargo/config.toml
[build]
rustflags = [
    "--remap-path-prefix", "/build=src",
    "-C", "link-arg=-Wl,--build-id=none",     # no build ID in ELF
]

[net]
offline = false                                # CI: set true to fail on uncached deps
git-fetch-with-cli = true                     # consistent fetch behavior

For procedural macros / build scripts that read time:

// build.rs — DON'T do this in build script:
//   println!("cargo:rustc-env=BUILD_TIME={}", chrono::Utc::now());

// DO this:
let build_time = std::env::var("SOURCE_DATE_EPOCH")
    .map(|s| s.parse::<i64>().unwrap_or(0))
    .unwrap_or(0);
println!("cargo:rustc-env=BUILD_TIME={}", build_time);

Containerized Build (Docker)

For full hermetic builds without Guix:

# Dockerfile.reproducible
FROM rust:1.85-slim-bookworm@sha256:fixed_digest_here AS builder

ENV SOURCE_DATE_EPOCH=1735689600
ENV LC_ALL=C
ENV TZ=UTC

WORKDIR /build
COPY rust-toolchain.toml ./
RUN rustup show

COPY . .
RUN cargo build --release --target aarch64-unknown-linux-gnu

FROM scratch
COPY --from=builder /build/target/aarch64-unknown-linux-gnu/release/bhodl /bhodl

Build:

docker build -f Dockerfile.reproducible -t bhodl:1.0.0 --no-cache .

Pin base image by digest, not tag. Use --no-cache for clean state. Output binary should be reproducible across runs.

Anti-Patterns

Anti-patternWhy it's badCorrect approach
Embedding Date::now() in buildDifferent binary each buildUse SOURCE_DATE_EPOCH
Username/hostname in build artifactsBinary differs per builderStrip via --remap-path-prefix etc.
Floating Rust toolchain (stable)Compiler updates change outputPin exact version (1.85.0)
Loose Cargo.toml versions (tokio = "1")Patch upgrades change outputCommit Cargo.lock for reproducible binaries
Pulling deps fresh from registry on every buildDep retracted/changedVendor deps or use cargo build --offline
Multi-threaded codegen (codegen-units > 1)Order-dependent outputcodegen-units = 1 for release
Nondeterministic ProGuard/R8 rulesDifferent obfuscation orderPin AGP + KSP versions, deterministic seeds
Stripping debug info post-build inconsistentlySometimes stripped, sometimes notStrip in build profile
tar without --mtime=@$SOURCE_DATE_EPOCHArchive timestamps varyAlways set mtime
Glob ordering in zip/tarFilesystem-dependentSort file list explicitly
Building on host system without containerDrift over timeUse Docker/Guix/Nix for hermeticity

Troubleshooting

SymptomCauseFix
cmp shows diff after second buildSome non-determinism remainingdiffoscope to find source
Binary differs in .debug_* sectionsBuild path leak--remap-path-prefix (Rust) / -fdebug-prefix-map (clang)
Zip files differFile order or compressionUse zip -X -r and sort input
Cargo.lock not consistent across runsAuto-update on buildCommit lock file, use --locked flag
Same Cargo.lock produces diff binariesToolchain version driftPin via rust-toolchain.toml
AAB differs across buildsR8 nondeterminismPin AGP, set -Pandroid.useAndroidX=true, deterministic R8 mode
iOS dSYM differsBuild server hostnameBest-effort: post-process or skip dSYM signing
diffoscope reports META-INF/MANIFEST.MF differsJAR build timestampsSet-MainAttribute Manifest-Build-Jdk-Spec to fixed
Different output on Linux vs macOS hostCross-toolchain divergenceBuild only on one platform per target

When NOT to Use This Skill

ScenarioUse Instead
Artifact signingsecurity/sigstore-cosign
Cross-compile mechanicsbuild-tools/rust-cross-compile
Gradle KMP setupbuild-tools/gradle-kmp
Standard CI/CD without reproducibilityGeneric CI/CD skill
Apple App Store submissionApple-specific (signing irreproducible by design)
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/infrastructure/reproducible-builds

Default branch

main

Latest commit

9496306

Tree SHA

fe4e2f1