Skip to content

BP-BOOTSTRAP, building the compiler with itself

Status: partial Applies to: GCC 16.2.0 (tag releases/gcc-16.2.0) Target-dependent: no Generated sections: 2 Last verified: 2026-09-05 against releases/gcc-16.2.0

This document specifies the three stage bootstrap: what a stage is, what order the stages run in, and what the comparison between the last two of them proves. It is the top level of the tree driving gcc/ several times, so BP-BUILD is what each stage does and this is what makes there be more than one.

1. Purpose and scope

A bootstrap builds GCC three times. Stage one is built by whatever compiler was already on the machine. Stage two is built by stage one. Stage three is built by stage two, from the same sources as stage two, and the two results are required to be identical object file for object file.

That last requirement is the whole point, and it is worth stating what it does and does not prove.

It proves that stage one's code generation, applied to the compiler's own source, produced a compiler that generates the same code as stage two does. If stage one miscompiled the compiler, stage two is a broken compiler, and a broken compiler compiling the same source usually produces different object files from a working one. The comparison catches that without anybody having written a test for it.

It does not prove the compiler is correct. Three stages of a compiler that has always miscompiled the same construct in the same way agree with each other perfectly. What the comparison detects is a difference between two compilers built from one source, which is a specific and narrow thing, and it is still the strongest self check in the project by a wide margin, because its input is the largest and least forgiving C++ program the developers have to hand.

The reason it works at all is that the compiler is a fixed point. Source that has been compiled by a correct compiler and then used to compile itself should produce the same output as the original. Stage two and stage three are that fixed point tested at one iteration.

What this document does not cover: what a single stage does, which is BP-BUILD; the test suite, which is BP-TESTSUITE; and what any individual target library contains. Nine of the target libraries, libgcc and libstdc++-v3 among them, are marked bootstrap=true and so are rebuilt in every stage and compared along with everything else; the other seventeen are built once against the final compiler.

The unit of this document is the top level of the tree. Nothing in gcc/ knows a bootstrap is happening.

2. Data structures

The bootstrap has almost no data. Its state is directories, two files with a stage name in them, and a set of makefile variables per stage. Everything below is read out of the pinned tree rather than typed, because all four are lists GCC keeps in machine readable form and none of them appears as a table in GCC's documentation.

2.1 The stages

Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.

There are 9 bootstrap stages declared, 4 of them numbered and 5 named. A default make bootstrap runs three of them and compares the last two. The rest exist for the profiled and the auto profiled bootstraps, which build extra compilers whose only job is to produce the profile the final one is optimised with.

Stage Built by Compared against make target Flags, against the default
stage1 the compiler already on the machine nothing none CFLAGS = @stage1_cflags@, TFLAGS += -fno-checking
stage2 stage1 nothing make bootstrap2 CFLAGS += -fno-checking, TFLAGS += -fno-checking
stage3 stage2 stage2 make bootstrap CFLAGS += -fchecking=1, TFLAGS += -fchecking=1
stage4 stage3 stage3 make bootstrap4 the default
stageprofile stage1 nothing none CFLAGS = $(STAGE2_CFLAGS) -fprofile-generate, TFLAGS = $(STAGE2_TFLAGS)
stagetrain stageprofile nothing none CFLAGS = $(filter-out -fchecking=1,$(STAGE3_CFLAGS)), TFLAGS = $(filter-out -fchecking=1,$(STAGE3_TFLAGS))
stagefeedback stagetrain nothing make profiledbootstrap CFLAGS = $(STAGE4_CFLAGS) -fprofile-use -fprofile-reproducible=parallel-runs, TFLAGS = $(STAGE4_TFLAGS)
stageautoprofile stage1 nothing none CFLAGS = $(filter-out -gtoggle,$(STAGE2_CFLAGS)) -g, TFLAGS = $(STAGE2_TFLAGS)
stageautofeedback stageautoprofile nothing make autoprofiledbootstrap CFLAGS = $(STAGE3_CFLAGS), TFLAGS = $(STAGE3_TFLAGS)

Only 2 of the 9 stages compare anything: stage3, stage4. A stage with no comparison is a compiler that got built and believed. The Built by column is the whole argument: every stage after the first is compiled by the stage above it, so stage3 and stage2 are the same source built by two different compilers, and the compiler that built stage2 is the one being tested.

2.2 What is rebuilt in each stage

Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.

25 of the 54 host modules are built again in every stage. bootstrap=true in Makefile.def is what puts one inside the loop, and it is the whole of the difference:

bfd, opcodes, binutils, fixincludes, gas, gcc, gmp, mpfr, mpc, isl, gold, gettext, ld, libbacktrace, libcpp, libcody, libdecnumber, libiberty, libiberty-linker-plugin, libiconv, zlib, lto-plugin, libctf, libsframe, libgrust.

The other 29 are built once, against the compiler the bootstrap finished with. That is the line between a bug that a stage comparison can catch and a bug it cannot. libiberty is linked into the compiler and is in the list, so a miscompilation of it shows up as a differing object file. gdb is not, so it could be miscompiled by every stage and the bootstrap would still say it succeeded.

2.3 The comparison and what it forgives

Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.

A stage comparison walks every *.o under stage3-*, finds the matching file under stage2-*, and requires them to be equal after the first 16 bytes. 6 patterns are forgiven on every target:

Pattern Why it is allowed to differ
gcc/cc*-checksum$(objext) The checksum of the compiler's own object files, which is a hash of stage two's binaries in stage two and of stage three's in stage three. It is supposed to differ, and it is what makes every other object file's PCH validation work.
gcc/ada/*tools/* Ada's tools are built by the Ada compiler of the stage that is building, and stage two's gnat1 is not stage three's.
gcc/m2/gm2-compiler-boot/M2Version* M2Version.def says its implementation module is generated, and one of the four things it returns is GetGM2Date, documented there as the date of the build. A date in an object file cannot survive being built twice.
gcc/m2/gm2-compiler-boot/SYSTEM* SYSTEM.def is generated by m2/tools-src/makeSystem, which runs the current stage's own gm2 to find out what the target's types are. What it writes is therefore a product of the compiler doing the building.
gcc/m2/gm2version* The version number again, from the front end side. There is a gcc/m2/gm2version.h in the tree and no matching source file, so the implementation is another thing generated during the build.
gcc/cobol/parse$(objext) GCC's sources do not say, and this table will not guess. parse.cc is generated from parse.y by bison, and something in it differs between two stages of an otherwise identical build.

One more, *libgomp*$(objext), is added by the powerpc*-ibm-aix* arm of a case on the target, so on every other target it is not in the list at all.

Anything else that differs goes into .bad_compare, and the build stops with Bootstrap comparison failure! and the list. The 16 bytes are skipped because some object formats put a timestamp there. configure works out at build time whether the local cmp can skip bytes itself, and falls back to a pair of tail -c +17 calls into temporary files when it cannot.

2.4 The build configurations

Generated by bpc build from the pinned GCC tree. Do not edit inside the markers, edit the generator.

19 build configurations ship in config/, and --with-build-config takes a space separated list of them. A configuration is a makefile fragment included after the defaults, so it can add to a stage's flags, replace them, or replace the comparison itself:

--with-build-config= What it changes What GCC says it is for
bootstrap-O1 BOOT_CFLAGS, so every stage the fragment carries no comment
bootstrap-O3 BOOT_CFLAGS, so every stage the fragment carries no comment
bootstrap-Og BOOT_CFLAGS, so every stage the fragment carries no comment
bootstrap-asan stage2, stage3 This option enables -fsanitize=address for stage2 and stage3
bootstrap-cet stage2, stage3, stage4 This option enables -fcf-protection for stage2, stage3 and stage4
bootstrap-debug-big stage2, stage3, the comparison itself This BUILD_CONFIG option is a bit like bootstrap-debug-lean, but it trades space for speed: instead of recompiling programs during stage3, it generates dumps during stage2 and stage3, saving them all until the final compare
bootstrap-debug-ckovw FORCE_COMPARE_DEBUG, POSTSTAGE1_HOST_EXPORTS, BASE_TARGET_EXPORTS This BUILD_CONFIG option is to be used along with bootstrap-debug-lean and bootstrap-debug-lib in a full bootstrap, to check that all host and target files are built with -fcompare-debug
bootstrap-debug-lean stage2, stage3, the comparison itself This BUILD_CONFIG option is a bit like bootstrap-debug, but rather than comparing stripped object files, it compares compiler internal state during stage3
bootstrap-debug-lib stage1, stage2, stage3, the comparison itself This BUILD_CONFIG option tests that target libraries built during stage3 would have generated the same executable code if they were compiled with -g0
bootstrap-debug stage2, the comparison itself This BUILD_CONFIG option builds checks that toggling debug information generation doesn't affect the generated object code
bootstrap-hwasan stage2, stage3 This option enables -fsanitize=hwaddress for stage2 and stage3
bootstrap-lto-lean stage3, stagefeedback, stagetrain, the comparison itself This option enables LTO for stage4 and LTO for generators in stage3 with profiledbootstrap
bootstrap-lto-locality-cpp-template stage2, stage3, stagefeedback, stageprofile, stagetrain, the comparison itself This option enables LTO and locality partitioning for stage2 and stage3 in slim mode
bootstrap-lto-locality stage2, stage3, stagefeedback, stageprofile, stagetrain, the comparison itself This option enables LTO and locality partitioning for stage2 and stage3 in slim mode
bootstrap-lto-noplugin stage2, stage3, stagefeedback, stageprofile, stagetrain, the comparison itself This option enables LTO for stage2 and stage3 on hosts without linker plugin support
bootstrap-lto stage2, stage3, stagefeedback, stageprofile, stagetrain, the comparison itself This option enables LTO for stage2 and stage3 in slim mode
bootstrap-native BOOT_CFLAGS, so every stage the fragment carries no comment
bootstrap-time BOOT_CFLAGS, so every stage the fragment carries no comment
bootstrap-ubsan stage2, stage3 This option enables -fsanitize=undefined for stage2 and stage3

bootstrap-debug is the one to know. It is on by default on any target where it works, and it turns the stage comparison into a check that generating debug information does not change the code generated, which is a class of bug that nothing else in GCC looks for.

2.5 The records this document uses

record Stage
    id             symbol                 1, 2, 3, 4, profile, train, feedback, ...
    previous       symbol or nothing      the stage whose compiler builds this one
    compares       symbol or nothing      the stage this one is compared against
    target         symbol or nothing      the make target that stops here
    cflags         sequence of symbol     what this stage's own flags are
    lean_of        symbol or nothing      the stage this one may delete when lean

record StageTree
    current    symbol or nothing     contents of stage_current, or nothing
    last       symbol or nothing     contents of stage_last
    live       symbol or nothing     which stage's directories are named gcc
    lean       set of symbol         stages whose directories have been deleted

record Comparison
    left       symbol                the earlier stage
    right      symbol                the later stage
    skip       number                bytes ignored at the front of every file
    forgiven   sequence of symbol    shell patterns allowed to differ
    bad        sequence of symbol    what did differ and was not forgiven

StageTree.live is the field that surprises people. At any moment during a bootstrap exactly one stage's build directory is called gcc, and the others are called stage1-gcc, stage2-gcc and so on. Section 3.2 is about why.

3. Algorithms

3.1 Deciding whether to bootstrap at all

function bootstrap_wanted (asked: symbol, host: Triple, target: Triple) -> boolean or error(message)
    complexity: O(1)

    have_compiler = exists("gcc/configure")

    if asked == "no"
        return false
    if asked == "default"
        return have_compiler and host == build and target == build
    if asked == "yes" and not have_compiler
        return error("cannot bootstrap without a compiler")
    if asked == "yes" and not (host == build and target == build)
        warn("trying to bootstrap a cross compiler")
    return true

The case statement is at configure.ac:1549@releases/gcc-16.2.0. Two things follow from it that catch people out.

A native build bootstraps unless you say otherwise. --disable-bootstrap is the flag most people actually want and do not know to pass, and it is the difference between a twenty minute build and an hour.

A cross build does not bootstrap, and asking for one is a warning rather than an error. It cannot compare anything useful: stage two would have to run on the target to build stage three.

3.2 Unpacking and packing away a stage

function start_stage (s: Stage, tree: StageTree) -> StageTree
    complexity: O(m) in the number of bootstrap modules

    if tree.current is not nothing and tree.current != s.id
        end_stage(tree.current, tree)

    write("stage_current", s.id)
    write("stage_last", s.id)

    for each m in bootstrap_modules
        rename("stage" + s.id + "-" + m, m)
        if s.previous is not nothing and s.previous not in tree.lean
            rename("stage" + s.previous + "-" + m, "prev-" + m)

    tree.live = s.id
    return tree

function end_stage (s: Stage, tree: StageTree) -> StageTree
    complexity: O(m) in the number of bootstrap modules

    for each m in bootstrap_modules
        rename(m, "stage" + s.id + "-" + m)
        if s.previous is not nothing and s.previous not in tree.lean
            rename("prev-" + m, "stage" + s.previous + "-" + m)

    delete("stage_current")
    tree.live = nothing
    return tree

Between the two, the stage's own build runs: make all-stageN, with the compiler taken from prev-gcc and the flags taken from s.cflags. That part is BP-BUILD in full and is not repeated here.

The renaming is the part worth reading twice. The directory a stage builds in is always called gcc, and the previous stage's is always called prev-gcc, and the stage numbered names exist only while that stage is not the one being built. The start and end rules that do it are at Makefile.tpl:1767@releases/gcc-16.2.0 and Makefile.tpl:1784@releases/gcc-16.2.0.

The reason is the comparison. Debug information records the directory a file was compiled in. If stage two built in stage2-gcc and stage three built in stage3-gcc, every object file with debug information in it would differ, in a string, and the comparison would fail on every file. GCC's own comment at Makefile.tpl:1751@releases/gcc-16.2.0 says exactly this. The renaming makes both stages compile in a directory with the same name, so the recorded path is identical and only real differences remain.

stage_current exists while a stage is unpacked and is deleted when it is packed away again. stage_last is not deleted, and is how a stopped bootstrap knows which stage to put back. The unstage variable at Makefile.tpl:1733@releases/gcc-16.2.0 is what any non bootstrap target runs first, and it runs stage_last-start unless a stage is already unpacked. That is why make all-gdb on a tree where a bootstrap has stopped finds a directory called gcc at all: it holds the last stage that was built, and gdb gets built against that compiler rather than against nothing.

3.3 The comparison

function compare (c: Comparison) -> nothing or error(message)
    complexity: O(total size of the object files), one pass each

    if exists("stage" + c.left + "-lean")
        report("Cannot compare object files as stage " + c.left + " was deleted.")
        return nothing

    c.bad = []
    delete(".bad_compare")
    for each file in every "*.o" under "stage" + c.right + "-*", and extra_compare
        f1 = "stage" + c.left + "-" + file
        f2 = "stage" + c.right + "-" + file
        if not exists(f1)
            continue
        if equal_after(f1, f2, skip: c.skip)
            continue
        if matches_any(file, c.forgiven)
            report("warning: " + file + " differs")
        else
            append(c.bad, file)

    if c.bad is not empty
        return error("Bootstrap comparison failure!" + c.bad)
    return nothing

The rule is at Makefile.tpl:1824@releases/gcc-16.2.0. Five details of it decide what a reader sees when it fails.

The list of files comes from a find over the later stage's directories for anything ending in the object suffix, plus whatever the stage's extra-compare names. Nothing else in the tree is compared: not the executables, not the libraries, not the generated sources.

A file present in the later stage and absent from the earlier one is skipped rather than reported. That is what makes the comparison survive a stage that legitimately builds more than the one before it.

A forgiven file prints warning: ... differs and does not fail. Those warnings are in every successful bootstrap log, and a reader who greps for differs finds them and concludes the build failed when it did not.

The comparison happens after the stage that is being compared has finished, as the last step of stage3-bubble at Makefile.tpl:1815@releases/gcc-16.2.0. So the failure arrives at the end of a build that has already taken most of an hour.

The skip is 16 bytes, and configure decides at build time how to skip them: cmp --ignore-initial=16, or cmp f1 f2 16 16, or a pair of tail -c +17 calls into temporary files if neither form works. The probe is at config/acx.m4:474@releases/gcc-16.2.0.

3.4 The whole bootstrap

function bootstrap (final: Stage) -> nothing or error(message)
    complexity: O(k * n), k stages over n translation units

    tree = StageTree with nothing current, nothing last, no lean
    for each s in stages up to final
        if "stage" + s.id + "-lean" exists or "stage" + s.previous + "-lean" exists
            report("Skipping rebuild of stage" + s.id)
        else
            start_stage(s, tree)
            if lean_requested and s.lean_of is not nothing
                delete every directory of stage s.lean_of
                write("stage" + s.lean_of + "-lean", timestamp)
            make("all-stage" + s.id)
            end_stage(s, tree)

        if s.compares is not nothing
            compare(Comparison(left: s.compares, right: s.id, skip: 16,
                               forgiven: exclusions, bad: []))
            if lean_requested
                delete every directory of stage s.compares
                write("stage" + s.compares + "-lean", timestamp)
    write("stage_final", "stage" + final.id)

Each stage's rule depends on the previous stage's rule, which is what makes make bootstrap a single dependency chain rather than a script. GCC calls it bubbling, at Makefile.tpl:1800@releases/gcc-16.2.0, because a fix made at the bottom rises through the stages: stage3-bubble depends on stage2-bubble, which rebuilds stage two without reconfiguring it, and then stage three on top.

LEAN at Makefile.tpl:1745@releases/gcc-16.2.0 is false by default. make bootstrap-lean sets it, and each stage then deletes the stage two before it as soon as its directories have been renamed out of the way, and the comparison deletes the earlier of the two stages it has finished comparing. That is the trade a machine with less disk than patience makes: a full bootstrap tree is between six and twelve gigabytes, and a lean one is closer to three, and the lean one cannot be compared afterwards because the files are gone. The Cannot compare message in 3.3 is that case.

4. Invariants

I1. Stage two and stage three are compiled from identical sources with identical flags, differing only in which compiler compiles them. Established by: both stages using the same srcdir, and STAGE2_CFLAGS and STAGE3_CFLAGS differing only in -fno-checking against -fchecking=1, which controls the compiler's checking of its own IR while compiling, not the code it emits. Checked by: nothing directly. May be broken by: a build configuration that sets different flags for the two stages, which several in section 2.4 deliberately do, and which is why bootstrap-lto compares differently from a plain bootstrap and still compares.

I2. At most one stage's build directory is called gcc at any moment, and stage_current exists and names that stage exactly when one is. Established by: the start and end rules in 3.2. Checked by: nothing. May be broken by: interrupting a build between the rename and the write, or by a make -j that runs two stage rules at once, which the dependency chain prevents and nothing else does.

I3. The path recorded in an object file's debug information is the same in stage two and stage three. Established by: the renaming in 3.2. Checked by: the comparison, which fails loudly if it is violated. May be broken by: anything that puts an absolute path into an object file that is not the build directory, which __FILE__ in a source file compiled from an absolute path would do.

I4. Every object file compared exists in both stages, or is skipped. Established by: the test ! -f $$f1 guard in the compare rule. Checked by: nothing. May be broken by: nobody. It is a deliberate weakening: a file that exists only in stage three is never compared, and nothing reports that it was not.

I5. A file matching an entry in compare_exclusions never fails a bootstrap. Established by: the case arm in the compare rule and the value at configure.ac:4405@releases/gcc-16.2.0. Checked by: nothing. May be broken by: adding a pattern, which is a one line change with no test in front of it. Every entry is a hole in the only end to end self check GCC has, and the list is six patterns long for that reason.

I6. The compiler that builds stage two and later is the previous stage's xgcc, invoked from prev-gcc with -B pointing at it. Established by: POSTSTAGE1_HOST_EXPORTS at Makefile.tpl:279@releases/gcc-16.2.0. Checked by: nothing. May be broken by: a CC on the make command line, which overrides it and produces a bootstrap that silently used the system compiler for every stage.

I7. Stage one is built with -g and no optimisation flag of its own. Established by: stage1_cflags="-g" at configure.ac:4344@releases/gcc-16.2.0 and STAGE1_CFLAGS at Makefile.tpl:548@releases/gcc-16.2.0. Checked by: nothing. May be broken by: --with-stage1-ldflags and friends, which are meant to be used. This is why stage one takes a quarter of the bootstrap's wall clock and produces the slowest of the three compilers, and why BOOT_CFLAGS rather than CFLAGS is what changes the optimisation of the stages that matter.

5. Observable behaviour

make at the top level of a native configured tree runs a bootstrap, because all depends on bootstrap when --enable-bootstrap is on, and it is on by default. This is the single most common surprise in this document: a reader who configured without thinking about it and typed make is building the compiler three times and does not know.

The log announces each stage as a line reading Configuring stage 2 in ./gcc and the comparison as Comparing stages 2 and 3. A successful comparison prints Comparison successful. and nothing else. A failed one prints Bootstrap comparison failure! followed by one line per differing file, and exits non zero.

Wall clock, on this project's GitHub hosted amd64 runners with a cold cache, which is the slower of the two architectures every time:

What Minutes How known
The boot configuration, a full make bootstrap 240 estimated
The rel configuration, one stage, for comparison 22 measured

The bootstrap number is an estimate and this section is partial for that reason. Issue #6 is the gate: the boot job green four times with a recorded wall clock. Both numbers are from containers/matrix.toml, which says which of its rows are measured.

A bootstrap that fails its comparison leaves both stage trees on disk and a file called .bad_compare listing what differed. The two object files can then be disassembled and diffed, which is the only way to find out what actually happened, and contrib/compare-debug is the script the debug oriented build configurations use to do a smarter version of the same thing.

make bootstrap2 stops after stage two and compares nothing. It is the fastest way to find out whether stage one can build a working compiler at all, and it is what to run when the interesting question is "does this patch build" rather than "is this patch correct".

6. Edge cases and error paths

A cross build cannot bootstrap and says so as a warning. --enable-bootstrap on a cross configuration prints trying to bootstrap a cross compiler and proceeds. What it produces is stage one built by the system compiler, and stage two built by a stage one that emits code for the target and therefore cannot build anything that runs on the host. The failure is a link error deep in stage two, not a configure error.

The default is to bootstrap, and the default is the expensive one. Nothing in the output of configure says a bootstrap is about to happen in a way a first time reader will notice.

--disable-bootstrap and make bootstrap together do something. The bootstrap targets are conditional on gcc-bootstrap, so on a tree configured without it, make bootstrap is an unknown target rather than a bootstrap. This is a good failure and is worth knowing, because the opposite would be worse.

A lean bootstrap cannot be compared afterwards. make bootstrap-lean deletes stage two once stage three is built, so rerunning the comparison prints Cannot compare object files as stage 2 was deleted. and exits successfully. A script that treats a zero exit from make compare as proof the stages matched is wrong on exactly this case.

Warnings saying a file differs are normal. Every successful bootstrap prints one for each forgiven pattern that actually differed, usually the checksum objects. The distinction between warning: x differs and x differs in the log is the distinction between success and failure, and the two lines are one word apart.

Interrupting a bootstrap leaves the tree with a directory called gcc that is one of the stages. stage_last records which, and stage_current records whether it is currently unpacked. The next make of any non bootstrap target runs unstage first, which unpacks stage_last if nothing is unpacked already, so ordinary targets work against the last stage that got built. The next make of a bootstrap target picks up where it stopped, because the stage directories are what carries the progress and there is no other state.

make -j does not parallelise the stages, and cannot. Stage two needs stage one's compiler. The parallelism is inside a stage. A machine with many cores spends the bootstrap at low occupancy during the link steps and there is nothing to be done about it from here.

Stage one is built by a compiler nobody chose. It is whatever cc and c++ resolve to, and its version determines whether stage one compiles at all. GCC requires C++14 of the bootstrap compiler, and when that compiler is g++ it forces -std=c++14 on stage one at configure.ac:1583@releases/gcc-16.2.0, specifically so that a developer with a recent g++ finds out immediately that they used a C++17 feature rather than after somebody else's build fails.

A comparison failure is not always a compiler bug. Non deterministic code generation, an uninitialised value read during compilation, and a hash table iterated in address order all produce it. So does a genuinely miscompiled stage two. The failure does not distinguish them, and finding out which is the work.

--with-build-config values compose and are not checked. It takes a space separated list, each name becomes an included fragment, and a name with no fragment is silently ignored rather than refused. --with-build-config=bootstrap-lto bootstrap-O3 is two fragments. --with-build-config=bootstrap-lt0 is none of them and no message.

7. Interactions

Each stage is one execution of everything in BP-BUILD, so the generator programs in BP-BUILD section 2.1 are compiled and run once per stage. A bootstrap runs genrecog three times and produces three identical insn-recog.cc files, which is the clearest single illustration of BP-BUILD invariant I2.

The comparison is the strongest available check on BP-BUILD I2, that a generated file depends on nothing but its inputs, and BP-BUILD section 8 says so. If a generator embedded a timestamp, the stage comparison would fail on every object compiled from its output.

--enable-checking interacts with the stages rather than applying to them uniformly. Stage one gets --enable-checking=yes,types by default even when the build as a whole asked for release, at configure.ac:4368@releases/gcc-16.2.0, because a slow stage one is affordable and a miscompiled stage two is not. An LTO bootstrap takes the arm above it instead and drops gc from that list, for PR62077. BP-BUILD section 2.3 has the categories.

bootstrap-debug, the default build configuration where it works, turns the comparison into a check that -g does not change generated code. That is a check on BP-FINAL and on every pass that consults debug flags, and it is the reason -gtoggle exists as an option at all.

The plugin ABI in BP-PLUGIN is unaffected by bootstrapping, because a plugin loads into the installed compiler and the installed compiler is the last stage.

Globals, named honestly because section 3 passed them as arguments: stage_current, stage_last, stage_final and the stageN-lean marker files are the bootstrap's entire persistent state, and all of them are files in the build directory rather than variables. BUILD_CONFIG is a makefile variable set by configure and read by the include line that pulls in the fragments in section 2.4. LEAN is a makefile variable overridden on the command line by the -lean targets.

8. Conformance

The bootstrap is itself a conformance test and has none of its own. There is no test that checks the bootstrap machinery, and a bug in the renaming in 3.2 would show up as a comparison failure that nobody could explain.

What exists:

The comparison in 3.3 is run by every bootstrap, which is what the GCC project's own regression testers do continuously across dozens of targets. A stage comparison failure on any of them is treated as release blocking.

This project's boot container configuration runs a full make bootstrap in CI. It is the slowest thing in the matrix by a factor of four and is on a schedule rather than on every push.

Restating section 4 as assertions an implementation could run:

I1 is checkable by diffing the flags two stages were invoked with, which the log records. I3 is checkable by running strings over a pair of compared objects and looking for a stage numbered path. I5 is checkable by counting the entries and requiring the count to be justified, which is what bpc coverage does for other inventories and does not do for this one. I6 is checkable by make bootstrap CC=false and observing whether stage two builds, which it should not.

I2, I4 and I7 have no check. I4 in particular is a deliberate hole and is stated as one.

9. Port notes

The header says this document is not target dependent, and that is nearly true. The stages, the renaming, the bubbling and the comparison are the same everywhere. What varies is small and worth naming.

What Where it is decided Range across targets
Whether bootstrapping is on native or cross, in configure.ac on for native, off for cross
Extra forgiven patterns the case "$target" at configure.ac:4410@releases/gcc-16.2.0 none, except one more on AIX
Whether bootstrap-debug works whether the target's debug output is stable on by default where it works
How the 16 bytes are skipped a run time probe of the local cmp three implementations

What is forced and what is not.

Comparing two stages is forced, in the sense that it is the only self check available that uses a program large enough to matter and requires no oracle. Any compiler that can compile itself should do this, and most do not.

Three stages rather than two is forced. Two stages compare nothing: stage one was built by a different compiler and would differ from stage two for legitimate reasons. The comparison needs two compilers built from the same source by the same rules, which first exists at stage three.

The directory renaming is not forced and is a symptom. It exists because debug information records absolute paths and the comparison is over whole object files. A build system that compiled with relative paths, or a comparison that understood object file structure, would not need it. contrib/compare-debug is the second of those, written and then used only by the debug oriented configurations rather than by default.

The stamp files, the -lean markers and stage_current are not forced. They are make's lack of any way to hold state, worked around with files. A reimplementation with a real build graph should keep the stage semantics and none of the marker files.

The exclusion list is forced in shape and should be shorter in practice. Three of its six entries are one front end, Modula-2, and two of those three are it recording its own version into an object file, which a build system could arrange not to do. The checksum entry cannot be avoided: an object that contains a hash of the other objects necessarily differs when they do.