Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
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.

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`.
2 changes: 1 addition & 1 deletion src/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<key>/`, 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 `rust/library/` and `rust/src/bootstrap/`, and the compiler — at `rust/build/sysroots/<key>/`, 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`.
Expand Down
31 changes: 29 additions & 2 deletions src/buildlock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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<String>,
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<Guard> {
Expand Down
31 changes: 17 additions & 14 deletions src/clang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,21 +115,24 @@ fn absent(toolchain: &Path) -> Vec<String> {
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<String> {
let gone = absent(toolchain);
(!gone.is_empty()).then(|| {
format!(
"the toyos toolchain at {} carries no {}, and every C compile needs it: \
`clang::provision` puts it there after each build that makes a stage2, and it did not",
toolchain.display(),
gone.join(", "),
)
})
}

/// Refuse a toolchain directory without its C toolchain.
pub(crate) fn assert_present(toolchain: &Path) {
let gone = absent(toolchain);
assert!(
gone.is_empty(),
"the toyos toolchain at {} carries no {}, and every C compile needs it: \
`clang::provision` puts it there after each build that makes a stage2, and it did not",
toolchain.display(),
gone.join(", "),
);
if let Some(defect) = defect(toolchain) {
panic!("{defect}");
}
}

/// The one version directory under an LLVM build's `lib/clang`.
Expand Down Expand Up @@ -200,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::<String>().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"));
Expand All @@ -223,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");
}
}
97 changes: 80 additions & 17 deletions src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,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")
}

Expand Down Expand Up @@ -147,8 +147,7 @@ fn compiler_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.
pub fn record(rust_dir: &Path) {
let at = primary_record(rust_dir);
let want = source(rust_dir);
Expand All @@ -157,6 +156,36 @@ 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.
///
/// **The source's content, never its files' times**: a checkout that rewrites a
/// file with the bytes it had is no new compiler.
fn primary_is(rust_dir: &Path, checkout: &Path) -> Result<bool, std::io::Error> {
let names = source(checkout);
Ok(fs::read_to_string(primary_record(rust_dir))?.trim() == names)
}

/// 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 {
Expand Down Expand Up @@ -204,16 +233,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);
}
Expand All @@ -222,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 {
Expand Down Expand Up @@ -594,6 +620,43 @@ 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 files = snapshot(&rust_dir.join("compiler"));
let later = std::time::SystemTime::now() + std::time::Duration::from_secs(3600);
for (file, bytes) in &files {
fs::write(file, bytes).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");
}

/// `fork`'s LLVM checked out at a commit of its own.
fn llvm_checkout(fork: &Path) -> PathBuf {
let llvm = fork.join(LLVM);
Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ pub mod soundfont;
/// build system at all.
#[cfg(test)]
pub mod sourcegate;
pub mod stamps;
pub mod sysroot;
pub mod testargs;
pub mod tiers;
Expand Down
55 changes: 0 additions & 55 deletions src/stamps.rs

This file was deleted.

Loading
Loading