Skip to content

A cross toolchain is never rebuilt over another one's sysroot: the sysroot records which toolchain built it - #37

Merged
DevomB merged 3 commits into
mainfrom
pass1-clean-headers
Sep 20, 2026
Merged

DevomB merged 3 commits into
mainfrom
pass1-clean-headers

Conversation

@DevomB

@DevomB DevomB commented Sep 20, 2026

Copy link
Copy Markdown
Owner

Fixes the stage 02 failure on main (run 35492373903, 794c481).

What failed. gcc pass 2's libstdc++: cannot convert '<brace-enclosed initializer list>' to 'unsigned int' at __GTHREAD_COND_INIT.

Why. It was not a from-scratch rebuild. The workflow's last restore key is a bare work02-, so it restored the previous run's sysroot and rebuilt the toolchain on top of it. gcc's fixincludes keeps a private copy of headers it "fixes", and pthread.h is one. Pass 1 found the old glibc's pthread.h and kept it; glibc was then rebuilt from the release branch, which changes struct __pthread_cond_s and its initializer together; the pass-1 compiler read its old private copy beside the new struct.

Evidence in the logs. gcc-pass1.log: "Fixing directory .../sysroot/usr/include", "Applying pthread_incomplete_struct_argument to pthread.h". Job log: "Cache restored from key: work02-...-35482105602".

Fix (second commit, the general rule). The sysroot records which toolchain built it: a hash of the binutils, gcc, glibc, kernel-header, mpfr, gmp and mpc versions and glibc's patch set. A populated sysroot with a missing or different marker has every stamp archived and is removed before stage 01's first step. It refuses if anything is mounted under the sysroot and removes with --one-file-system. The first commit's narrower header removal is replaced by it.

Why the workflow's fallback key stays. build/config/** is in that cache key. Without the fallback, every kernel fragment edit would rebuild the toolchain and base system from nothing. With this rule the fallback is safe.

Checked outside the build against six fake sysroots: fresh, same toolchain, gcc moved, a cached tree with no marker, kernel headers without glibc, and something mounted under it (refused, nothing deleted).

Not proven until a Distro run gets through stage 02.

…me out of a cache

Run 35492373903 (794c481) failed in stage 02, in gcc pass 2's libstdc++:
"cannot convert '<brace-enclosed initializer list>' to 'unsigned int'" at
__GTHREAD_COND_INIT. It was called a from-scratch rebuild and was not one.
The workflow's last restore key is a bare "work02-", so with every input
changed it still restored the previous run's sysroot, and the toolchain was
rebuilt on top of it.

gcc's fixincludes keeps a private copy of every header it "fixes", and
pthread.h is one (pthread_incomplete_struct_argument). Pass 1 normally runs
before any C library exists and finds nothing. Here it found the old
sysroot's glibc 2.40 headers and kept that pthread.h. glibc was then rebuilt
from the release branch, which changes struct __pthread_cond_s and
PTHREAD_COND_INITIALIZER together. The pass-1 compiler went on reading its
private, old pthread.h beside the sysroot's new thread-shared-types.h, and
the old initializer does not fit the new struct. The build log says all of
it: "Fixing directory .../sysroot/usr/include", "Applying
pthread_incomplete_struct_argument to pthread.h" in gcc-pass1.log, and the
cache line "restored from key: work02-...-35482105602".

So pass 1 removes usr/include when a C library's headers are there. The step
only runs when the toolchain is being rebuilt, and linux-headers (which
already clears the directory) and glibc put the headers back right after it.
An incremental rebuild is then the same as a from-scratch one in the respect
that broke, and a later glibc header change cannot meet a stale private copy,
because pass 1 no longer has one.

Neither the new gcc nor the glibc branch was at fault by itself; the order
they were built in, over an old tree, was.
…sroot records which toolchain built it

The first version of this fix removed usr/include before gcc pass 1 when
glibc's headers were there. Two readers' worth of looking said that was one
header too narrow. A tree with the kernel's headers and no glibc would keep
them, and fixincludes freezes private copies of dozens of linux/*.h too. And
the headers are only what was shown to matter: a restored sysroot also keeps
the old compiler's directory under tools and superseded libraries beside
their replacements.

So the rule is the general one and sits before the first step. The sysroot
holds a marker: a hash of the binutils, gcc, glibc, kernel-header, mpfr, gmp
and mpc versions and of glibc's patch set. A populated sysroot whose marker
is missing or different was built by another toolchain: every stamp is
archived, the sysroot is removed, and stage 01 starts from layout. It refuses
if anything is mounted under the sysroot, and removes with
--one-file-system, because stage 03 binds /dev, /proc and this repository in
there. A sysroot with no headers gets this toolchain's marker.

Stamps could not have caught this: the header that went stale is not an
input of any step. It is the third time something outside a step's inputs
has decided a result (the microcode beside the kernel tree, the release keys
outside the cache), and the first where the cure is a record of what the
tree was built by.

The workflow keeps its bare work02- restore key on purpose. build/config/**
is in that cache's key, so without the fallback every kernel fragment edit
would rebuild the toolchain and the base system from nothing; with this rule
the fallback is safe, because a restored tree from another toolchain clears
itself.

Exercised outside the build against six fake sysroots: fresh (marker
written), same toolchain (left alone), gcc moved (cleared, two stamps
archived), a cached tree with no marker (cleared), kernel headers and no
glibc (cleared), something mounted under it (refused, nothing deleted).
@DevomB DevomB changed the title gcc pass 1 never sees a C library's headers, even when the sysroot came out of a cache A cross toolchain is never rebuilt over another one's sysroot: the sysroot records which toolchain built it Sep 20, 2026
…cord lives with the stamps

Review of the rule found that its removal could reach the source tree. Stage
03 binds this repository into the sysroot. A bind mount from the same
filesystem has the sysroot's own device number, so rm --one-file-system
walks into it: that guard does not exist for exactly the mount that matters.
That left `grep " ${LFS}/" /proc/mounts` as the only thing between a stale
record and an rm -rf of the checkout, and it is blind three ways, each
failing open: /proc/mounts writes a space as \040 (this checkout is called
"Linux Distro"), it names the resolved path where $LFS may hold a symlink,
and grep reads $LFS as a pattern, so a bracket in it misses or errors.

The test now resolves the path, escapes it the way /proc/mounts does, and
compares each mountpoint as a fixed string from its start; awk gets the
string through ENVIRON, because -v would turn \040 back into a space. A
mounts file that cannot be read counts as mounted. And the question is asked
a second way that does not depend on parsing anything: the four directories
stage 03 binds the repository, the sources and the work tree onto are empty
when nothing is mounted, so any of them holding a file is a refusal.

The record moves from the top of the sysroot to the stamps directory. Stage
06's root image is a copy of the sysroot with an exclude list that did not
name it, so every verified root would have carried a build-host file at /.
The cache restores the stamps and the sysroot together, a lost stamps
directory reads as no record and clears, and the archive step moves the old
record aside with the other stamps. The identity also hashes gcc and
binutils patch sets, for the day the tree has any.

Exercised outside the build: the six sysroots again, the sixth now with a
file standing in for the repository (refused, and the file survived), and
the mount test against a mounts file holding a plain path, a path with a
space, one with brackets, one reached through a symlink, and a sibling
whose name merely starts the same; all eight answers right, and an
unreadable mounts file answers mounted.
@DevomB
DevomB merged commit 76f954a into main Sep 20, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant