Bootstraptypical

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.

The question

Why do two builds of the same source produce different binaries, and does it matter?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

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.

What this phase may assume or do

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_EPOCH and -ffile-prefix-map are 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

typicalSOURCE_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.

Causes and remediestypical
CauseWhere it shows upRemedy
Wall-clock timestampstypical__DATE__, archive members, embedded build times, file mtimes in packagesSOURCE_DATE_EPOCH from the last commit; deterministic archive mode (ar D)
Absolute pathstypicalDWARF compilation directory, __FILE__, assertion messages, RPATH-ffile-prefix-map=$PWD=., build in a canonical directory
Address-dependent orderingSymbol order, section order, anything iterated from a pointer-keyed containerDeterministic containers and explicit sorting inside the compiler
ParallelismTemporary names, generated symbol suffixes, link order of independently built unitsNames derived from content or from a declared index rather than from scheduling
Unique build identifiersBuild IDs, UUIDs, embedded build numbersDerive from a hash of the inputs, or exclude from the comparison explicitly
Environment leakagetypicalHostname, user, locale affecting sort order or number formattingHermetic 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

simplifiedOur determinism is a consequence of the system being small: no threads, no filesystem, no clock, no allocator whose addresses are observable, no debug info and no build system. A production compiler has all of those and achieving the same property took the GCC, Clang and Debian communities years of individual bug fixes. The test demonstrates that the property is checkable; it does not demonstrate that it is easy.

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.

The determinism assertion in scripts/compilers-sim.test.ts
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.
  • diffoscope explains differences rather than reporting them, which is what makes them fixable.
  • reprotest varies 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-map so 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 -j1 and 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: reprotest builds 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: ar needs deterministic mode (D) to omit timestamps and uids, and many toolchains now default to it — ar tv lib.a shows 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.ts compiles 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.

Two builds of the same source obviously produce the same binary.
They usually do not, until someone does the work. Timestamps and build paths alone are enough to make almost every unprepared project fail on the first attempt.
Reproducibility is a security feature and nothing else.
It is also what makes build caching sound and what removes the artifact as a variable when debugging. Most teams get more day-to-day value from the second and third.
It only requires the compiler to be deterministic.
The build system, the archiver, the packaging step and every generator have to be too. The compiler is usually not the hardest part.

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.

typicalSOURCE_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.
simplifiedOur AtlasLang implementation is deterministic because it has nothing to be non-deterministic with: no threads, no clock, no filesystem, no observable allocator addresses, no debug information and no build system. The test in 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.
implementationWhich sources of non-determinism a given compiler has already eliminated is a per-project, per-version fact. GCC and Clang have both fixed many address-ordered iteration bugs over the years and neither claims the work is finished; the Reproducible Builds status pages are the current record for any particular package.

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

Domains that do not exist yet
  • DevOps / Production Engineering — Build caching, artifact promotion and rebuild-on-demand pipelines
    A 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 signal
    Reproducibility 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.