toolchain: a bootstrap finishes what it reassembles, and a sysroot is whole or made again - #558
Conversation
… whole or made again Three changes with one root: bootstrap recreates the primary's stage2/bin without the cargo link, and everything downstream of that trusted the directory to have it. - The primary bootstraps when compiler::source (the git content of rust/compiler) differs from the record rust/build/toyos-compiler, the same function a linked worktree already compares against. The mtime stamp target/stamps/compiler.stamp and the "record which compiler" step are gone. - Both acts that run bootstrap in the primary (the toolchain and the hosted rustc) finish what it reassembled - stage2's cargo, the hosted sysroot's host target - inside the same exclusive hold of the global lock. A separate step that needed the lock again queued behind every sysroot build that took it shared in between; one step remains, for a bootstrap that was stopped before it finished, and on every other build it decides nothing and takes no lock. - A sysroot provisions its own cargo when it is published instead of cloning whatever stage2/bin held, is refused before its SOURCES is written unless it is whole, and counts as finished only if toolchain_defect finds nothing. One found with SOURCES and without its cargo is rebuilt under the key's exclusive lock, replacing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's #573 (compiler identity keying, the lockfile Restore in x_build) and #541 (clang from our own LLVM, provisioned into every stage2) restructured the files this branch fixes. Resolved by keeping main's structure and putting the branch's three fixes back onto it: - The primary bootstraps when compiler::primary_is_current says its compiler/ (and, since main, its LLVM commit) is not what the record names. main had added the record comparison beside the mtime stamp; the stamp is what still moved on mtimes alone, so it goes, and src/stamps.rs with it: nothing else reads it now that toyos-cc is gone. primary_is asks source() before reading the record, so an LLVM edit no commit holds is still refused before any build, as main's decision did. - Whole now includes clang: toolchain_defect ends with clang::defect, so a sysroot without its C toolchain is not finished either, and publish refuses a clone that is not whole before fill runs its libc builds. build_c joins libc::build in the fill. - complete() provisions clang as well as cargo and the host target, inside reassemble's hold: main's separate "give the toyos toolchain the clang of its LLVM" act would otherwise queue behind sysroot builds exactly as the cargo act did. An installed toolchain keeps main's two steps, since its clang is the artifact's and there is no LLVM beside it to provision from. - The no-wait test no longer races a thread against a 5 s timeout: it asserts the predicate complete_toolchain decides by is false once reassemble returns. - src/CLAUDE.md keeps main's text, with "never written again once whole". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Its exit condition holds on this branch: publish provisions the sysroot's own cargo and refuses a clone that is not whole before SOURCES, and finished() treats one found with SOURCES and without its cargo as unfinished, so it is rebuilt (a_sysroot_is_whole_or_it_is_made_again, red under m2 and m2b). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Review, round 1, at 8692732CI: BLOCKER
NOTE
REMOVE
SEND BACK |
…cisions tested - publish no longer places a cargo of its own. A compiler that is not whole is refused before any std is built for it, naming its stage2 and, for the primary's, `cargo run -- --build-only` there; nothing is published. The check is on `compiler.stage2` at publish's top, and build_std runs inside `fill`, so the one check is both the early refusal and the refusal of the clone. - buildlock::keyed_made is the use-or-make loop sysroot::held and compiler::choose each carried, and it refuses a make that leaves its product not whole by the defect instead of making it again. sysroot's `finished` becomes `unfinished`, the reason. - toolchain::bootstrap is ensure's decision as a pure function, tested over all eight inputs. - rebuild_compiler removes the primary's compiler record before bootstrap and writes it after reassemble, so a stopped bootstrap is run again by the primary and refused by name in linked worktrees (compiler::forget). - clang::missing is deleted; callers ask clang::defect. - The fix-3 test gives the hosted rustc its lib/rustlib, so complete's host target step is covered. - Deleted: src/CLAUDE.md's "and never written again once whole", sysroot.rs's rewritten header prose, compiler.rs's false "bootstraps exactly when" clause, and publish's cargo doc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er than hangs it With both publish's refusal and keyed_made's refusal deleted, the fresh arm made its sysroot again forever; each make now asserts how many there may have been so far. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t reads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 2, at c385c0cCI: Round 1
BLOCKER
NOTE
REMOVE
SEND BACK |
…he wholeness gap ensure inlined keyed_made with its own || unfinished(&dir) closure, and the tests called a separate, textually identical copy of that call — so the review's mutation of ensure's closure alone left every test green, since no test's code path ran through it. held(root, key, dir, make) is now the one production function both call, and the mutation now turns a_sysroot_is_whole_or_it_is_made_again red at sysroot.rs:990. keyed_building/keyed_using are private (keyed_made is their only caller outside buildlock's own tests); complete_toolchain, an abstraction with one caller, is inlined into ensure; toolchain.rs's rewritten "Asked first..." comment is deleted, since main's original text described a moved/dir_changed mechanism this branch already removed. Files issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md: record reads compiler/ as it stands after the bootstrap it records, not before, so a mid-bootstrap edit is recorded as current for a stage2 that does not contain it — true of main too, so tracked rather than fixed here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review, round 3, at f23fea7CI: Round 2
Growth: +502/−241 overall. This round is production +18/−21, tests −5, and the issue +28. BLOCKERNone. NOTENone. REMOVE
LAND AFTER NAMED CHANGES |
Final text round for PR #558 (ToyOSOrg/ToyOS): delete the comment explaining `complete`, the sentence from the issue tracker about main's ordering, the Gates preamble about merging origin/main, and clauses from the PR body and table that reviewed earlier rounds identified as redundant or already explained. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bootstrap recreates the primary's
stage2without itscargolink and without the clangsrc/clang.rsprovisions. Three decisions downstream of that trusted the directory to have them:target/stamps/compiler.stamp), not on a changed compiler;SOURCES, so one cloned whilestage2had no cargo stayed broken for good;cargo runqueued for the global lock behind other worktrees' sysroot builds after a bootstrap, to run the separate provisioning steps.This closes
issues/build/a-sysroot-cloned-during-a-toolchain-rebuild-never-gets-its-cargo.md, which it deletes: its exit condition is decision 2.What changed, per decision
The primary bootstraps only when its compiler's content changed, when asked to, or when rustup has no
toyostoolchain. The decision istoolchain::bootstrap(force_rebuild, current, toolchain_exists), a pure function.currentiscompiler::primary_is_current(rust/), which comparescompiler::source(rust/)with the recordrust/build/toyos-compiler.sourceis the git tree ofcompiler/, plus the working diff, plus untracked files, plus thesrc/llvm-projectcommit (clang from our own LLVM builds ToyOS's C, and toyos-cc is gone #541). A missing record means bootstrap.rebuild_compilerremoves the record (compiler::forget) before the bootstrap and writes it afterreassemble. A stopped bootstrap is therefore run again by the primary, and a linked worktree getschoose's named refusal (run cargo run -- --build-only there) before any std build.movedcomparison beside it (primary_is_currentsubsumes it), the "record which compiler the toolchain is" act, andsrc/stamps.rs, which nothing reads now that toyos-cc is gone.choose(a linked worktree against the primary) uses the sameprimary_is.identity.rs: every worktree's build system reads the record file. A format change would make each of those worktrees build a compiler of its own. Git's tree hash is already content identity.recordreadssource(rust_dir)after the bootstrap it records, not before — acompiler/edit made mid-bootstrap is folded into the record for astage2that does not contain it. Main has the same order, so this is filed rather than fixed here:issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md.A toolchain directory is whole or it is not used.
toolchain_defectis the one definition of whole: no narrated binary,rust-lld, and the C toolchain (clang::defect;clang::missingis gone).assert_toolchain_is_honestrefuses by it.buildlock::keyed_madeis the use-or-make loop thatsysroot::ensureandcompiler::chooseeach carried inline. It makes the product whiledefectsays it is not whole. It refuses amakethat leaves the product not whole by that defect, and does not runmakeagain.keyed_buildingandkeyed_usingare private now:keyed_madeis their only caller outsidebuildlock's own tests, so a second use-or-make protocol cannot be written around them.unfinishedis "noSOURCES" ortoolchain_defect. A sysroot that hasSOURCESbut is not whole is made again, all of it, under the key's exclusive lock. That includes one whose only defect is a danglingbin/cargolink: this is rare, and a sysroot has no repair path.publishchecks the compiler'sstage2beforefill, andfillnow runsbuild_std. A stopped bootstrap'sstage2, without cargo or without clang, is refused before any std is built. The refusal names thatstage2and, for the primary's,cargo run -- --build-onlyin the primary checkout. Nothing is published.publishplaces no cargo of its own:completeandcompiler::build_in_forkprovision it in everystage2.The owner's
cargo rundoes not wait when no global change is needed. Both acts that run bootstrap (the toolchain and the hosted rustc) go throughreassemble.reassemblerunscomplete()inside the same exclusive hold.complete()coversstage2's cargo, its clang, and the hosted sysroot's host target. Main's separate cargo and clang acts are gone.Owner::Installed) keeps main's two steps, host target and cargo. Its clang is the artifact's own, and no LLVM sits beside it to provision from.ensureand its tests share one productionheld.held(root, key, dir, make)is now the one function both call.src/CLAUDE.md: main's "and never written again" is deleted from the sysroot bullet.Gates
cargo test -p toyos-build --libcargo test --workspace --exclude toyos-buildcargo run -- --clippyBuild-system code only; no guest gate was run.
Negative controls
Each arm is a checked patch (
git apply --check). For each one the mutated tree built (cargo test -p toyos-build --lib --no-run, exit 0), the named tests ran red (exit 101), andgit apply -Rleft the tree clean. The line numbers are from the unmutated file.publish's refusal of a compiler that is not whole (the review's "deleteassert_toolchain_is_honest(&partial)": the check is now onstage2, beforefill)a_sysroot_is_whole_or_it_is_made_againat sysroot.rs:984 anda_sysroot_that_cannot_be_made_whole_is_made_once_and_refusedat :1029.keyed_maderefusedsysroot fresh was made, and is not whole: … is missing cargo, which names neither thestage2nor the remedykeyed_made's refusal aftermakea_sysroot_that_cannot_be_made_whole_is_made_once_and_refusedat :1043: the secondmakepanickeda sysroot that was not whole was made again(left 2, right 1)a sysroot that was not whole was made again: make 2(:984), and theonceassert (:1028)if force_rebuild || !current→if force_rebuildthe_primary_bootstraps_when_asked_stale_or_missing:force_rebuild false, current false, toolchain_exists true, leftNonecrate::compiler::forget(rust_dir);a_stopped_bootstrap_leaves_no_record:a stopped bootstrap left the record of the compiler before itcomplete's host-target stepa_bootstrap_leaves_nothing_to_wait_on_a_sysroot_build_for:a bootstrap let its exclusive hold go with a global step left, …primary_is_currentbecomes main's!(stamps::dir_changed(compiler/, stamp) || moved), andrecordwrites the stamp where main wrote it (after bootstrap)only_the_compiler_s_content_makes_the_primary_bootstrap:every file of compiler/ was rewritten with its own bytes, and the primary would bootstrapreassembleno longer completes, so completion runs under a hold of its own, as on maina_bootstrap_leaves_nothing_to_wait_on_a_sysroot_build_for:a bootstrap let its exclusive hold go with a global step left, …complete/incompletewithout clanga bootstrap let its exclusive hold go with stage2 not wholeheld's|| unfinished(dir)→|| (!dir.join(SOURCES).is_file()).then(String::new)a_sysroot_is_whole_or_it_is_made_againat sysroot.rs:990:assertion left == right failed: a sysroot without its cargo was trusted because it has SOURCESNo test depends on timing or on a dropped descriptor being closed. Every acquisition in
keyed_madeblocks, and none of the tests asserts a lock free.Independent oracle: a recorded real failure, the owner's pasted wait on
give the toyos toolchain its own cargo. The host's own timestamps agree with it: thestage2cargo link appeared at 22:08:04, and sysroot5dc157f7fac727bewas made at 22:03:59 without cargo. m3 is that base behaviour, and its red names the queue.🤖 Generated with Claude Code