Reproducible Compilation
Identical inputs, byte-identical output. What breaks it is mundane — timestamps, absolute paths, hash-map iteration order inside the compiler, parallelism-dependent naming, embedded build IDs — and fixing it is what makes independent verification possible at all.
Why do two builds of the same source produce different binaries, and does it matter?
Compilation as a function from a declared input set to an artifact. Reproducibility is the claim that this function is deterministic — that the artifact is fully determined by the inputs and by nothing else. Everything that breaks it is an undeclared input: the clock, the working directory, the memory allocator's addresses, the number of build threads. Naming the function's domain honestly is the whole exercise.
A build is reproducible only if every value that reaches the output is a function of the declared inputs. That excludes wall-clock time, absolute paths, hostnames, user names, environment variables not in the declared set, random or address-derived values including hash seeds and pointer-ordered iteration, and anything whose value depends on scheduling or on how work was distributed across threads. Where a genuinely varying value must be embedded, it must be derivable from the inputs — a source date taken from the last commit rather than from the clock — or the comparison must exclude it explicitly.
Key points
- Reproducibility means the artifact is a function of the declared inputs and nothing else; everything that breaks it is an undeclared input.
- The recurring causes are timestamps, absolute paths, address-dependent iteration order, parallelism-dependent naming and deliberately-unique identifiers.
SOURCE_DATE_EPOCHand-ffile-prefix-mapare the standard remedies for the first two, and derive the value from an input rather than observing the environment.- It makes independent verification possible, which is the practical answer to the trusting-trust problem, and it makes build caching and bisecting sound.
- It is the prerequisite for the stage 2 / stage 3 bootstrap comparison being a meaningful test at all.
- Every reproducibility claim is relative to a declared input set and a set of comparison exclusions, and both should be stated.
The five things that break it
SOURCE_DATE_EPOCH and the -ffile-prefix-map family are conventions honoured by GCC, Clang and a growing set of build tooling, not language requirements. Support varies by tool and version, and older toolchains use the narrower -fdebug-prefix-map. Any claim that a project is reproducible is relative to a specific toolchain and a specific set of comparison exclusions.Non-reproducibility is almost never mysterious. It is a small set of recurring causes, and once you know them, diagnosing a difference is a matter of checking the list.
Timestamps. __DATE__ and __TIME__ in C, build times embedded in headers, archive member timestamps in .a files, and file modification times captured into artifacts. The standard remedy is the SOURCE_DATE_EPOCH convention: derive the timestamp from the source — usually the last commit date — so it is an input rather than an observation.
Absolute paths. Debug information records the compilation directory and source paths, so building in /home/alice/proj and /build produces different DWARF. -fdebug-prefix-map and -ffile-prefix-map rewrite them to a canonical form, and __FILE__ needs the same treatment.
Iteration order inside the compiler. This is the subtle one and the most instructive. If a compiler pass iterates over a hash map keyed by pointers, the order depends on allocation addresses, which depend on ASLR — so the same source produces differently-ordered output on every run. Compilers have found and fixed many of these; the general rule is that any container whose iteration order is observable in the output must have a deterministic order.
Parallelism. Anything whose name or number depends on which thread got there first: temporary file names, generated symbol suffixes, the order in which independently-compiled units are combined. A build that is reproducible with -j1 and not with -j8 has one of these.
Deliberately unique identifiers. Build IDs, UUIDs generated per build, and embedded version strings that include a build number. These need to be derived from content — a hash of the inputs — rather than generated, or excluded from the comparison explicitly.
| Cause | Where it shows up | Remedy |
|---|---|---|
| Wall-clock timestampstypical | __DATE__, archive members, embedded build times, file mtimes in packages | SOURCE_DATE_EPOCH from the last commit; deterministic archive mode (ar D) |
| Absolute pathstypical | DWARF compilation directory, __FILE__, assertion messages, RPATH | -ffile-prefix-map=$PWD=., build in a canonical directory |
| Address-dependent ordering | Symbol order, section order, anything iterated from a pointer-keyed container | Deterministic containers and explicit sorting inside the compiler |
| Parallelism | Temporary names, generated symbol suffixes, link order of independently built units | Names derived from content or from a declared index rather than from scheduling |
| Unique build identifiers | Build IDs, UUIDs, embedded build numbers | Derive from a hash of the inputs, or exclude from the comparison explicitly |
| Environment leakagetypical | Hostname, user, locale affecting sort order or number formatting | Hermetic build with a declared environment — see [[hermetic-compilation]] |
Why it is worth the work
The security argument is the one that funded most of the effort. If a build is reproducible, many independent parties can build the same source and compare artifacts. Agreement means no single builder was compromised; disagreement is a loud, specific signal. That converts "trust the release engineer" into "trust that several unrelated parties were not compromised identically", which is the practical descendant of the reasoning in [[trusting-trust]].
The engineering arguments are less dramatic and probably more valuable day to day. Reproducibility makes build caching sound: if the output is a function of the inputs, a cache keyed by the inputs is correct, which is what makes distributed build caches in Bazel and similar systems work at all. It makes a bisect meaningful, because a behavior difference between two builds must come from a source difference. And it removes an entire class of "works on my machine" where the artifact, not the environment, was different.
It also makes the stage 2 / stage 3 bootstrap comparison meaningful, which is the connection back to [[self-hosting]]: without determinism that check produces differences that prove nothing, and teams eventually disable it.
Our own compiler is deterministic by construction
AtlasLang is a small enough system to have this property for free, and it is worth saying how rather than merely claiming it. Nothing in src/compilers/sim reads a clock, a path, an environment variable or a random source. Every pass iterates arrays and maps in insertion order, virtual registers are numbered from a counter that starts at zero for each function, and there is no parallelism anywhere. There is nothing available to be non-deterministic with.
That is asserted rather than assumed. scripts/compilers-sim.test.ts compiles every example twice and asserts that the IR text, the SSA text, the optimized IR text and the emitted assembly are all byte-identical between the two runs. The test would fail on any of the five causes above, and it is cheap enough to run on every change.
The honest caveat is that this is easy for us and hard for a real compiler. We have no threads, no allocator whose addresses we can accidentally observe, no debug information containing paths, no archives with timestamps, and no build system. Determinism at our scale is a matter of not doing anything careless; determinism at GCC's scale is a decade-long programme. What our test demonstrates is the *shape* of the property and the fact that it is checkable, not that achieving it is easy.
1test('compiling the same source twice produces byte-identical output at every stage', () => {2 for (const ex of EXAMPLES) {3 const a = compile(ex.source, ...)4 const b = compile(ex.source, ...)5 assert.deepEqual(a.irText, b.irText)6 assert.deepEqual(a.ssaText, b.ssaText)7 assert.deepEqual(a.optimizedText, b.optimizedText)8 assert.deepEqual(9 a.assembly.lines.map((l) => l.text),10 b.assembly.lines.map((l) => l.text),11 )12 }13})Comparing all four stages rather than only the final assembly is deliberate: a difference in the IR that happens to be erased by a later pass is still a determinism bug, and it will surface as a difference in some other program. Checking the intermediate representations localises the cause to a stage, which is the same reason the pipeline explorer shows all of them.
What "reproducible" is always relative to
Every reproducibility claim is relative to two things that ought to be stated with it: what counts as the input set, and what is excluded from the comparison. A project that reproduces given the same container image, the same toolchain version and the same source is making a much narrower claim than one that reproduces across distributions and compiler versions, and both are useful.
Exclusions matter just as much. Signatures cannot be reproducible, since a signature depends on a key. Embedded build identifiers are often normalised away. Debug sections are sometimes compared separately or excluded. A claim of byte-identity that quietly excludes a third of the file is not the same claim as one that does not, and reading the exclusion list is part of understanding the guarantee.
The tooling is mature enough that this is checkable rather than aspirational. diffoscope recursively unpacks archives, disassembles binaries and decodes metadata to explain *why* two artifacts differ, which is what turns "these files differ" into an actionable bug. reprotest builds twice under deliberately varied conditions — different paths, times, hostnames, locales, thread counts — to find the causes before someone else does. Running either on a project that has never been checked is usually instructive within minutes.
- State the input set: same source, same toolchain version, same container digest — or something broader.
- State the exclusions: signatures, build IDs, and any deliberately-unique field.
diffoscopeexplains differences rather than reporting them, which is what makes them fixable.reprotestvaries path, time, hostname, locale and parallelism deliberately, which finds causes a same-machine rebuild would not.- The Reproducible Builds project publishes per-distribution status, and the numbers are a good calibration of how hard this is at scale.
How it works
The steps, in the order the compiler takes them.
- Identify every value that reaches the output and is not derived from a declared input: times, paths, hostnames, addresses, thread scheduling, generated identifiers.
- Replace observations with derivations: a timestamp from the last commit rather than from the clock, an identifier from a hash of the inputs rather than from a generator.
- Canonicalise paths at compile time with
-ffile-prefix-mapso that the build directory does not appear in debug information. - Make every iteration whose order is observable in the output use a deterministic order, independent of allocation addresses.
- Make names and layout independent of how work was scheduled across threads.
- Build twice under deliberately varied conditions and compare with a tool that explains differences rather than merely reporting them.
- Publish the input set and the exclusions alongside the claim, so that a third party can attempt the same build and know what agreement would mean.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- Two builds of identical source differ, and the cause is
__DATE__in a version banner nobody thought of as an input. - A build reproduces on the same machine and not on another, because debug information records the build directory and the two are different.
- A build reproduces with
-j1and not with-j8, because a generated symbol name depends on which thread reached it first. - A compiler pass iterates a pointer-keyed hash map, so symbol order varies between runs on an address-randomised system and every build differs.
- A distributed build cache serves a stale artifact because the cache key omitted an input the build actually depends on — the same defect as non-reproducibility, surfacing as a wrong build rather than a different one.
- A team declares reproducibility and has never had two independent parties compare artifacts, so the property is asserted and never exercised.
- A bootstrap comparison fails intermittently, the team concludes the check is flaky, and disables the strongest test the toolchain had.
When it helps
- Any situation where somebody other than the builder needs confidence that an artifact corresponds to its source — distributions, security-sensitive software, anything shipped to users who cannot rebuild it.
- Build caching at scale, which is only sound if the output is genuinely a function of the cache key.
- Debugging: eliminating the artifact as a variable means a behavioral difference between two builds must come from an input difference.
When it hurts
- When pursued as an end in itself on software nobody redistributes. The verification benefit requires someone to actually rebuild and compare, and if nobody will, the work buys only the caching and debugging benefits.
- When the exclusion list grows until the claim is vacuous. Excluding everything that differs produces a reproducible build that establishes nothing.
What it costs
Every one of these is paid by something.
- Reproducibility buys independent verification, sound caching and one fewer variable when debugging, and costs the elimination of every convenient environment observation — which is a long tail of small changes across the compiler, the build system and every dependency.
- Deriving timestamps from the source buys determinism and costs accurate build-time metadata, which some deployment and debugging workflows genuinely used.
- Deterministic iteration inside a compiler buys reproducible output and can cost performance, since an insertion-ordered or sorted container is sometimes slower than the hash map it replaces.
- Hermetic, pinned builds buy the input control reproducibility needs and cost incremental-build convenience plus a standing obligation to update pinned inputs that would otherwise have drifted forward on their own.
What else you could do
What a different compiler or language does instead, and when that is better.
- Signed artifacts with provenance attestation — SLSA, sigstore — which record who built what from which inputs. Far cheaper and a weaker property: it identifies the builder rather than allowing anyone to check the result.
- Trusted build infrastructure with restricted access and audit logs, which reduces the probability of compromise instead of making it detectable.
- Content-addressed build systems such as Nix and Guix, where reproducibility is structural rather than retrofitted, at the cost of adopting the whole model.
- Accepting non-reproducibility and comparing behavior instead — extensive testing of the shipped artifact. It catches functional differences and is blind to anything that does not change tested behavior.
See it for yourself
The flag, dump or tool that shows you this directly.
- Find your differences: build twice and run
diffoscope a.deb b.deb(or on any two artifacts). It unpacks recursively and explains the difference rather than just reporting one. - Provoke them deliberately:
reprotestbuilds under varied path, time, hostname, locale, user and parallelism, which finds causes a same-machine rebuild will not. - Fix the common two:
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)and-ffile-prefix-map=$PWD=.remove most first-round differences in a C or C++ project. - Check archives:
arneeds deterministic mode (D) to omit timestamps and uids, and many toolchains now default to it —ar tv lib.ashows whether yours does. - Calibrate: the Reproducible Builds project publishes per-distribution reproducibility statistics, which show both how far the practice has come and how much of it is still open.
- Our own check:
scripts/compilers-sim.test.tscompiles each example twice and asserts byte-identical IR, SSA, optimized IR and assembly.
Plausible wrong readings
Stated the way a confident engineer states them.
- "Reproducible means anyone can rebuild it." It means anyone with the same declared inputs gets the same bytes. If the input set includes a specific container digest, "anyone" means anyone with that image.
- "A lockfile makes a build reproducible." It fixes dependency versions. Timestamps, paths, parallelism and iteration order are all still free to vary.
- "Non-determinism in a compiler is harmless because the code is equivalent." It destroys the strongest test a self-hosted toolchain has, makes caching unsound, and makes independent verification impossible.
- "If two builds differ, one of them is wrong." Usually both are correct and one of them recorded the time. The point is not correctness but verifiability.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
A reproducible build means that compiling the same source with the same tools produces exactly the same bytes, every time and on any machine. Most builds are not, because they record the time or the directory they were built in. Fixing that lets several people build the same thing independently and check they got the same result — which is the only practical way to tell that a released binary matches its source.
practical
Start by building twice and running diffoscope on the results; the first differences are almost always a timestamp or a build path. Set SOURCE_DATE_EPOCH from the last commit and pass -ffile-prefix-map=$PWD=., and check that your archiver is in deterministic mode. Then run reprotest to vary the things a same-machine rebuild does not — hostname, locale, thread count — because those find the parallelism and environment causes. State the input set and the exclusions when you claim the property.
advanced
The conceptual value of this work exceeds its security value, and it is worth naming: making a build reproducible forces you to discover what the build actually depends on. Every difference found is an undeclared input, which means the build was never the function of the source it was described as being. That is why reproducibility, hermeticity and correct caching turn out to be the same project — a sound cache key is exactly an enumeration of the real inputs, and you cannot write one without doing this work. The security property, that independent parties can verify an artifact, then falls out of a discipline that was worth adopting for ordinary engineering reasons. It is one of the better examples in the domain of a rigorous property paying for itself twice.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
SOURCE_DATE_EPOCH, -ffile-prefix-map and deterministic archive mode are conventions honoured by GCC, Clang and much of the surrounding tooling, with support varying by tool and version. They are not language requirements, and a project that reproduces on one toolchain may not on another — every claim is relative to a specific toolchain and a specific exclusion list.scripts/compilers-sim.test.ts proves the property holds for us and says nothing about how hard it is to obtain in a compiler that has all of those.If you were asked this in an interview
- Name five things that stop two builds of identical source from producing identical binaries.
- Why does reproducibility matter for verifying a released binary, and what does agreement between two builders actually establish?
- Why is determinism a prerequisite for the stage 2 / stage 3 bootstrap comparison?
Connections
- DevOps / Production Engineering — Build caching, artifact promotion and rebuild-on-demand pipelinesA sound cache key is an enumeration of a build's real inputs, which is the same discovery reproducibility forces. Designing the caching and promotion pipeline that exploits the property is owned there; making the compiler and build deterministic enough to allow it is here.
- Testing & Reliability Engineering — Eliminating environmental variables so that a difference is a signalReproducibility is flakiness elimination applied to the build rather than to the test suite, and the reasoning is identical: an outcome that varies without an input change cannot be used as evidence. The general practice belongs there.