L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Développement · SKILL.md

reproducible-builds

reproducible-builds

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 o…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: reproducible-builds description: | 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 allowed-tools: Read, Grep, Glob, Write, Edit

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)
Skills enregistrées