From b53535a5b52ff7198508d4248825ca04589b7325 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 22:41:45 +0200 Subject: [PATCH 1/9] toolchain: a bootstrap finishes what it reassembles, and a sysroot is 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 --- src/compiler.rs | 82 ++++++++++++++++--- src/sysroot.rs | 142 ++++++++++++++++++++++++--------- src/toolchain.rs | 200 ++++++++++++++++++++++++++++++----------------- 3 files changed, 304 insertions(+), 120 deletions(-) diff --git a/src/compiler.rs b/src/compiler.rs index 4293edf567..1ea0bc8a3d 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -3,9 +3,11 @@ //! //! **Every worktree builds with the compiler its own fork checkout names.** The //! primary's `stage2` is built from what the primary's `rust/compiler/` holds, -//! and [`record`] writes which that is. A linked worktree whose fork checkout -//! holds the same `compiler/` ([`source`]) compiles with that one. One whose -//! `compiler/` differs — a new target spec, a codegen change — gets its own: +//! and [`record`] writes which that is; the primary bootstraps exactly when its +//! `compiler/` is no longer that ([`primary_is_current`]). A linked worktree +//! whose fork checkout holds the same `compiler/` ([`source`]) compiles with +//! that one. One whose `compiler/` differs — a new target spec, a codegen +//! change — gets its own: //! built by bootstrap in its own fork checkout, under that checkout's //! `build/toyos-compiler/`, and placed at `rust/build/compilers//`, where //! the key ([`key`]) is the identity (`src/identity.rs`) of the checkout's @@ -138,8 +140,7 @@ pub fn source(checkout: &Path) -> String { } /// Record which compiler the primary's `stage2` is. The primary calls this -/// after a toolchain build, and when the record is missing — its compiler stamp -/// has just said `stage2` is built from what its `rust/` holds. +/// after a toolchain build, and nowhere else. pub fn record(rust_dir: &Path) { let at = primary_record(rust_dir); let want = source(rust_dir); @@ -148,6 +149,26 @@ pub fn record(rust_dir: &Path) { } } +/// Whether the primary's `stage2` is the compiler `checkout`'s `compiler/` +/// names — `Err` when nothing records which compiler that is. +/// +/// **The source's content, never its files' times**: a checkout that rewrites a +/// file with the bytes it had is no new compiler, and a bootstrap it set off +/// recreated `stage2/bin` for nothing. +fn primary_is(rust_dir: &Path, checkout: &Path) -> Result { + Ok(fs::read_to_string(primary_record(rust_dir))?.trim() == source(checkout)) +} + +/// Whether the primary's `stage2` is built from what its own `rust/compiler/` +/// holds: false until a bootstrap has finished and [`record`]ed it. +pub fn primary_is_current(rust_dir: &Path) -> bool { + match primary_is(rust_dir, rust_dir) { + Ok(current) => current, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => false, + Err(e) => panic!("read {}: {e}", primary_record(rust_dir).display()), + } +} + /// The key of the compiler `fork`'s sources name: their content, so committing /// what was built as local changes names the same compiler. pub fn key(fork: &Path) -> String { @@ -185,16 +206,15 @@ fn choose(root: &Path, rust_dir: &Path, fork: &Path, build: impl Fn(&Path) -> Pa if fork == rust_dir { return Compiler::primary(rust_dir); } - let record = primary_record(rust_dir); - let built_from = fs::read_to_string(&record).unwrap_or_else(|e| { + let names_primary = primary_is(rust_dir, fork).unwrap_or_else(|e| { panic!( "{} cannot be read ({e}), so nothing says which compiler the primary's stage2 is, \ and no worktree can know whether it names that one.\n\ The primary checkout writes it: run `cargo run -- --build-only` there once.", - record.display(), + primary_record(rust_dir).display(), ) }); - if built_from.trim() == source(fork) { + if names_primary { let _ = fs::remove_file(&recorded); return Compiler::primary(rust_dir); } @@ -531,6 +551,50 @@ mod tests { assert_eq!(builds.get(), 0, "a missing record built a compiler"); } + /// **The primary bootstraps when its `compiler/` holds other content, and + /// never because its files' times moved**: every file rewritten with its own + /// bytes is the compiler just recorded. + #[test] + fn only_the_compiler_s_content_makes_the_primary_bootstrap() { + let scratch = TempDir::new("compiler-current"); + let (_primary, rust_dir, _) = estate(&scratch); + assert!(primary_is_current(&rust_dir), "the compiler just recorded is not current"); + + let mut files = Vec::new(); + let mut stack = vec![rust_dir.join("compiler")]; + while let Some(at) = stack.pop() { + for entry in fs::read_dir(&at).unwrap().flatten() { + let path = entry.path(); + if path.is_dir() { stack.push(path) } else { files.push(path) } + } + } + let later = std::time::SystemTime::now() + std::time::Duration::from_secs(3600); + for file in &files { + fs::write(file, fs::read(file).unwrap()).unwrap(); + fs::File::options().write(true).open(file).unwrap().set_modified(later).unwrap(); + assert_eq!(fs::metadata(file).unwrap().modified().unwrap(), later); + } + assert!(!files.is_empty()); + assert!( + primary_is_current(&rust_dir), + "every file of compiler/ was rewritten with its own bytes, and the primary would bootstrap" + ); + + let spec = rust_dir.join("compiler/rustc_target/src/lib.rs"); + let held = fs::read(&spec).unwrap(); + write(&spec, "pub fn targets() { riscv() }\n"); + assert!(!primary_is_current(&rust_dir), "a change to compiler/ kept the old stage2"); + fs::write(&spec, held).unwrap(); + assert!(primary_is_current(&rust_dir), "compiler/ put back is not the compiler recorded"); + + write(&rust_dir.join("compiler/rustc_target/src/new_target.rs"), "pub fn t() {}\n"); + assert!(!primary_is_current(&rust_dir), "an untracked file in compiler/ kept the old stage2"); + fs::remove_file(rust_dir.join("compiler/rustc_target/src/new_target.rs")).unwrap(); + + fs::remove_file(primary_record(&rust_dir)).unwrap(); + assert!(!primary_is_current(&rust_dir), "a stage2 nothing recorded was taken for current"); + } + /// Every source a compiler is built from moves its key: LLVM by commit, /// the tools by content. #[test] diff --git a/src/sysroot.rs b/src/sysroot.rs index 5fbc008d8a..61d4a11f45 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -7,11 +7,12 @@ //! ([`SYSROOT_SOURCES`]), the std fork's `library/` and `src/bootstrap/` in the //! checkout that builds it, and the compiler that builds it. `rust/build/ //! sysroots//` is a whole toolchain — the compiler's files cloned from its -//! `stage2`, the guest targets' libraries built from this key's sources — and -//! nothing writes it after its [`SOURCES`] file exists. A build compiles against -//! the directory its own key names, so two worktrees with different ABIs or -//! different compilers never refuse or wait for each other, and main and every -//! branch matching it share one copy. +//! `stage2`, this machine's cargo, the guest targets' libraries built from this +//! key's sources — and nothing writes it after its [`SOURCES`] file exists; one +//! that has it and is not whole is not [`finished`], and is made again. A build +//! compiles against the directory its own key names, so two worktrees with +//! different ABIs or different compilers never refuse or wait for each other, +//! and main and every branch matching it share one copy. //! //! **Each worktree builds std in its own fork checkout, and nothing but the //! primary's own sync moves the primary's.** The primary builds in its `rust/`; @@ -298,9 +299,11 @@ pub fn recorded_key(root: &Path) -> Option { fs::read_to_string(root.join(RECORD)).ok().map(|k| k.trim().to_string()) } -/// Whether `dir` is a finished sysroot. +/// Whether `dir` is a finished sysroot: its [`SOURCES`] written, and a whole +/// toolchain (`toolchain::toolchain_defect`). One that carries the first and not +/// the second is made again rather than trusted. fn finished(dir: &Path) -> bool { - dir.join(SOURCES).is_file() + dir.join(SOURCES).is_file() && toolchain::toolchain_defect(dir).is_none() } /// The sysroot this worktree's sources name, made if nobody has made it, and @@ -314,19 +317,24 @@ pub fn ensure(root: &Path, rust_dir: &Path, lock: &mut Held) -> Sysroot { fs::create_dir_all(record.parent().expect("a file under target/")).ok(); fs::write(&record, &key).unwrap_or_else(|e| panic!("write {}: {e}", record.display())); - let using = lock.without_shared(|| loop { - let using = buildlock::keyed_using(root, Keyed::Sysroot, &key); - if finished(&dir) { - break using; + let using = lock.without_shared(|| held(root, &key, &dir, || build(root, &compiler, &fork, &key, &dir))); + Sysroot { dir, primary_compiler: compiler.primary, _using: Some(using) } +} + +/// The sysroot `key` names at `dir`, held in use, [`finished`] — by `make`, +/// under the key's lock, if nobody has finished it. +fn held(root: &Path, key: &str, dir: &Path, make: impl Fn()) -> Guard { + loop { + let using = buildlock::keyed_using(root, Keyed::Sysroot, key); + if finished(dir) { + return using; } drop(using); - let _building = buildlock::keyed_building(root, Keyed::Sysroot, &key); - if !finished(&dir) { - build(root, &compiler, &fork, &key, &dir); + let _building = buildlock::keyed_building(root, Keyed::Sysroot, key); + if !finished(dir) { + make(); } - }); - toolchain::assert_toolchain_is_honest(&dir); - Sysroot { dir, primary_compiler: compiler.primary, _using: Some(using) } + } } /// Make the sysroot `key` names at `dir`, from `root`'s sources and the std fork @@ -340,32 +348,48 @@ fn build(root: &Path, compiler: &Compiler, fork: &Path, key: &str, dir: &Path) { eprintln!("Building sysroot {key}: std from {}, the compiler {}", fork.display(), compiler.stage2.display()); let built = build_std(root, compiler, fork); + publish(&compiler.stage2, dir, |partial| { + for target in GUEST_TARGETS { + place_std(&stamp(&built, target), &partial.join("lib/rustlib").join(target).join("lib")); + } + let libc_target = dir.with_extension("libc-target"); + for arch in Arch::ALL { + crate::libc::build(root, partial, &libc_target, arch); + } + let _ = fs::remove_dir_all(&libc_target); + + // The sources the key named are the ones built, or this is not that key's. + let again = self::key(root, compiler, fork); + assert!( + again == key, + "the sources moved while sysroot {key} was being built (they are now {again}); \ + nothing was kept, and the next build makes the one they name" + ); + format!("{key}\nfork {}\n{}\n", fork.display(), witness(root)) + }); +} + +/// Put at `dir` a whole toolchain: `stage2`'s files, this machine's cargo, and +/// what `fill` adds to them, then the [`SOURCES`] `fill` returns, last. A `dir` +/// already there is one [`finished`] refused, and it is replaced. +/// +/// **The cargo is this step's, never `stage2`'s**: bootstrap recreates +/// `stage2/bin` without one, and a clone carries whatever `bin/` held when it +/// was taken. +fn publish(stage2: &Path, dir: &Path, fill: impl FnOnce(&Path) -> String) { let partial = dir.with_extension("partial"); if partial.exists() { fs::remove_dir_all(&partial).unwrap_or_else(|e| panic!("remove {}: {e}", partial.display())); } - clone_tree(&compiler.stage2, &partial); - for target in GUEST_TARGETS { - place_std(&stamp(&built, target), &partial.join("lib/rustlib").join(target).join("lib")); + clone_tree(stage2, &partial); + toolchain::provision_toolchain_cargo(&partial); + let sources = fill(&partial); + toolchain::assert_toolchain_is_honest(&partial); + fs::write(partial.join(SOURCES), sources) + .unwrap_or_else(|e| panic!("write {}: {e}", partial.join(SOURCES).display())); + if dir.exists() { + fs::remove_dir_all(dir).unwrap_or_else(|e| panic!("remove {}: {e}", dir.display())); } - let libc_target = dir.with_extension("libc-target"); - for arch in Arch::ALL { - crate::libc::build(root, &partial, &libc_target, arch); - } - let _ = fs::remove_dir_all(&libc_target); - - // The sources the key named are the ones built, or this is not that key's. - let again = self::key(root, compiler, fork); - assert!( - again == key, - "the sources moved while sysroot {key} was being built (they are now {again}); \ - nothing was kept, and the next build makes the one they name" - ); - fs::write( - partial.join(SOURCES), - format!("{key}\nfork {}\n{}\n", fork.display(), witness(root)), - ) - .unwrap_or_else(|e| panic!("write {}: {e}", partial.join(SOURCES).display())); fs::rename(&partial, dir) .unwrap_or_else(|e| panic!("rename {} -> {}: {e}", partial.display(), dir.display())); } @@ -786,6 +810,48 @@ mod tests { assert!(message.contains(&c1) && message.contains(&c2), "{message}"); } + /// **A sysroot is whole, or it is made again**: one published from a + /// `stage2` whose `bin/` has no cargo — what bootstrap leaves until the + /// primary finishes it — has its cargo all the same, and one found with its + /// `SOURCES` and without its cargo, as `5dc157f7fac727be` was, is rebuilt + /// rather than trusted, once. + #[test] + fn a_sysroot_is_whole_or_it_is_made_again() { + let base = TempDir::new("whole"); + git(&base, &["init", "-q"]); + let stage2 = base.join("stage2"); + write(&stage2.join("bin/rustc"), "rustc"); + write(&toolchain::rust_lld(&stage2), "lld"); + let dir = sysroots_dir(&base.join("rust")).join("5dc157f7fac727be"); + let made = std::cell::Cell::new(0); + let make = |dir: &Path| { + made.set(made.get() + 1); + publish(&stage2, dir, |partial| { + write(&partial.join("lib/rustlib/x86_64-unknown-toyos/lib/libstd.rlib"), "std"); + "5dc157f7fac727be\n".to_string() + }) + }; + + let fresh = sysroots_dir(&base.join("rust")).join("fresh"); + drop(held(&base, "fresh", &fresh, || make(&fresh))); + assert_eq!( + toolchain::toolchain_defect(&fresh), + None, + "a sysroot was published without its cargo, cloned from a stage2 that had none" + ); + + clone_tree(&stage2, &dir); + write(&dir.join(SOURCES), "5dc157f7fac727be\n"); + let before = made.get(); + let using = held(&base, "5dc157f7fac727be", &dir, || make(&dir)); + assert_eq!(made.get(), before + 1, "a sysroot without its cargo was trusted because it has SOURCES"); + assert_eq!(toolchain::toolchain_defect(&dir), None); + assert!(dir.join("lib/rustlib/x86_64-unknown-toyos/lib/libstd.rlib").is_file()); + drop(using); + drop(held(&base, "5dc157f7fac727be", &dir, || make(&dir))); + assert_eq!(made.get(), before + 1, "a whole sysroot was made again"); + } + /// A key no registered worktree records goes, and so does a half-built one; /// a key a worktree records stays, and so does one somebody is using. #[test] diff --git a/src/toolchain.rs b/src/toolchain.rs index 524eb536b1..dfb11506e9 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -286,15 +286,6 @@ fn cargo_link_stale(stage2: &Path) -> bool { /// Put a `cargo` beside the toolchain's `rustc`. /// -/// **The one step that provisions it, and every path that can produce a -/// toolchain directory goes through it**: the primary's bootstrap and its -/// staleness rebuild (both upstream of [`ensure`]'s call), a linked worktree -/// adopting the shared one, and a runner that unpacked the published artifact. -/// The fix this replaces was made once, by hand, in a step the rebuild path does -/// not run — so the 2026-08-14 sysroot rebuild recreated `bin/` without it and -/// nothing noticed, and CI, which links its toolchain fresh from the artifact -/// every run, never had it at all. -/// /// **A symlink, and what survives the artifact round-trip is this step rather /// than the link.** `src/release.rs` excludes it from the tarball for the reason /// it excludes `lib/rustlib/`: it names a path only the publishing runner @@ -309,33 +300,84 @@ pub(crate) fn provision_toolchain_cargo(stage2: &Path) { }); } -/// Refuse a toolchain layout that would make rustup narrate, or that has no -/// linker for the guest targets. +/// Why the toolchain at `stage2` is not whole, if it is not: a binary rustup +/// would narrate a fallback for, or no linker for the guest targets. /// -/// Unconditional and after the step that provisions, because the defect being -/// gated is a provisioning step that silently stopped running: a check that only -/// runs when the step runs asserts nothing about the build that skipped it. -pub(crate) fn assert_toolchain_is_honest(stage2: &Path) { +/// The one definition of whole: [`assert_toolchain_is_honest`] refuses by it, +/// and a sysroot is finished only by it (`src/sysroot.rs`). +pub(crate) fn toolchain_defect(stage2: &Path) -> Option { let bin = stage2.join("bin"); let narrated = narrated_binaries(&bin); - assert!( - narrated.is_empty(), - "the toyos toolchain at {} is missing {}, so rustup answers for {} by falling back to \ - another toolchain and narrating it on every invocation.\n\ - provision_toolchain_cargo is the step that puts them there, and it did not.", - bin.display(), - narrated.join(" and "), - if narrated.len() == 1 { "it" } else { "them" }, - ); + if !narrated.is_empty() { + return Some(format!( + "the toyos toolchain at {} is missing {}, so rustup answers for {} by falling back to \ + another toolchain and narrating it on every invocation.\n\ + provision_toolchain_cargo is the step that puts them there, and it did not.", + bin.display(), + narrated.join(" and "), + if narrated.len() == 1 { "it" } else { "them" }, + )); + } // Every guest target names `rust-lld` and rustc looks for it here, so a // toolchain without it is refused here, by name, rather than at the first link. let lld = rust_lld(stage2); - assert!( - lld.is_file(), - "the toyos toolchain at {} carries no {}, the linker every guest target names: \ - bootstrap puts it there when `write_config` says `lld = true`, and it did not", - stage2.display(), - lld.display(), + (!lld.is_file()).then(|| { + format!( + "the toyos toolchain at {} carries no {}, the linker every guest target names: \ + bootstrap puts it there when `write_config` says `lld = true`, and it did not", + stage2.display(), + lld.display(), + ) + }) +} + +/// Refuse a toolchain that is not whole ([`toolchain_defect`]). +/// +/// Unconditional and after the step that provisions, because the defect being +/// gated is a provisioning step that silently stopped running: a check that only +/// runs when the step runs asserts nothing about the build that skipped it. +pub(crate) fn assert_toolchain_is_honest(stage2: &Path) { + if let Some(defect) = toolchain_defect(stage2) { + panic!("{defect}"); + } +} + +/// Whether the primary's toolchain lacks what bootstrap does not put there: +/// `stage2`'s cargo, or the host target in the hosted rustc's sysroot. +fn incomplete(rust_dir: &Path) -> bool { + cargo_link_stale(&stage2(rust_dir)) || host_target_missing(rust_dir) +} + +/// Give the primary's toolchain what [`incomplete`] finds missing. +fn complete(rust_dir: &Path) { + if cargo_link_stale(&stage2(rust_dir)) { + provision_toolchain_cargo(&stage2(rust_dir)); + } + if host_target_missing(rust_dir) { + link_host_target(rust_dir); + } +} + +/// Run `bootstrap` in the primary's `rust/`, then [`complete`] what it +/// reassembled. Called inside the act that holds the global lock exclusively. +/// +/// **In the same hold, because bootstrap recreates `stage2/bin` without its +/// cargo**: a completion under a hold of its own queues behind every sysroot +/// build that takes the lock shared in between, and those last minutes. +fn reassemble(rust_dir: &Path, bootstrap: impl FnOnce()) { + bootstrap(); + complete(rust_dir); +} + +/// Complete a toolchain whose bootstrap was stopped before [`reassemble`] +/// finished it. Every other build decides there is nothing to do, and takes no +/// lock. +fn complete_toolchain(lock: &mut buildlock::Held, rust_dir: &Path) { + lock.act_if( + Scope::Global, + "complete the toyos toolchain", + || incomplete(rust_dir).then_some(()), + |()| complete(rust_dir), ); } @@ -417,12 +459,14 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S Owner::Us => {} } - let compiler_stamp = stamps_dir.join("compiler.stamp"); let hosted_stamp = stamps_dir.join("hosted-rustc.stamp"); lock.act_if( Scope::Global, "build the rust toolchain", || { + if force_rebuild || !crate::compiler::primary_is_current(&rust_dir) { + return Some(Bootstrap { invalidate_hosted: true }); + } let toolchain_exists = Command::new("rustup") .args(["run", "toyos", "rustc", "--version"]) .stdout(std::process::Stdio::null()) @@ -430,32 +474,17 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S .status() .map(|s| s.success()) .unwrap_or(false); - if stamps::dir_changed(&rust_dir.join("compiler"), &compiler_stamp) || force_rebuild { - Some(Bootstrap { invalidate_hosted: true }) - } else if !toolchain_exists { - Some(Bootstrap { invalidate_hosted: false }) - } else { - None - } + (!toolchain_exists).then_some(Bootstrap { invalidate_hosted: false }) }, |kind| { eprintln!("Building full toolchain (this takes a while on first run)..."); - full_bootstrap(root, &rust_dir); - stamps::write_dir_stamp(&rust_dir.join("compiler"), &compiler_stamp); + reassemble(&rust_dir, || full_bootstrap(root, &rust_dir)); crate::compiler::record(&rust_dir); if kind.invalidate_hosted { let _ = fs::remove_file(&hosted_stamp); } }, ); - // The compiler stamp above has just said `stage2` is built from what `rust/` - // holds, so a missing record is written from it. - lock.act_if( - Scope::Global, - "record which compiler the toolchain is", - || (!rust_dir.join("build/toyos-compiler").exists()).then_some(()), - |()| crate::compiler::record(&rust_dir), - ); let hosted_rustc = rust_dir.join(format!("build/{}/stage2/bin/rustc", HOSTED_ARCH.userland())); lock.act_if( @@ -463,7 +492,7 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S "build the ToyOS-hosted rustc", || (!hosted_stamp.exists() || !hosted_rustc.exists()).then_some(()), |()| { - build_hosted_rustc(&rust_dir); + reassemble(&rust_dir, || build_hosted_rustc(&rust_dir)); assert!(hosted_rustc.exists(), "Failed to build hosted rustc"); fs::write(&hosted_stamp, "").unwrap(); }, @@ -483,26 +512,9 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S }, ); - // After both bootstrap steps above, because either of them recreates `bin/` - // and a fix that lives upstream of a rebuild is a fix that rots. Every - // sysroot clones this `bin/`, so it is where the cargo is provisioned. - lock.act_if( - Scope::Global, - "give the toyos toolchain its own cargo", - || cargo_link_stale(&stage2).then_some(()), - |()| provision_toolchain_cargo(&stage2), - ); + complete_toolchain(lock, &rust_dir); assert_toolchain_is_honest(&stage2); - // The hosted rustc's own sysroot needs the host target proc-macros compile - // against. - lock.act_if( - Scope::Global, - "add the host target to the ToyOS sysroot", - || host_target_missing(&rust_dir).then_some(()), - |()| link_host_target(&rust_dir), - ); - sysroot::ensure(root, &rust_dir, lock) } @@ -537,12 +549,7 @@ fn check_installed_toolchain(root: &Path, rust_dir: &Path, force_rebuild: bool) // This is CI's whole share of the cargo provisioning — it links its // toolchain fresh from the published artifact on every run, so nothing // upstream of the download can have put one there. - if host_target_missing(rust_dir) { - link_host_target(rust_dir); - } - if cargo_link_stale(&stage2) { - provision_toolchain_cargo(&stage2); - } + complete(rust_dir); assert_toolchain_is_honest(&stage2); let want = sysroot::witness(root); @@ -982,6 +989,53 @@ mod tests { assert_toolchain_is_honest(&stage2); } + /// **A bootstrap leaves the primary nothing that waits on another + /// worktree's sysroot build**: the act that recreates `stage2/bin` gives it + /// its cargo before the exclusive lock goes, so the step after it, run while + /// a sysroot build holds the lock shared, decides it has nothing to do. + #[test] + fn a_bootstrap_leaves_nothing_to_wait_on_a_sysroot_build_for() { + let root = TempDir::new("no-wait"); + let git = Command::new("git").args(["init", "-q"]).current_dir(&*root).status().unwrap(); + assert!(git.success()); + let rust_dir = root.join("rust"); + let bin = stage2(&rust_dir).join("bin"); + let lld = rust_lld(&stage2(&rust_dir)); + fs::create_dir_all(lld.parent().unwrap()).unwrap(); + fs::write(&lld, b"").unwrap(); + + let mut lock = buildlock::shared(&root, "the primary's build"); + lock.act_if(Scope::Global, "build the rust toolchain", || Some(()), |()| { + reassemble(&rust_dir, || { + // What bootstrap leaves: `bin/` made again, holding `rustc` alone. + let _ = fs::remove_dir_all(&bin); + fs::create_dir_all(&bin).unwrap(); + fs::write(bin.join("rustc"), b"").unwrap(); + }) + }); + let left = toolchain_defect(&stage2(&rust_dir)); + + let sysroot_build = buildlock::compiler_shared(&root, "building sysroot 5dc157f7fac727be"); + let (done, finished) = std::sync::mpsc::channel(); + let rest = rust_dir.clone(); + std::thread::spawn(move || { + complete_toolchain(&mut lock, &rest); + done.send(()).unwrap(); + }); + let waited = finished.recv_timeout(NO_WAIT); + drop(sysroot_build); + assert!( + waited.is_ok(), + "the primary's build queued for the global lock behind a sysroot build after a \ + bootstrap had finished: it needed a global change the bootstrap left undone" + ); + assert_eq!(left, None, "the bootstrap let the global lock go with stage2 not whole"); + } + + /// Longer than deciding from a symlink and two directories takes, and + /// shorter than any sysroot build holds the lock. + const NO_WAIT: std::time::Duration = std::time::Duration::from_secs(5); + /// The negative control is the defect itself: this is verbatim what cargo /// wrote for a worktree build before the override existed. #[test] From 29f54fa2848bf9d5e92c8ce30863e1865a7a8f90 Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 23:12:40 +0200 Subject: [PATCH 2/9] compiler: two doc comments say the invariant and stop Co-Authored-By: Claude Opus 5.5 --- src/compiler.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/compiler.rs b/src/compiler.rs index 1ea0bc8a3d..d8e377edb1 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -140,7 +140,7 @@ pub fn source(checkout: &Path) -> String { } /// Record which compiler the primary's `stage2` is. The primary calls this -/// after a toolchain build, and nowhere else. +/// after a toolchain build. pub fn record(rust_dir: &Path) { let at = primary_record(rust_dir); let want = source(rust_dir); @@ -153,8 +153,7 @@ pub fn record(rust_dir: &Path) { /// names — `Err` when nothing records which compiler that is. /// /// **The source's content, never its files' times**: a checkout that rewrites a -/// file with the bytes it had is no new compiler, and a bootstrap it set off -/// recreated `stage2/bin` for nothing. +/// file with the bytes it had is no new compiler. fn primary_is(rust_dir: &Path, checkout: &Path) -> Result { Ok(fs::read_to_string(primary_record(rust_dir))?.trim() == source(checkout)) } From eefb15d241d19568b0c2972d8453b1f949ac55ee Mon Sep 17 00:00:00 2001 From: japabu Date: Sun, 27 Sep 2026 23:25:18 +0200 Subject: [PATCH 3/9] src/CLAUDE.md: a sysroot is fixed once whole, not once it exists Co-Authored-By: Claude Opus 5.5 --- src/CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CLAUDE.md b/src/CLAUDE.md index 82b6c66c41..6afd33a7ed 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -19,7 +19,7 @@ Loads when you read a file under `src/` — the root cargo project, package name ## The host's locks and slots -- **Sysroots are content-addressed** (`src/sysroot.rs`): one per key — the identity (`src/identity.rs`, so a comment is no change) of `toyos-abi/src`, `toyos/src`, `userland/libc/src` and their manifests, the std fork's `library/` and `src/bootstrap/`, and the compiler — at `rust/build/sysroots//`, made by whichever worktree first needs it and never written again. Every build compiles against its own key's, so two worktrees with different ABIs never refuse or wait for each other; the only shared step is the primary's compiler, which a sysroot build reads under the global lock in shared mode. A new key costs one std build of the three guest targets; `--worktree remove` sweeps the keys no worktree records. +- **Sysroots are content-addressed** (`src/sysroot.rs`): one per key — the identity (`src/identity.rs`, so a comment is no change) of `toyos-abi/src`, `toyos/src`, `userland/libc/src` and their manifests, the std fork's `library/` and `src/bootstrap/`, and the compiler — at `rust/build/sysroots//`, made by whichever worktree first needs it, and never written again once whole. Every build compiles against its own key's, so two worktrees with different ABIs never refuse or wait for each other; the only shared step is the primary's compiler, which a sysroot build reads under the global lock in shared mode. A new key costs one std build of the three guest targets; `--worktree remove` sweeps the keys no worktree records. - **The std fork is built per worktree, and nothing but the primary's own sync moves the primary's `rust/`.** A linked worktree's `rust/` becomes, on its first build, a git worktree of the primary's fork repository at the commit its tree pins — that is where the fork is edited, committed and pinned. A worktree whose fork `compiler/` differs from the one the primary built builds its own compiler, keyed by that source and placed beside the primary's without touching it. If that checkout later falls behind the commit its tree pins (a merge moved the pin), the build moves the checkout to it itself, fetching from the primary's repository first if it holds the commit, unless the checkout has local changes, which it refuses to move out from under. - `src/buildlock.rs` serialises the stateful phases in two scopes: `Global` (the primary's compiler and the rustup link — one directory in `.git/`, shared by every worktree) and `Worktree` (the crate-target cleans, and this worktree's std build). Only `./x.py` typed by hand in `rust/` escapes it. - **Never kill a build that has taken the global lock** — the kill removes the shell wrappers, not the bootstrap, which inherits the file descriptor and runs on regardless; a toolchain rebuild interrupted or unobserved this way can leave `stage2/bin` without a `cargo`. From 8692732e33cf6014141fac790a39b8a8b3e0b8a1 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 20:41:02 +0200 Subject: [PATCH 4/9] issues: a sysroot without its cargo is made again, so its issue goes 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 Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j --- ...-toolchain-rebuild-never-gets-its-cargo.md | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 issues/build/a-sysroot-cloned-during-a-toolchain-rebuild-never-gets-its-cargo.md diff --git a/issues/build/a-sysroot-cloned-during-a-toolchain-rebuild-never-gets-its-cargo.md b/issues/build/a-sysroot-cloned-during-a-toolchain-rebuild-never-gets-its-cargo.md deleted file mode 100644 index 9594732d9f..0000000000 --- a/issues/build/a-sysroot-cloned-during-a-toolchain-rebuild-never-gets-its-cargo.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-27 ---- - -# A sysroot cloned during a toolchain rebuild never gets its cargo - -The primary checkout's `rust/build/aarch64-apple-darwin/stage2/bin` was -recreated at 22:00 on 2026-09-27 and, read while a `toyos-build --build-only` -ran in the primary, held `rustc` and `rustdoc` and no `cargo`; the `cargo` link -appeared at 22:08. At 22:03:59 a `cargo test --test toyos-build` in the -`wt/toyos-nokthread` worktree rebuilt std and published -`rust/build/sysroots/5dc157f7fac727be` cloned from that `bin/`, so it has -`rustc` and `rustdoc` and no `cargo`. The key is found again on every later -run and nothing re-provisions it: every harness run from that worktree since -panics at `tests/toyos.rs:2970` with `the toyos toolchain at -.../sysroots/5dc157f7fac727be/bin is missing cargo` on the C corpus, before any -test runs. - -**Evidence:** the two directory listings and the harness log above, read on -the dev host; not reproduced on purpose. - -**Exit condition:** a sysroot is never published without the provisioned -`cargo`, and a run that finds a published one without it provisions it or -refuses by name at the toolchain step rather than inside the corpus. From be5f500369553f792971298ac458cd9794c61222 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 23:02:21 +0200 Subject: [PATCH 5/9] Review round 1: one policy for a stage2 that is not whole, and the decisions 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 --- src/CLAUDE.md | 2 +- src/buildlock.rs | 27 +++++++ src/clang.rs | 11 +-- src/compiler.rs | 38 ++++++---- src/sysroot.rs | 185 +++++++++++++++++++++++++++++++---------------- src/toolchain.rs | 77 +++++++++++++++++--- 6 files changed, 242 insertions(+), 98 deletions(-) diff --git a/src/CLAUDE.md b/src/CLAUDE.md index d5785cb4d6..2d21ce98bf 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -19,7 +19,7 @@ Loads when you read a file under `src/` — the root cargo project, package name ## The host's locks -- **Sysroots are content-addressed** (`src/sysroot.rs`): one per key — the identity (`src/identity.rs`, so a comment is no change) of `toyos-abi/src`, `toyos/src`, `userland/libc/src` and their manifests, the std fork's `rust/library/` and `rust/src/bootstrap/`, and the compiler — at `rust/build/sysroots//`, made by whichever worktree first needs it, and never written again once whole. Every build compiles against its own key's, so two worktrees with different ABIs never refuse or wait for each other; the only shared step is the primary's compiler, which a sysroot build reads under the global lock in shared mode. A new key costs one std build of the three guest targets; `--worktree remove` sweeps the keys no worktree records. +- **Sysroots are content-addressed** (`src/sysroot.rs`): one per key — the identity (`src/identity.rs`, so a comment is no change) of `toyos-abi/src`, `toyos/src`, `userland/libc/src` and their manifests, the std fork's `rust/library/` and `rust/src/bootstrap/`, and the compiler — at `rust/build/sysroots//`, made by whichever worktree first needs it. Every build compiles against its own key's, so two worktrees with different ABIs never refuse or wait for each other; the only shared step is the primary's compiler, which a sysroot build reads under the global lock in shared mode. A new key costs one std build of the three guest targets; `--worktree remove` sweeps the keys no worktree records. - **The std fork is built per worktree, and nothing but the primary's own sync moves the primary's `rust/`.** A linked worktree's `rust/` becomes, on its first build, a git worktree of the primary's fork repository at the commit its tree pins — that is where the fork is edited, committed and pinned. A worktree whose fork `compiler/` differs from the one the primary built builds its own compiler, keyed by that source and placed beside the primary's without touching it. If that checkout later falls behind the commit its tree pins (a merge moved the pin), the build moves the checkout to it itself, fetching from the primary's repository first if it holds the commit, unless the checkout has local changes, which it refuses to move out from under. - `src/buildlock.rs` serialises the stateful phases in two scopes: `Global` (the primary's compiler and the rustup link — one directory in `.git/`, shared by every worktree) and `Worktree` (the crate-target cleans, and this worktree's std build). Only `./x.py` typed by hand in `rust/` escapes it. - **Never kill a build that has taken the global lock** — the kill removes the shell wrappers, not the bootstrap, which inherits the file descriptor and runs on regardless; a toolchain rebuild interrupted or unobserved this way can leave `stage2/bin` without a `cargo`. diff --git a/src/buildlock.rs b/src/buildlock.rs index 1e24eed06d..8383372c2d 100644 --- a/src/buildlock.rs +++ b/src/buildlock.rs @@ -286,6 +286,33 @@ pub fn keyed_using(root: &Path, kind: Keyed, key: &str) -> Guard { Guard { file, records_holder: false } } +/// What `key` names, held in use and whole: made by `make` under +/// [`keyed_building`] while `defect`, which says why it is not whole, says it is +/// not. A `make` that leaves it not whole is refused by that defect rather than +/// run again. +pub fn keyed_made( + root: &Path, + kind: Keyed, + key: &str, + defect: impl Fn() -> Option, + mut make: impl FnMut(), +) -> Guard { + loop { + let using = keyed_using(root, kind, key); + if defect().is_none() { + return using; + } + drop(using); + let _building = keyed_building(root, kind, key); + if defect().is_some() { + make(); + if let Some(defect) = defect() { + panic!("{} {key} was made, and is not whole: {defect}", kind.name()); + } + } + } +} + /// What `key` names, exclusively and only if nobody is making or using it: what /// a sweep holds while it removes one. pub fn keyed_idle(root: &Path, kind: Keyed, key: &str) -> Option { diff --git a/src/clang.rs b/src/clang.rs index dad48057f9..188d0938a3 100644 --- a/src/clang.rs +++ b/src/clang.rs @@ -115,11 +115,6 @@ fn absent(toolchain: &Path) -> Vec { gone } -/// Whether `toolchain` lacks any of the C toolchain. -pub(crate) fn missing(toolchain: &Path) -> bool { - !absent(toolchain).is_empty() -} - /// Why `toolchain` cannot compile C, if it cannot. pub(crate) fn defect(toolchain: &Path) -> Option { let gone = absent(toolchain); @@ -208,13 +203,13 @@ mod tests { write(&bin(&stage2).join("rust-lld"), "lld"); write(&bin(&stage2).join("llvm-ar"), "the archiver"); - assert!(missing(&stage2)); + assert!(defect(&stage2).is_some()); let refused = std::panic::catch_unwind(|| assert_present(&stage2)).expect_err("no clang, and not refused"); let said = refused.downcast_ref::().expect("a formatted refusal"); assert!(said.contains("clang") && said.contains("ld.lld") && said.contains("include"), "{said}"); provision(&stage2); - assert!(!missing(&stage2)); + assert_eq!(defect(&stage2), None); assert_eq!(fs::read_to_string(bin(&stage2).join("clang")).unwrap(), "the clang"); assert!(!fs::symlink_metadata(bin(&stage2).join("clang")).unwrap().file_type().is_symlink(), "clang is a copy, not the link"); assert_eq!(fs::read_link(bin(&stage2).join("ld.lld")).unwrap(), Path::new("rust-lld")); @@ -231,6 +226,6 @@ mod tests { assert_eq!(fs::read_to_string(resource_parent(&stage2).join("23/include/stddef.h")).unwrap(), "v23"); fs::remove_file(bin(&stage2).join("llvm-ar")).unwrap(); - assert!(missing(&stage2), "a missing llvm-ar went unnoticed"); + assert!(defect(&stage2).is_some(), "a missing llvm-ar went unnoticed"); } } diff --git a/src/compiler.rs b/src/compiler.rs index a23bee5274..e9a53c5c41 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -3,11 +3,9 @@ //! //! **Every worktree builds with the compiler its own fork checkout names.** The //! primary's `stage2` is built from what the primary's `rust/compiler/` holds, -//! and [`record`] writes which that is; the primary bootstraps exactly when its -//! `compiler/` is no longer that ([`primary_is_current`]). A linked worktree -//! whose fork checkout holds the same `compiler/` ([`source`]) compiles with -//! that one. One whose `compiler/` differs — a new target spec, a codegen -//! change — gets its own: +//! and [`record`] writes which that is. A linked worktree whose fork checkout +//! holds the same `compiler/` ([`source`]) compiles with that one. One whose +//! `compiler/` differs — a new target spec, a codegen change — gets its own: //! built by bootstrap in its own fork checkout, under that checkout's //! `build/toyos-compiler/`, and placed at `rust/build/compilers//`, where //! the key ([`key`]) is the identity (`src/identity.rs`) of the checkout's @@ -113,7 +111,7 @@ impl Compiler { } /// The primary's record of which `compiler/` its `stage2` was built from. -fn primary_record(rust_dir: &Path) -> PathBuf { +pub(crate) fn primary_record(rust_dir: &Path) -> PathBuf { rust_dir.join("build/toyos-compiler") } @@ -158,6 +156,16 @@ pub fn record(rust_dir: &Path) { } } +/// Remove the record of which compiler the primary's `stage2` is. The primary +/// calls this before a toolchain build. +pub fn forget(rust_dir: &Path) { + let at = primary_record(rust_dir); + match fs::remove_file(&at) { + Err(e) if e.kind() != std::io::ErrorKind::NotFound => panic!("remove {}: {e}", at.display()), + _ => {} + } +} + /// Whether the primary's `stage2` is the compiler `checkout`'s `compiler/` /// names — `Err` when nothing records which compiler that is. /// @@ -242,18 +250,16 @@ fn choose(root: &Path, rust_dir: &Path, fork: &Path, build: impl Fn(&Path) -> Pa fs::create_dir_all(recorded.parent().expect("a file under target/")).ok(); fs::write(&recorded, &key).unwrap_or_else(|e| panic!("write {}: {e}", recorded.display())); let mut placed = false; - let using = loop { - let using = buildlock::keyed_using(root, Keyed::Compiler, &key); - if dir.join(SOURCE).is_file() { - break using; - } - drop(using); - let _building = buildlock::keyed_building(root, Keyed::Compiler, &key); - if !dir.join(SOURCE).is_file() { + let using = buildlock::keyed_made( + root, + Keyed::Compiler, + &key, + || (!dir.join(SOURCE).is_file()).then(|| format!("{} carries no {SOURCE}", dir.display())), + || { place(root, fork, &key, &dir, &build); placed = true; - } - }; + }, + ); // A compiler edit loop places one per edit, and the one this replaced is // named by nobody now; the one in use is held, so the sweep leaves it. if placed { diff --git a/src/sysroot.rs b/src/sysroot.rs index b4c49bb38e..2fdd91143f 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -7,9 +7,7 @@ //! ([`SYSROOT_SOURCES`]), the std fork's `library/` and `src/bootstrap/` in the //! checkout that builds it, and the compiler that builds it. `rust/build/ //! sysroots//` is a whole toolchain — the compiler's files cloned from its -//! `stage2`, this machine's cargo, the guest targets' libraries built from this -//! key's sources — and nothing writes it after its [`SOURCES`] file exists; one -//! that has it and is not whole is not [`finished`], and is made again. A build +//! `stage2`, the guest targets' libraries built from this key's sources. A build //! compiles against the directory its own key names, so two worktrees with //! different ABIs or different compilers never refuse or wait for each other, //! and main and every branch matching it share one copy. @@ -301,11 +299,16 @@ pub fn recorded_key(root: &Path) -> Option { fs::read_to_string(root.join(RECORD)).ok().map(|k| k.trim().to_string()) } -/// Whether `dir` is a finished sysroot: its [`SOURCES`] written, and a whole -/// toolchain (`toolchain::toolchain_defect`). One that carries the first and not -/// the second is made again rather than trusted. -fn finished(dir: &Path) -> bool { - dir.join(SOURCES).is_file() && toolchain::toolchain_defect(dir).is_none() +/// Why `dir` is not a finished sysroot, if it is not: no [`SOURCES`], or not a +/// whole toolchain (`toolchain::toolchain_defect`). One found with the first and +/// not the second is made again rather than trusted — all of it even when only +/// its `bin/cargo` link dangles, because that is rare and a sysroot has no +/// repair path. +fn unfinished(dir: &Path) -> Option { + if !dir.join(SOURCES).is_file() { + return Some(format!("{} carries no {SOURCES}", dir.display())); + } + toolchain::toolchain_defect(dir) } /// The sysroot this worktree's sources name, made if nobody has made it, and @@ -319,26 +322,14 @@ pub fn ensure(root: &Path, rust_dir: &Path, lock: &mut Held) -> Sysroot { fs::create_dir_all(record.parent().expect("a file under target/")).ok(); fs::write(&record, &key).unwrap_or_else(|e| panic!("write {}: {e}", record.display())); - let using = lock.without_shared(|| held(root, &key, &dir, || build(root, &compiler, &fork, &key, &dir))); + let using = lock.without_shared(|| { + buildlock::keyed_made(root, Keyed::Sysroot, &key, || unfinished(&dir), || { + build(root, &compiler, &fork, &key, &dir) + }) + }); Sysroot { dir, primary_compiler: compiler.primary, _using: Some(using) } } -/// The sysroot `key` names at `dir`, held in use, [`finished`] — by `make`, -/// under the key's lock, if nobody has finished it. -fn held(root: &Path, key: &str, dir: &Path, make: impl Fn()) -> Guard { - loop { - let using = buildlock::keyed_using(root, Keyed::Sysroot, key); - if finished(dir) { - return using; - } - drop(using); - let _building = buildlock::keyed_building(root, Keyed::Sysroot, key); - if !finished(dir) { - make(); - } - } -} - /// Make the sysroot `key` names at `dir`, from `root`'s sources and the std fork /// at `fork`, with `compiler`. The caller holds the key's lock. fn build(root: &Path, compiler: &Compiler, fork: &Path, key: &str, dir: &Path) { @@ -349,8 +340,8 @@ fn build(root: &Path, compiler: &Compiler, fork: &Path, key: &str, dir: &Path) { let _compiler = compiler.primary.then(|| buildlock::compiler_shared(root, &what)); eprintln!("Building sysroot {key}: std from {}, the compiler {}", fork.display(), compiler.stage2.display()); - let built = build_std(root, compiler, fork); - publish(&compiler.stage2, dir, |partial| { + publish(compiler, dir, |partial| { + let built = build_std(root, compiler, fork); for target in GUEST_TARGETS { place_std(&stamp(&built, target), &partial.join("lib/rustlib").join(target).join("lib")); } @@ -372,21 +363,25 @@ fn build(root: &Path, compiler: &Compiler, fork: &Path, key: &str, dir: &Path) { }); } -/// Put at `dir` a whole toolchain: `stage2`'s files, this machine's cargo, and -/// what `fill` adds to them, then the [`SOURCES`] `fill` returns, last. A `dir` -/// already there is one [`finished`] refused, and it is replaced. -/// -/// **The cargo is this step's, never `stage2`'s**: bootstrap recreates -/// `stage2/bin` without one, and a clone carries whatever `bin/` held when it -/// was taken. -fn publish(stage2: &Path, dir: &Path, fill: impl FnOnce(&Path) -> String) { +/// Put at `dir` a whole toolchain: `compiler`'s files and what `fill` adds to +/// them, then the [`SOURCES`] `fill` returns, last. A `dir` already there is one +/// [`unfinished`] refused, and it is replaced. A compiler that is not whole is +/// refused before `fill` runs, and nothing is published. +fn publish(compiler: &Compiler, dir: &Path, fill: impl FnOnce(&Path) -> String) { + if let Some(defect) = toolchain::toolchain_defect(&compiler.stage2) { + let fix = if compiler.primary { + "\nA bootstrap in the primary checkout was stopped before it finished: \ + `cargo run -- --build-only` there completes it." + } else { + "" + }; + panic!("no sysroot is made from {}, and no std was built for one: {defect}{fix}", compiler.stage2.display()); + } let partial = dir.with_extension("partial"); if partial.exists() { fs::remove_dir_all(&partial).unwrap_or_else(|e| panic!("remove {}: {e}", partial.display())); } - clone_tree(stage2, &partial); - toolchain::provision_toolchain_cargo(&partial); - toolchain::assert_toolchain_is_honest(&partial); + clone_tree(&compiler.stage2, &partial); let sources = fill(&partial); fs::write(partial.join(SOURCES), sources) .unwrap_or_else(|e| panic!("write {}: {e}", partial.join(SOURCES).display())); @@ -936,51 +931,115 @@ mod tests { assert!(message.contains(&c1) && message.contains(&c2), "{message}"); } - /// **A sysroot is whole, or it is made again**: one published from a - /// `stage2` whose `bin/` has no cargo — what bootstrap leaves until the - /// primary finishes it — has its cargo all the same, and one found with its - /// `SOURCES` and without its cargo is rebuilt rather than trusted, once. + /// The primary's compiler under `base`: `rustc` and `rust-lld`, and the C + /// toolchain `src/clang.rs` provisions beside them if `clang`; no cargo. + fn primary_compiler(base: &Path, clang: bool) -> Compiler { + let compiler = Compiler::primary(&base.join("rust")); + write(&compiler.stage2.join("bin/rustc"), "rustc"); + let lld = toolchain::rust_lld(&compiler.stage2); + write(&lld, "lld"); + write(&lld.with_file_name("llvm-ar"), "llvm-ar"); + if clang { + for tool in ["clang", "ld.lld"] { + write(&lld.with_file_name(tool), tool); + } + write(&lld.parent().unwrap().parent().unwrap().join("lib/clang/22/include/stddef.h"), "stddef"); + } + compiler + } + + /// The sysroot `key` names at `dir`, made by `make` as [`ensure`] makes it. + fn held(base: &Path, key: &str, dir: &Path, make: impl FnMut()) -> Guard { + buildlock::keyed_made(base, Keyed::Sysroot, key, || unfinished(dir), make) + } + + /// What a panic in `f` said. + fn refusal(f: impl FnOnce()) -> String { + let refused = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).expect_err("nothing was refused"); + *refused.downcast::().expect("a formatted refusal") + } + + /// **A sysroot is whole, or it is made again**: a `stage2` without cargo — + /// what bootstrap leaves until the primary completes it — is refused by + /// name and nothing is published, and one found with its `SOURCES` and + /// without its cargo is rebuilt rather than trusted, once. #[test] fn a_sysroot_is_whole_or_it_is_made_again() { let base = TempDir::new("whole"); git(&base, &["init", "-q"]); - let stage2 = base.join("stage2"); - write(&stage2.join("bin/rustc"), "rustc"); - let lld = toolchain::rust_lld(&stage2); - write(&lld, "lld"); - // The C toolchain `src/clang.rs` provisions beside it. - for tool in ["llvm-ar", "clang", "ld.lld"] { - write(&lld.with_file_name(tool), tool); - } - write(&lld.parent().unwrap().parent().unwrap().join("lib/clang/22/include/stddef.h"), "stddef"); - let dir = sysroots_dir(&base.join("rust")).join("found"); + let compiler = primary_compiler(&base, true); let made = std::cell::Cell::new(0); let make = |dir: &Path| { made.set(made.get() + 1); - publish(&stage2, dir, |partial| { + publish(&compiler, dir, |partial| { write(&partial.join("lib/rustlib/x86_64-unknown-toyos/lib/libstd.rlib"), "std"); "found\n".to_string() }) }; let fresh = sysroots_dir(&base.join("rust")).join("fresh"); - drop(held(&base, "fresh", &fresh, || make(&fresh))); - assert_eq!( - toolchain::toolchain_defect(&fresh), - None, - "a sysroot was published without its cargo, cloned from a stage2 that had none" - ); + let said = refusal(|| drop(held(&base, "fresh", &fresh, || make(&fresh)))); + assert!(said.contains("is missing cargo") && said.contains("`cargo run -- --build-only`"), "{said}"); + assert!(!fresh.exists() && !fresh.with_extension("partial").exists(), "a sysroot was published from a stage2 without cargo"); + assert_eq!(made.get(), 1); - clone_tree(&stage2, &dir); + let dir = sysroots_dir(&base.join("rust")).join("found"); + clone_tree(&compiler.stage2, &dir); write(&dir.join(SOURCES), "found\n"); - let before = made.get(); + toolchain::provision_toolchain_cargo(&compiler.stage2); let using = held(&base, "found", &dir, || make(&dir)); - assert_eq!(made.get(), before + 1, "a sysroot without its cargo was trusted because it has SOURCES"); + assert_eq!(made.get(), 2, "a sysroot without its cargo was trusted because it has SOURCES"); assert_eq!(toolchain::toolchain_defect(&dir), None); assert!(dir.join("lib/rustlib/x86_64-unknown-toyos/lib/libstd.rlib").is_file()); drop(using); drop(held(&base, "found", &dir, || make(&dir))); - assert_eq!(made.get(), before + 1, "a whole sysroot was made again"); + assert_eq!(made.get(), 2, "a whole sysroot was made again"); + } + + /// **A sysroot that cannot be made whole is refused after one make, never + /// made again**: a `stage2` without clang — what a stopped bootstrap leaves — + /// is refused before any std is built for it, with nothing published, and a + /// make that leaves its sysroot not whole is refused by what it lacks. + #[test] + fn a_sysroot_that_cannot_be_made_whole_is_made_once_and_refused() { + let base = TempDir::new("no-clang"); + git(&base, &["init", "-q"]); + let compiler = primary_compiler(&base, false); + toolchain::provision_toolchain_cargo(&compiler.stage2); + let made = std::cell::Cell::new(0); + let once = || { + made.set(made.get() + 1); + assert_eq!(made.get(), 1, "a sysroot that was not whole was made again"); + }; + + let dir = sysroots_dir(&base.join("rust")).join("cloned"); + let filled = std::cell::Cell::new(false); + let said = refusal(|| { + drop(held(&base, "cloned", &dir, || { + once(); + publish(&compiler, &dir, |_| { + filled.set(true); + "cloned\n".to_string() + }) + })) + }); + assert!(said.contains("carries no") && said.contains("/clang"), "{said}"); + assert!(said.contains(&compiler.stage2.display().to_string()) && said.contains("`cargo run -- --build-only`"), "{said}"); + assert!(!filled.get(), "a std was built for a sysroot of a compiler without clang"); + assert!(!dir.exists() && !dir.with_extension("partial").exists(), "a sysroot was published from a stage2 without clang"); + assert_eq!(made.get(), 1); + + made.set(0); + let dir = sysroots_dir(&base.join("rust")).join("made"); + let said = refusal(|| { + drop(held(&base, "made", &dir, || { + once(); + clone_tree(&compiler.stage2, &dir); + write(&dir.join(SOURCES), "made\n"); + })) + }); + assert!(said.starts_with("sysroot made was made, and is not whole") && said.contains("/clang"), "{said}"); + assert_eq!(made.get(), 1); } /// A key no registered worktree records goes, and so does a half-built one; diff --git a/src/toolchain.rs b/src/toolchain.rs index be61bc1a03..27842a5d9b 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -12,7 +12,7 @@ use crate::sysroot::{self, Sysroot, SYSROOT_SOURCES}; /// separates "the compiler changed" from "the rustup link is missing": only the /// first makes the ToyOS-hosted rustc stale, and rebuilding that one costs /// minutes. -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Debug)] struct Bootstrap { invalidate_hosted: bool, } @@ -347,7 +347,7 @@ pub(crate) fn assert_toolchain_is_honest(stage2: &Path) { /// `stage2`'s cargo and clang, or the host target in the hosted rustc's sysroot. fn incomplete(rust_dir: &Path) -> bool { let stage2 = stage2(rust_dir); - cargo_link_stale(&stage2) || crate::clang::missing(&stage2) || host_target_missing(rust_dir) + cargo_link_stale(&stage2) || crate::clang::defect(&stage2).is_some() || host_target_missing(rust_dir) } /// Give the primary's toolchain what [`incomplete`] finds missing. @@ -356,7 +356,7 @@ fn complete(rust_dir: &Path) { if cargo_link_stale(&stage2) { provision_toolchain_cargo(&stage2); } - if crate::clang::missing(&stage2) { + if crate::clang::defect(&stage2).is_some() { crate::clang::provision(&stage2); } if host_target_missing(rust_dir) { @@ -375,6 +375,26 @@ fn reassemble(rust_dir: &Path, bootstrap: impl FnOnce()) { complete(rust_dir); } +/// [`reassemble`] the primary's compiler with `bootstrap`, with nothing recording +/// which compiler `stage2` is until it is whole: a bootstrap that is stopped is +/// run again by the primary, and refused by name in every linked worktree. +fn rebuild_compiler(rust_dir: &Path, bootstrap: impl FnOnce()) { + crate::compiler::forget(rust_dir); + reassemble(rust_dir, bootstrap); + crate::compiler::record(rust_dir); +} + +/// What the primary bootstraps: a new compiler when asked to or when `stage2` +/// is not the one its `compiler/` names, and the same one again when rustup has +/// no `toyos` toolchain to run. +fn bootstrap(force_rebuild: bool, current: bool, toolchain_exists: bool) -> Option { + if force_rebuild || !current { + Some(Bootstrap { invalidate_hosted: true }) + } else { + (!toolchain_exists).then_some(Bootstrap { invalidate_hosted: false }) + } +} + /// Complete a toolchain whose bootstrap was stopped before [`reassemble`] /// finished it. Every other build decides there is nothing to do, and takes no /// lock. @@ -449,9 +469,6 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S || { // Asked first, so an LLVM edit no commit holds is refused before any build. let current = crate::compiler::primary_is_current(&rust_dir); - if force_rebuild || !current { - return Some(Bootstrap { invalidate_hosted: true }); - } let toolchain_exists = Command::new("rustup") .args(["run", "toyos", "rustc", "--version"]) .stdout(std::process::Stdio::null()) @@ -459,12 +476,11 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S .status() .map(|s| s.success()) .unwrap_or(false); - (!toolchain_exists).then_some(Bootstrap { invalidate_hosted: false }) + bootstrap(force_rebuild, current, toolchain_exists) }, |kind| { eprintln!("Building full toolchain (this takes a while on first run)..."); - reassemble(&rust_dir, || full_bootstrap(root, &rust_dir)); - crate::compiler::record(&rust_dir); + rebuild_compiler(&rust_dir, || full_bootstrap(root, &rust_dir)); if kind.invalidate_hosted { let _ = fs::remove_file(&hosted_stamp); } @@ -1085,13 +1101,16 @@ mod tests { reassemble(&rust_dir, || { // What bootstrap leaves: `stage2` made again, with `rustc` and the - // LLVM tools it assembles, and neither cargo nor clang. + // LLVM tools it assembles, and neither cargo nor clang; and the + // hosted rustc's sysroot without the host target. let _ = fs::remove_dir_all(&stage2); let lld = rust_lld(&stage2); for file in [stage2.join("bin/rustc"), lld.clone(), lld.with_file_name("llvm-ar")] { fs::create_dir_all(file.parent().unwrap()).unwrap(); fs::write(file, b"").unwrap(); } + let hosted = rust_dir.join(format!("build/{}/stage2/lib/rustlib", HOSTED_ARCH.userland())); + fs::create_dir_all(&hosted).unwrap(); }); assert!( !incomplete(&rust_dir), @@ -1101,6 +1120,44 @@ mod tests { assert_eq!(toolchain_defect(&stage2), None, "a bootstrap let its exclusive hold go with stage2 not whole"); } + /// **A stopped bootstrap is run again**: nothing records which compiler + /// `stage2` is while one runs, so the primary's next build is not told the + /// old one is current. + #[test] + fn a_stopped_bootstrap_leaves_no_record() { + let rust_dir = TempDir::new("stopped"); + let record = crate::compiler::primary_record(&rust_dir); + fs::create_dir_all(record.parent().unwrap()).unwrap(); + fs::write(&record, "the compiler before").unwrap(); + let stopped = std::panic::catch_unwind(|| rebuild_compiler(&rust_dir, || panic!("stopped"))); + assert!(stopped.is_err()); + assert!(!record.exists(), "a stopped bootstrap left the record of the compiler before it"); + } + + /// **The primary bootstraps a new compiler exactly when asked to or when its + /// `stage2` is not current, and otherwise only when rustup has none.** + #[test] + fn the_primary_bootstraps_when_asked_stale_or_missing() { + let new = Some(Bootstrap { invalidate_hosted: true }); + let again = Some(Bootstrap { invalidate_hosted: false }); + for (force_rebuild, current, toolchain_exists, want) in [ + (false, true, true, None), + (false, true, false, again), + (false, false, true, new), + (false, false, false, new), + (true, true, true, new), + (true, true, false, new), + (true, false, true, new), + (true, false, false, new), + ] { + assert_eq!( + bootstrap(force_rebuild, current, toolchain_exists), + want, + "force_rebuild {force_rebuild}, current {current}, toolchain_exists {toolchain_exists}" + ); + } + } + /// The negative control is the defect itself: this is verbatim what cargo /// wrote for a worktree build before the override existed. #[test] From ef49a89b8e9a40c0513883d93d5385d1c2ad71aa Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 23:07:55 +0200 Subject: [PATCH 6/9] sysroot: the wholeness test bounds its makes, so a loop fails it rather 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 --- src/sysroot.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sysroot.rs b/src/sysroot.rs index 2fdd91143f..b62f6a8702 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -969,8 +969,10 @@ mod tests { git(&base, &["init", "-q"]); let compiler = primary_compiler(&base, true); let made = std::cell::Cell::new(0); - let make = |dir: &Path| { + // `most` bounds the makes so far, so a make that loops fails rather than hangs. + let make = |dir: &Path, most: usize| { made.set(made.get() + 1); + assert!(made.get() <= most, "a sysroot that was not whole was made again"); publish(&compiler, dir, |partial| { write(&partial.join("lib/rustlib/x86_64-unknown-toyos/lib/libstd.rlib"), "std"); "found\n".to_string() @@ -978,7 +980,7 @@ mod tests { }; let fresh = sysroots_dir(&base.join("rust")).join("fresh"); - let said = refusal(|| drop(held(&base, "fresh", &fresh, || make(&fresh)))); + let said = refusal(|| drop(held(&base, "fresh", &fresh, || make(&fresh, 1)))); assert!(said.contains("is missing cargo") && said.contains("`cargo run -- --build-only`"), "{said}"); assert!(!fresh.exists() && !fresh.with_extension("partial").exists(), "a sysroot was published from a stage2 without cargo"); assert_eq!(made.get(), 1); @@ -987,12 +989,12 @@ mod tests { clone_tree(&compiler.stage2, &dir); write(&dir.join(SOURCES), "found\n"); toolchain::provision_toolchain_cargo(&compiler.stage2); - let using = held(&base, "found", &dir, || make(&dir)); + let using = held(&base, "found", &dir, || make(&dir, 2)); assert_eq!(made.get(), 2, "a sysroot without its cargo was trusted because it has SOURCES"); assert_eq!(toolchain::toolchain_defect(&dir), None); assert!(dir.join("lib/rustlib/x86_64-unknown-toyos/lib/libstd.rlib").is_file()); drop(using); - drop(held(&base, "found", &dir, || make(&dir))); + drop(held(&base, "found", &dir, || make(&dir, 2))); assert_eq!(made.get(), 2, "a whole sysroot was made again"); } From c385c0c3da6681c4041573e5b35f26a714066ea5 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 23:09:02 +0200 Subject: [PATCH 7/9] sysroot: the bounded make's refusal is a formatted one, which the test reads Co-Authored-By: Claude Opus 5.5 --- src/sysroot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysroot.rs b/src/sysroot.rs index b62f6a8702..1afd6c44d5 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -972,7 +972,7 @@ mod tests { // `most` bounds the makes so far, so a make that loops fails rather than hangs. let make = |dir: &Path, most: usize| { made.set(made.get() + 1); - assert!(made.get() <= most, "a sysroot that was not whole was made again"); + assert!(made.get() <= most, "a sysroot that was not whole was made again: make {}", made.get()); publish(&compiler, dir, |partial| { write(&partial.join("lib/rustlib/x86_64-unknown-toyos/lib/libstd.rlib"), "std"); "found\n".to_string() From f23fea748120004b85adb5a07042d823165bea62 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 23:51:19 +0200 Subject: [PATCH 8/9] Review round 2: ensure and its tests share the one held that closes the wholeness gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...r-source-after-the-bootstrap-it-records.md | 28 +++++++++++++++++++ src/buildlock.rs | 4 +-- src/sysroot.rs | 17 +++++------ src/toolchain.rs | 23 ++++++--------- 4 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md diff --git a/issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md b/issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md new file mode 100644 index 0000000000..354d45eed8 --- /dev/null +++ b/issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md @@ -0,0 +1,28 @@ +--- +status: open +kind: tooling +opened: 2026-09-28 +--- + +# `record` reads `compiler/` as it stands after the bootstrap it records + +`compiler::record` (`src/compiler.rs`) is called once `reassemble` has finished +building `stage2`, and it computes `source(rust_dir)` at that point — the +`compiler/` tree, working diff and untracked files as they read *then*, not as +they read when the bootstrap it is recording began. A `compiler/` edit made +while that bootstrap was running (`x.py build` takes minutes) is folded into +the record even though `stage2` was built without it, so the next build sees +`primary_is_current` return true for a `stage2` that does not contain the edit, +and skips a bootstrap that is actually owed. + +Main has the same order (`record`'s only caller runs it after the build, and +main's predecessor — the `compiler.stamp` mtime plus the `moved` comparison — +had the identical property), so this is not a regression of this branch; it is +carried forward unchanged. + +Exit condition: `record` (or whoever calls it) captures `source(rust_dir)` +before the bootstrap starts, not after, and a test edits `compiler/` mid-build +(a `bootstrap` closure that writes a file before returning) and asserts the +next `primary_is_current` is false. + +Owner: whoever next touches `compiler::record` or `rebuild_compiler`. diff --git a/src/buildlock.rs b/src/buildlock.rs index 8383372c2d..2b63a4c296 100644 --- a/src/buildlock.rs +++ b/src/buildlock.rs @@ -265,14 +265,14 @@ impl Keyed { /// Make what `key` names: exclusive, and waited for by every other process that /// wants the same key, which then finds it made. -pub fn keyed_building(root: &Path, kind: Keyed, key: &str) -> Guard { +fn keyed_building(root: &Path, kind: Keyed, key: &str) -> Guard { let lock = format!("{} lock", kind.name()); exclusive(&keyed_lock_path(root, kind, key), &lock, &format!("building {} {key}", kind.name())) } /// Use what `key` names: shared, so any number of builds use it at once, a /// builder of it is waited for, and a sweep cannot remove it. -pub fn keyed_using(root: &Path, kind: Keyed, key: &str) -> Guard { +fn keyed_using(root: &Path, kind: Keyed, key: &str) -> Guard { let path = keyed_lock_path(root, kind, key); let file = open_lock_file(&path); if !try_lock(&file, LOCK_SH) { diff --git a/src/sysroot.rs b/src/sysroot.rs index 1afd6c44d5..e6220f123f 100644 --- a/src/sysroot.rs +++ b/src/sysroot.rs @@ -311,6 +311,12 @@ fn unfinished(dir: &Path) -> Option { toolchain::toolchain_defect(dir) } +/// The sysroot `key` names at `dir`, made by `make` if nobody has made it, and +/// held in use for as long as the returned guard lives. +fn held(root: &Path, key: &str, dir: &Path, make: impl FnMut()) -> Guard { + buildlock::keyed_made(root, Keyed::Sysroot, key, || unfinished(dir), make) +} + /// The sysroot this worktree's sources name, made if nobody has made it, and /// held in use for as long as the returned value lives. pub fn ensure(root: &Path, rust_dir: &Path, lock: &mut Held) -> Sysroot { @@ -322,11 +328,7 @@ pub fn ensure(root: &Path, rust_dir: &Path, lock: &mut Held) -> Sysroot { fs::create_dir_all(record.parent().expect("a file under target/")).ok(); fs::write(&record, &key).unwrap_or_else(|e| panic!("write {}: {e}", record.display())); - let using = lock.without_shared(|| { - buildlock::keyed_made(root, Keyed::Sysroot, &key, || unfinished(&dir), || { - build(root, &compiler, &fork, &key, &dir) - }) - }); + let using = lock.without_shared(|| held(root, &key, &dir, || build(root, &compiler, &fork, &key, &dir))); Sysroot { dir, primary_compiler: compiler.primary, _using: Some(using) } } @@ -948,11 +950,6 @@ mod tests { compiler } - /// The sysroot `key` names at `dir`, made by `make` as [`ensure`] makes it. - fn held(base: &Path, key: &str, dir: &Path, make: impl FnMut()) -> Guard { - buildlock::keyed_made(base, Keyed::Sysroot, key, || unfinished(dir), make) - } - /// What a panic in `f` said. fn refusal(f: impl FnOnce()) -> String { let refused = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).expect_err("nothing was refused"); diff --git a/src/toolchain.rs b/src/toolchain.rs index 27842a5d9b..24a111c6e7 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -395,18 +395,6 @@ fn bootstrap(force_rebuild: bool, current: bool, toolchain_exists: bool) -> Opti } } -/// Complete a toolchain whose bootstrap was stopped before [`reassemble`] -/// finished it. Every other build decides there is nothing to do, and takes no -/// lock. -fn complete_toolchain(lock: &mut buildlock::Held, rust_dir: &Path) { - lock.act_if( - Scope::Global, - "complete the toyos toolchain", - || incomplete(rust_dir).then_some(()), - |()| complete(rust_dir), - ); -} - /// Ensure the toolchain is up to date, and return the sysroot this checkout's /// sources name — made if nobody has made it (`src/sysroot.rs`). /// @@ -467,7 +455,6 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S Scope::Global, "build the rust toolchain", || { - // Asked first, so an LLVM edit no commit holds is refused before any build. let current = crate::compiler::primary_is_current(&rust_dir); let toolchain_exists = Command::new("rustup") .args(["run", "toyos", "rustc", "--version"]) @@ -513,7 +500,15 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S }, ); - complete_toolchain(lock, &rust_dir); + // Completes a toolchain whose bootstrap was stopped before `reassemble` + // finished it; every other build decides there is nothing to do, and takes + // no lock. + lock.act_if( + Scope::Global, + "complete the toyos toolchain", + || incomplete(&rust_dir).then_some(()), + |()| complete(&rust_dir), + ); assert_toolchain_is_honest(&stage2); sysroot::ensure(root, &rust_dir, lock) From 4aca3c80382fe37fdf01f2fa63c8c008f24a9394 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 00:07:17 +0200 Subject: [PATCH 9/9] PR #558 round 3: delete superseded text. 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 --- ...d-reads-compiler-source-after-the-bootstrap-it-records.md | 5 ----- src/toolchain.rs | 3 --- 2 files changed, 8 deletions(-) diff --git a/issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md b/issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md index 354d45eed8..9300b15bae 100644 --- a/issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md +++ b/issues/build/record-reads-compiler-source-after-the-bootstrap-it-records.md @@ -15,11 +15,6 @@ the record even though `stage2` was built without it, so the next build sees `primary_is_current` return true for a `stage2` that does not contain the edit, and skips a bootstrap that is actually owed. -Main has the same order (`record`'s only caller runs it after the build, and -main's predecessor — the `compiler.stamp` mtime plus the `moved` comparison — -had the identical property), so this is not a regression of this branch; it is -carried forward unchanged. - Exit condition: `record` (or whoever calls it) captures `source(rust_dir)` before the bootstrap starts, not after, and a test edits `compiler/` mid-build (a `bootstrap` closure that writes a file before returning) and asserts the diff --git a/src/toolchain.rs b/src/toolchain.rs index 24a111c6e7..d274c54787 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -500,9 +500,6 @@ pub fn ensure(root: &Path, force_rebuild: bool, lock: &mut buildlock::Held) -> S }, ); - // Completes a toolchain whose bootstrap was stopped before `reassemble` - // finished it; every other build decides there is nothing to do, and takes - // no lock. lock.act_if( Scope::Global, "complete the toyos toolchain",