Reproducible Builds
- title
- Reproducible Builds
- type
- concept
- summary
- Bit-for-bit identical output from the same source, and why compilers keep breaking it
- tags
- reproducible-builds, compilers, supply-chain, build-systems
- sources
- anubis-wasm-vendor-binary
- created
- 2026-07-21
- updated
- 2026-07-21
A build is reproducible when the same source, built with the same flags on the same target, produces byte-for-byte identical output. The intuition is that a compiler is a deterministic function from source to bytecode, so this should come for free. In practice it doesn't. Real toolchains leak non-source state into their output in ways that only show up when you diff two builds of the same commit.
Why anyone cares: reproducibility is what lets a third party verify a binary. If a project commits a prebuilt binary into its repo (see dependency-vendoring) and claims it was built from a given source, a packager can only trust that claim by rebuilding it and checking the hashes match. Without reproducibility, "here is the binary, and here is the source it came from" is unverifiable. That makes it a supply-chain property, not just a tidiness one.
Where determinism leaks
The anubis-wasm-vendor-binary build log walks through three distinct failure modes, all hit while producing one small vendored binary.
Embedded build metadata. The classic one: __DATE__ and __TIME__ macros stamp the wall-clock time of the compile into the output. Two builds seconds apart differ because they literally record different timestamps. The fix is to never embed build time, or to pin it (SOURCE_DATE_EPOCH is the common convention).
Shelling out to whatever is on $PATH. clang silently invokes wasm-opt from $PATH during a WebAssembly build. Different machines have different wasm-opt versions โ v108 on one box, v130 on another โ so the "same" toolchain produces different bytes, and an old version can outright fail on newer instructions (WebAssembly Exceptions, in the Anubis case). A compiler that reaches outside its own pinned inputs isn't a pure function of its arguments anymore. The Anubis fix was to pass --no-wasm-opt and cut the hidden dependency.
Codegen that depends on memory addresses. clang's exception-handling path let raw pointer values leak into the order it emitted try_table blocks. The computation was identical; the byte ordering and the catch_all_ref indices were not, so builds differed by about 29 bytes. Two mitigations: disable address-space layout randomization with setarch --addr-no-randomize so pointers land in the same places, and record known-good sha256 checksums per architecture with CI verifying them on both x86_64 and arm64.
Cross-architecture iteration order. The pointer-ordering problem also differs between architectures, because pointer iteration order on arm64 isn't the same as on x86_64. Anubis got reproducibility within an architecture but not across โ the remaining gap is an upstream LLVM bug (the iteration order isn't seeded). "Deterministic within an architecture" had to be good enough.
The general shape: a compiler is deterministic in theory, but a production toolchain pulls in the clock, $PATH, ASLR, and address-dependent iteration order. Each has to be pinned or recorded before the output is actually reproducible. Or, as the source put it: "You'd think that given the same bytes of input you'd get the same bytes of output. lol. lmao. No, you don't."
Related
- anubis-wasm-vendor-binary โ the worked example this page is built from
- dependency-vendoring โ reproducibility is what makes a committed vendored binary trustworthy
- supply-chain-security โ the trust chain reproducible builds protect
- abi-stability โ a different binary-level contract; both are about what compiled artifacts guarantee