Repository navigation
Every app the images ship is built or checked for Linux, macOS and Windows, or its manifest says why it cannot - #674
Conversation
…hy it cannot The owner's rule: an app builds and runs on Linux under Wayland, macOS and Windows from the same source as on ToyOS, and a host build that fails is fixed in the app or its dependencies, never by making the app ToyOS-only. The one exemption is a program that by its nature cannot run elsewhere: a system server that owns ToyOS devices or kernel objects, like the compositor. calc broke the rule unseen, because CI built apps on macOS alone. The gate. `src/userlandhost.rs`'s `packages` reads every member of the userland workspace and its own manifest's `[package.metadata.toyos.host]`: nothing, for an app; `exempt = "<why>"`; or `fails = [<hosts>]` with the `issue` that records them, which must name the app. Any other key or shape is refused by name. `cargo run -- --ci host` gains a step per host OS. Each builds every app for this host, and `cargo check`s it against the other two hosts' triples. `judge` reds where a build and its declaration disagree, both ways, so a declared failure goes when the app builds. It runs one cargo per app, because features unify across the packages of one invocation. A Windows runner cannot run this build system (issues/build/the-build-system-does-not-compile-on-windows.md), so Windows is checked from the others. Fourteen members are exempt, each with its reason in its manifest: blockd, compositor, console, filepicker, fsd, init, inspect, logd, metalprobe, netd, soundd, swap, test-runner and update. Each claims a device, holds a SysCap, a slot or init's swap port, serves a machine-wide port, or is init. An exemption takes a package out of the build gate only: the host tests of blockd, fsd, logd, netd and soundd still run. libc is no member of the userland workspace. It is the sysroot's C library, and its manifest is part of the sysroot's and the toolchain release's key. The rule text: - userland/CLAUDE.md states it. - reviewer.md gains it as a BLOCKER. Its "No new dependency or fetch" becomes the dependency rule the owner approved, the same as implementer.md's, because a reviewer still reading the old line would block what the implementer is now told to do. - implementer.md's "No new dependency." becomes that rule. - orchestrator.md: a "blocked" report is checked against the code and the product's principles before any ruling, and no product is changed to make a check green. userland/README.md goes. Its one sentence was this rule without the exemption, and nothing loaded it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
`cargo check --manifest-path userland/sshd/Cargo.toml --target x86_64-pc-windows-msvc` stopped in socket2: "cannot find function `recvmsg` in module `sys`". The ToyOS port of socket2 (a35837c) had widened `Socket::recvmsg` from upstream's `all(unix, not(target_os = "redox"))` to every target but Redox and WASI, which takes in Windows. The fork's `toyos` branch gains 48d15f5, which names ToyOS beside upstream's condition, as its other arms do. This moves userland/Cargo.lock's one socket2 entry onto it. No other lockfile or gitlink names that branch. Measured on the macOS dev host: - socket2 alone, `cargo check --all-features`: x86_64-pc-windows-msvc exits 101 at 2b67e7b, and 0 at 48d15f5. x86_64-unknown-linux-gnu and aarch64-apple-darwin exit 0 at 48d15f5. - sshd for x86_64-pc-windows-msvc against this lockfile exits 0. - `cargo run -- --build-only`, with userland's socket2 taken from 48d15f5 through a file:// git source and the tree restored after, exits 0. The log compiles socket2 at 48d15f59 and then sshd for ToyOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Two cases in the fixture test reached the catch-all arm before the check they were meant for: a misspelt key alone, and an `issue` outside issues/ that did not exist. Now an unknown key sits beside a valid `exempt`, and the outside path exists and names the app. Each check, mutated away as a checked patch, turns `a_host_declaration_is_read_whole_and_refused_by_name` red with exit 101: the unknown-key filter made to match nothing, and the issues/ prefix test made to refuse only an empty path. The tree was restored after both. `Os::this` is `Os::current`, and the judge's two refusals say what is missing in fewer words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The Hosts bullet had landed inside Fit, ahead of Fit's architecture lines, which it would have taken as its own. It now follows Fit. The owner ruled that on Linux, ToyOS apps support Wayland alone: X11 is legacy, and no X11 backend feature is added or kept. Where the rule text names Linux it now says Wayland, userland/CLAUDE.md says never X11, and an X11 backend is a reviewer's BLOCKER. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
#667 filed what still built or ran on ToyOS alone. This takes that issue over, renamed, because the tree refutes its slug's claim: editor, files, paint and filepicker build on all three hosts. `issues/build/apps-on-toyos-window-terminal-toybox-and-doom-build-for-toyos-alone.md` is now `issues/build/apps-that-build-or-run-on-toyos-alone.md`. Nothing cited the old name. The set is now the one the gate measures. doom, toybox, terminal, shell and proctest build on none of Linux, macOS and Windows. Each declares `fails = ["linux", "macos", "windows"]` against the issue, which names each. Measured on the macOS dev host, natively and by `cargo check --target` for the other two: - shell and proctest reach `std::os::toyos`, as terminal and toybox do. #667's Linux-only table could not show it for those two. - filepicker leaves the issue. It is exempt: init starts it at boot to serve the machine-wide `filepicker` port. - editor, files and paint keep their row. They build, and toyos-window reaches no display but ToyOS's compositor. The exit now says what the gate can fail: no manifest names the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Review of 16ba295. CI: BLOCKER
NOTE
REMOVE
SEND BACK |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
ToyOSOrg/socket2 `toyos` 48d15f5 narrowed `Socket::recvmsg` to upstream's condition with ToyOS beside it, and left the `use crate::MsgHdrMut` only `recvmsg` reads under the port's wider one, so on Windows the import is unused and warns. 0b238fb gives the import `recvmsg`'s condition. In the fork, `RUSTFLAGS="-D warnings -A unexpected_cfgs" cargo check --all-features --target x86_64-pc-windows-msvc` exits 101 at 48d15f5 on that warning alone, and 0 at 0b238fb, as it does for x86_64-unknown-linux-gnu and aarch64-apple-darwin at both. `unexpected_cfgs` is allowed because stable rustc knows no `toyos` target and warns on every arm naming it. `userland/Cargo.lock` is the one lockfile or gitlink naming the branch, and its socket2 entry moves to 0b238fb. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…y everywhere, and an exemption names its case The review of 16ba295 sent the gate back on its universe, its judges and two host needs it did not declare. Each is answered at its root. The universe is `build::shipped`. `Shipped` gains `programs`, its `crates` but the kernel and the loader, from the same `config_crates` walk the build and the licence gate read; `userlandhost::programs` reads each one's manifest. A program outside the userland workspace is judged now: toyos-ld, which checks for all three triples. test-runner and metalprobe ship in no mode's image, so nothing reads a declaration of theirs and both go. One judgement per host. Every host's verdict is `cargo check --target` its triple, on whichever host runs `--ci host`. Before, the PR runner built Linux natively and the nightly's macOS runner checked it, and macOS the reverse, so one `fails` entry was right on one runner and wrong on the other. `Os::current` goes with the native arm. Linux is no longer linked on the PR runner: linking there would make Linux's verdict differ again from the macOS runner's check of it. `issues/build/no-app-is-built-for-a-host-each-is-only-checked.md` records what a check cannot see, on every host, with its exit. A host an app's `fails` names is not attempted. The gate compiled doom and toybox only to see them fail: on Linux cpal's `alsa-sys` ran `pkg-config`, and doom's build script fetched doomgeneric before it read `CC_<target>`. Neither runs now. The ratchet that made a declared failure that builds red goes with it, so a stale `fails` stays until the one who fixes the app removes it. An exemption names its case, the owner's two: `exempt.owns`, the ToyOS devices or kernel objects a program owns (blockd, compositor, console, fsd, init, logd, netd, soundd), or `exempt.manages`, the part of ToyOS it manages (inspect, swap, update), each with what and why. Any other shape is refused by name. filepicker is no longer exempt: it is a file dialog over `std::fs`, and its port is only how it is reached. It checks for all three triples, and the issue records that it runs on none, with editor, files and paint. A `fails` entry's `issue` must be `issues/<area>/<slug>.md` whose front matter says `open` or `assigned`: `issues/README.md` and `issues/../…` are refused. `userland/CLAUDE.md` and the reviewer's Hosts bullet state the two cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Round 2's negative controls, as the checked patches the PR body names. Each applies to 33f357f ( The measurement harness, applied under the three gate cases (unmutated, diff --git a/src/ci.rs b/src/ci.rs
index eb5d4b943..724806ce1 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -859,6 +859,17 @@ fn at_tip(ls_remote: &str, head: &str) -> Result<(), String> {
mod tests {
use super::*;
+ #[test]
+ fn measure_apps() {
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
+ let programs = crate::userlandhost::programs(root).expect("programs");
+ let verdicts: Vec<_> = Os::ALL.into_iter().map(|os| (os, apps_for(root, &programs, os))).collect();
+ for (os, verdict) in &verdicts {
+ eprintln!("MEASURE {os:?}: {verdict:?}");
+ }
+ assert!(verdicts.iter().all(|(_, v)| v.is_ok()), "a host is red");
+ }
+
/// A deterministic control on `host`'s `std::env::set_var("TMPDIR", ...)`:
/// delete that line and every child writes to the real `$TMPDIR` instead of
/// this job's private one, which never gains the lock a real step always
diff --git a/userland/console/Cargo.toml b/userland/console/Cargo.toml
index 4e35113d4..04acb0730 100644
--- a/userland/console/Cargo.toml
+++ b/userland/console/Cargo.toml
@@ -11,6 +11,3 @@ toyos-abi = { path = "../../toyos-abi" }
toyos-font = { path = "../toyos-font" }
toyos-window = { path = "../toyos-window" }
toyos-logstream = { path = "../../toyos-logstream" }
-
-[package.metadata.toyos.host]
-exempt.owns = "ToyOS's framebuffer, keyboard and mouse, claimed from the kernel, on which it runs the shell"
diff --git a/src/ci.rs b/src/ci.rs
index eb5d4b943..71c72e149 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -566,7 +566,7 @@ fn apps_for(root: &Path, programs: &[Program], os: Os) -> Result<String, String>
let (mut checked, mut declared, mut red) = (0, Vec::new(), Vec::new());
for program in programs {
let Host::App(fails) = &program.host else { continue };
- if fails.contains(&os) {
+ if false && fails.contains(&os) {
declared.push(program.dir.as_str());
continue;
}
diff --git a/src/userlandhost.rs b/src/userlandhost.rs
index 7cb912ba8..c06fb8ee1 100644
--- a/src/userlandhost.rs
+++ b/src/userlandhost.rs
@@ -195,7 +195,7 @@ pub struct Program {
/// declares about the hosts.
pub fn programs(root: &Path) -> Result<Vec<Program>, String> {
let mut found = Vec::new();
- for (dir, features) in crate::build::shipped(root)?.programs {
+ for (dir, features) in crate::build::shipped(root)?.programs.into_iter().filter(|(d, _)| d.starts_with(root.join("userland"))) {
let at = rel(root, &dir);
let text = std::fs::read_to_string(dir.join("Cargo.toml"))
.map_err(|e| format!("{at}/Cargo.toml: {e}"))?;
diff --git a/src/userlandhost.rs b/src/userlandhost.rs
index 7cb912ba8..1fb9ed549 100644
--- a/src/userlandhost.rs
+++ b/src/userlandhost.rs
@@ -270,7 +270,7 @@ fn owed(root: &Path, issue: &str, app: &str) -> Result<(), String> {
["issues", area, slug] => word(area) && slug.strip_suffix(".md").is_some_and(word),
_ => false,
};
- if !shaped {
+ if false && !shaped {
return Err(format!("`issue` is {issue:?}, which is no issues/<area>/<slug>.md"));
}
let text = std::fs::read_to_string(root.join(issue))
diff --git a/src/userlandhost.rs b/src/userlandhost.rs
index 7cb912ba8..ca7b862fc 100644
--- a/src/userlandhost.rs
+++ b/src/userlandhost.rs
@@ -281,7 +281,7 @@ fn owed(root: &Path, issue: &str, app: &str) -> Result<(), String> {
.into_iter()
.flat_map(str::lines)
.find_map(|line| line.strip_prefix("status: "));
- if !matches!(status, Some("open" | "assigned")) {
+ if false && !matches!(status, Some("open" | "assigned")) {
return Err(format!("{issue} is no work still owed: its status is {status:?}"));
}
if !text.contains(&format!("`{app}`")) {
diff --git a/src/userlandhost.rs b/src/userlandhost.rs
index 7cb912ba8..4c949ace1 100644
--- a/src/userlandhost.rs
+++ b/src/userlandhost.rs
@@ -225,7 +225,7 @@ fn declared(manifest: &toml::Value, root: &Path) -> Result<Host, String> {
let case = exempt.as_table().filter(|t| t.len() == 1).and_then(|t| t.iter().next());
match case {
Some((case, why))
- if ["owns", "manages"].contains(&case.as_str())
+ if !case.is_empty()
&& why.as_str().is_some_and(|why| !why.trim().is_empty()) =>
{
Ok(Host::Exempt)
diff --git a/src/build.rs b/src/build.rs
index f37b03422..0f0d5ac01 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1109,7 +1109,7 @@ pub fn shipped(root: &Path) -> Result<Shipped, String> {
));
}
for c in config_crates(root, &config) {
- if matches!(c.built, Built::Member | Built::Standalone) {
+ if matches!(c.built, Built::Member) {
programs.insert((c.dir.clone(), c.features));
}
crates.insert((c.dir, c.features));The runners: #!/bin/bash
# Each case: the measurement harness plus at most one mutation, applied as checked
# patches, built, run, and reverted; the tree is shown clean after each.
S=/private/tmp/claude-502/-Users-jan-Dev-jan-toyos/2280e09e-428b-4b81-bc00-1ede594b7247/scratchpad
cd /Users/jan/Dev/jan/toyos-hostapps || exit 2
for case in baseline m1-console-unexempt m2-no-skip; do
patches=("$S/measure.patch")
[ "$case" != baseline ] && patches+=("$S/$case.patch")
before=$(git status --porcelain | wc -l | tr -d ' ')
for p in "${patches[@]}"; do git apply --check "$p" && git apply "$p" || { echo "$case: $p does not apply"; exit 3; }; done
cargo test --lib --no-run > "$S/ctl-$case-build.log" 2>&1; built=$?
if [ $built = 0 ]; then
cargo test --lib measure_apps -- --nocapture > "$S/ctl-$case.log" 2>&1; rc=$?
else
rc=build-failed
fi
for p in "${patches[@]}"; do git apply -R "$p"; done
after=$(git status --porcelain | wc -l | tr -d ' ')
echo "$case: build EXIT=$built run EXIT=$rc dirty-before=$before dirty-after=$after"
done
echo DONE#!/bin/bash
# Each mutation applied as a checked patch, built, its tests run, and reverted.
S=/private/tmp/claude-502/-Users-jan-Dev-jan-toyos/2280e09e-428b-4b81-bc00-1ede594b7247/scratchpad
cd /Users/jan/Dev/jan/toyos-hostapps || exit 2
for m in m3-userland-only m4-no-shape m5-no-status m6-any-case m7-members-only; do
before=$(git status --porcelain | wc -l | tr -d ' ')
git apply --check "$S/$m.patch" && git apply "$S/$m.patch" || { echo "$m does not apply"; exit 3; }
cargo test --lib --no-run > "$S/unit-$m-build.log" 2>&1; built=$?
if [ $built = 0 ]; then
cargo test --lib -- userlandhost every_modes_crates_and_init_ship > "$S/unit-$m.log" 2>&1; rc=$?
else
rc=build-failed
fi
git apply -R "$S/$m.patch"
after=$(git status --porcelain | wc -l | tr -d ' ')
echo "$m: build EXIT=$built run EXIT=$rc dirty-before=$before dirty-after=$after"
grep -E "\.\.\. FAILED" "$S/unit-$m.log"
done
echo DONETheir output: |
|
Review of 33f357f, round 2, against Round 1's BLOCKERs
Round 1's NOTEs are all answered at this head except #673 and the Windows issue, both carried below. Its four REMOVEs are gone. BLOCKER
NOTE
REMOVE
SEND BACK |
…where the gate runs on it, and a stale `fails` is recorded `apps_for`'s two per-host decisions are pure functions now, `attempted` and `verb`, and `an_app_is_judged_for_every_host_its_fails_does_not_name` holds both with no cargo run: an app whose `fails` is `[windows]` is judged for Linux and macOS and skipped for Windows, an exempt program is judged for none, and on a Linux host Linux's apps are built and the other two hosts' checked. Nothing tested the skip before: every `fails` in the tree names all three hosts, so `if !fails.is_empty()` in place of `if fails.contains(&os)` stayed green everywhere. Where the gate runs on the host it judges, it builds: `cargo build --target <triple>` when the triple is the host's own, and `cargo check --target <triple>` elsewhere. On ubuntu-24.04 that links Linux's apps on every pull request, and on the nightly's macOS runner, macOS's. Round 2 dropped the link so that a host would have one verdict on every runner, which matters only beside a ratchet. A declared host is attempted on no runner, so a failure only one runner sees is answered by `fails` and deadlocks nothing. On the macOS dev host, from an empty target directory, checking the 11 apps for aarch64-apple-darwin took 20 s and 443 MB, and building them 26 s and 1.2 GB, exit 0 each. A `fails` entry that outlives its fix keeps that host unjudged while it claims the app fails there. Attempting declared hosts, red when one builds, is not sound while two runners judge one host. For example, once doom's build.rs compiles with the host's C compiler, doom builds for macOS on the macOS runner and fails the Linux runner's check of macOS. Its `fails` entry would then be red whether it stays or goes. The gap is recorded where the other gaps of judging a host from another host's runner are. That issue was `no-app-is-built-for-a-host-each-is-only-checked`, and its title stopped being true when Linux's apps began to be built. It is now `a-hosts-apps-are-judged-on-other-hosts-runners`, and its exit attempts each declared host on that host's own runner. Deleted as the review asked: - "the same on whichever host runs it", which a build script that probes the host that checks makes false; - the served port in the exemptions of blockd, compositor, logd, netd and soundd. filepicker serves a port and is an app, so the device or SysCap claim alone carries each exemption; - "as the system servers do," in `userland/CLAUDE.md`, since filepicker is a system server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Answers to the review of 33f357f, at 207bdde. BLOCKERs
NOTEs
REMOVEs, all deleted:
The controls, as checked patches, each applied to 207bdde by the runners below, built, run and reverted, with the tree clean before and after. The review's mutation, diff --git a/src/ci.rs b/src/ci.rs
index cf6beba9c..f42980b34 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -608,7 +608,7 @@ fn attempted(programs: &[Program], os: Os) -> (Vec<&Program>, Vec<&str>) {
let (mut attempted, mut declared) = (Vec::new(), Vec::new());
for program in programs {
let Host::App(fails) = &program.host else { continue };
- if fails.contains(&os) {
+ if !fails.is_empty() {
declared.push(program.dir.as_str());
} else {
attempted.push(program);The new diff --git a/src/ci.rs b/src/ci.rs
index cf6beba9c..41e177902 100644
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -595,7 +595,7 @@ fn apps_for(
/// `build` where the gate runs on `os`'s own triple, and `check` elsewhere.
fn verb(os: Os, host_triple: &str) -> &'static str {
- if os.triple() == host_triple {
+ if os.triple() != host_triple {
"build"
} else {
"check"Round 2's controls ran again here, because --- a/src/ci.rs
+++ b/src/ci.rs
@@ -882,6 +882,19 @@
mod tests {
use super::*;
+ #[test]
+ fn measure_apps() {
+ let root = Path::new(env!("CARGO_MANIFEST_DIR"));
+ let programs = crate::userlandhost::programs(root).expect("programs");
+ let host = crate::toolchain::host_triple();
+ let verdicts: Vec<_> =
+ Os::ALL.into_iter().map(|os| (os, apps_for(root, &programs, os, &host))).collect();
+ for (os, verdict) in &verdicts {
+ eprintln!("MEASURE {os:?}: {verdict:?}");
+ }
+ assert!(verdicts.iter().all(|(_, v)| v.is_ok()), "a host is red");
+ }
+
/// A deterministic control on `host`'s `std::env::set_var("TMPDIR", ...)`:
/// delete that line and every child writes to the real `$TMPDIR` instead of
/// this job's private one, which never gains the lock a real step always
--- a/src/ci.rs
+++ b/src/ci.rs
@@ -608,7 +608,7 @@
let (mut attempted, mut declared) = (Vec::new(), Vec::new());
for program in programs {
let Host::App(fails) = &program.host else { continue };
- if fails.contains(&os) {
+ if false && fails.contains(&os) {
declared.push(program.dir.as_str());
} else {
attempted.push(program);The runners: #!/bin/bash
# Each mutation applied as a checked patch to the committed tree, built, the
# per-host test run, and reverted; the tree is shown clean before and after.
S=/private/tmp/claude-502/-Users-jan-Dev-jan-toyos/2280e09e-428b-4b81-bc00-1ede594b7247/scratchpad/r3
cd /Users/jan/Dev/jan/toyos-hostapps || exit 2
TEST=ci::tests::an_app_is_judged_for_every_host_its_fails_does_not_name
for m in m-skip-any m-verb-swapped; do
before=$(git status --porcelain | wc -l | tr -d ' ')
git apply --check "$S/$m.patch" && git apply "$S/$m.patch" || { echo "$m does not apply"; exit 3; }
cargo test --lib --no-run > "$S/$m-build.log" 2>&1; built=$?
if [ $built = 0 ]; then
cargo test --lib -- --exact "$TEST" > "$S/$m.log" 2>&1; rc=$?
else
rc=build-failed
fi
git apply -R "$S/$m.patch"
after=$(git status --porcelain | wc -l | tr -d ' ')
echo "$m: build EXIT=$built run EXIT=$rc dirty-before=$before dirty-after=$after"
grep -E "\.\.\. FAILED|panicked|left:|right:" "$S/$m.log"
done
cargo test --lib -- --exact "$TEST" > "$S/unmutated.log" 2>&1; echo "unmutated: run EXIT=$? dirty=$(git status --porcelain | wc -l | tr -d ' ')"
echo DONE#!/bin/bash
# Round 2's controls at this head: the end-to-end measurement under no mutation,
# console unexempted and the skip removed; then the five reader mutations. Each
# patch is checked, applied, built, run and reverted; the tree is shown clean.
R=/private/tmp/claude-502/-Users-jan-Dev-jan-toyos/2280e09e-428b-4b81-bc00-1ede594b7247/scratchpad
S=$R/r3
cd /Users/jan/Dev/jan/toyos-hostapps || exit 2
for case in baseline m1-console-unexempt m2-no-skip3; do
patches=("$S/measure3.patch")
[ "$case" = m1-console-unexempt ] && patches+=("$R/m1-console-unexempt.patch")
[ "$case" = m2-no-skip3 ] && patches+=("$S/m2-no-skip3.patch")
before=$(git status --porcelain | wc -l | tr -d ' ')
for p in "${patches[@]}"; do git apply --check "$p" && git apply "$p" || { echo "$case: $p does not apply"; exit 3; }; done
cargo test --lib --no-run > "$S/ctl-$case-build.log" 2>&1; built=$?
if [ $built = 0 ]; then
cargo test --lib measure_apps -- --nocapture > "$S/ctl-$case.log" 2>&1; rc=$?
else
rc=build-failed
fi
for p in "${patches[@]}"; do git apply -R "$p"; done
after=$(git status --porcelain | wc -l | tr -d ' ')
echo "$case: build EXIT=$built run EXIT=$rc dirty-before=$before dirty-after=$after"
grep "^MEASURE" "$S/ctl-$case.log"
done
for m in m3-userland-only m4-no-shape m5-no-status m6-any-case m7-members-only; do
before=$(git status --porcelain | wc -l | tr -d ' ')
git apply --check "$R/$m.patch" && git apply "$R/$m.patch" || { echo "$m does not apply"; exit 3; }
cargo test --lib --no-run > "$S/unit-$m-build.log" 2>&1; built=$?
if [ $built = 0 ]; then
cargo test --lib -- userlandhost every_modes_crates_and_init_ship > "$S/unit-$m.log" 2>&1; rc=$?
else
rc=build-failed
fi
git apply -R "$R/$m.patch"
after=$(git status --porcelain | wc -l | tr -d ' ')
echo "$m: build EXIT=$built run EXIT=$rc dirty-before=$before dirty-after=$after"
grep -E "\.\.\. FAILED" "$S/unit-$m.log"
done
echo DONETheir output: |
|
Review of 207bdde, round 3, against Round 2's BLOCKERs
BLOCKER None. NOTE
REMOVE
LAND AFTER NAMED CHANGES |
Two hunks conflict, and each keeps both sides' changes. - `implementer.md`: this branch drops the ABI-brief sentence, and #674's dependency sentence replaces "No new dependency.". - `reviewer.md`'s Fit line: this branch's two BLOCKER clauses stay, and #674's dependency clause replaces "No new dependency or fetch.". The rest of #674 merged clean. `git diff origin/main -- .claude userland/CLAUDE.md` shows only this branch's own changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
src/ci.rs conflicted twice, both sides additive, and both are kept
whole: the `use` of `cicache::{self, Start}` beside #674's
`userlandhost::{Host, Os, Program}`, and `carry()` beside #674's
`apps_for`, `verb` and `attempted`. The merged file's changed lines
against main are exactly the branch's, and against the branch exactly
main's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The owner's rule: an app builds and runs on Linux under Wayland, macOS and Windows from the same source as on ToyOS, and a host build that fails is fixed in the app or its dependencies. A program is ToyOS-only by nature when its job exists only on ToyOS: it owns ToyOS devices or kernel objects, or it manages ToyOS itself. calc broke the rule unseen. This PR writes the rule where it is enforced, and gates it.
What changed, per decision
What the gate judges is what the images ship.
build::shippedgainsprograms: itscrateswithout the kernel and the loader, from the sameconfig_crateswalk that the build and the licence gate read.userlandhost::programsreads each program's[package.metadata.toyos.host]:exempt.ownsorexempt.manages, with what and why: the program is ToyOS-only;fails = [<hosts>]withissue: an app that does not build on those hosts yet.Any other key or shape is refused by name. This takes in toyos-ld, which is outside the userland workspace. test-runner and metalprobe ship in no mode's image, so nothing reads a declaration of theirs, and they carry none.
An exemption names one of the owner's two cases.
exempt.owns: the program owns ToyOS devices or kernel objects. That is blockd, compositor, console, fsd, init, logd, netd and soundd. Each names the device, SysCap or kernel object it claims. A port it serves is no part of the exemption: filepicker serves one, and it is an app.exempt.manages: the program manages ToyOS itself. That is inspect, swap and update.userland/CLAUDE.mdand the reviewer's Hosts bullet say the same two cases.filepicker is an app. Its exemption went, and the gate judges it on every host, as it judges the other apps. Making it run on a host is not small:
toyos-windowreaches no display off ToyOS, as with editor, files and paint;filepickerport.issues/build/apps-that-build-or-run-on-toyos-alone.mdrecords both, with an exit.A host's apps are built where the gate runs on that host's triple, and checked elsewhere.
apps_forrunscargo build --target <triple>when the triple is the host's own, andcargo check --target <triple>otherwise. Either way it uses the features the image builds the app with.So two runners can give one host two different verdicts. That deadlocks nothing, because a declared host is attempted on no runner: a failure that only one runner sees is answered by
fails.A host an app's
failsnames is not attempted. So the PR gate never runspkg-config, which cpal'salsa-sysprobes on Linux. It never fetches doomgeneric either: doom'sbuild.rsfetches it before it readsCC_<target>. Nothing is declared for either.The cost is that a
failsentry that outlives its fix keeps that host unjudged. A ratchet would attempt the declared hosts and go red when one builds. While two runners judge one host, that ratchet deadlocks: a fix that passes on one runner and fails on the other is red whether its entry stays or goes.issues/build/a-hosts-apps-are-judged-on-other-hosts-runners.mdrecords this with the other gaps of judging a host from another host's runner. Its exit attempts each declared host on that host's own runner.The two per-host decisions are pure functions.
attemptedseparates the apps judged for a host from those whosefailsnames it.verbpicksbuildorcheck.an_app_is_judged_for_every_host_its_fails_does_not_namecovers both, with no cargo run.A
failsentry'sissueis work still owed. It must be a pathissues/<area>/<slug>.md, its front matter'sstatusmust beopenorassigned, and the file must name the app in backticks.socket2. ToyOSOrg/socket2
toyoscommit 0b238fb givesuse crate::MsgHdrMutthe same condition asrecvmsg, its only user. That condition is upstream'sall(unix, not(target_os = "redox")), with ToyOS named beside it.userland/Cargo.lockmoves its one socket2 entry to 0b238fb. No other lockfile or gitlink names that branch.Also:
fails = ["linux", "macos", "windows"]againstissues/build/apps-that-build-or-run-on-toyos-alone.md.implementer.mdand the reviewer's Fit bullet carry the dependency rule.orchestrator.mdgains one line under Judge.userland/README.mdis deleted.Gates
On the macOS dev host (arm64), at 207bdde:
cargo run -- --ci host: EXIT=0, "[ci] Host: 59 step(s), all green". The three apps steps read:cargo check --target x86_64-unknown-linux-gnu";cargo build --target aarch64-apple-darwin";cargo check --target x86_64-pc-windows-msvc".Each step adds "userland/doom, userland/proctest, userland/shell, userland/terminal, userland/toybox not attempted, as their manifests declare".
cargo test --lib -- ci::tests userlandhost every_modes_crates_and_init_ship: EXIT=0, 21 tests.cargo clippy -p toyos-build --all-targets -- -D warnings -W clippy::redundant_clone: EXIT=0.cargo run -- --build-only: EXIT=0, "Boot image: …/target/bootable.img".What the build costs over the check, from an empty target directory on the same host, for the 11 apps and aarch64-apple-darwin: checking took 20 s and 443 MB, and building took 26 s and 1.2 GB, exit 0 each.
cargo tree --target x86_64-unknown-linux-gnu -e normal,build -i alsa-sysgives EXIT=101 for each of the 11 apps the Linux runner builds, "did not match any packages". toybox, as the positive control, gives EXIT=0, withalsa-sysunderalsaandcpal.Measured on socket2 alone at 0b238fb:
RUSTFLAGS="-D warnings -A unexpected_cfgs" cargo check --all-featuresgives EXIT=0 for x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu and aarch64-apple-darwin.unexpected_cfgsis allowed because stable rustc knows notoyostarget and warns on every arm that names one.Negative controls
Each mutation below is a checked patch. It was applied, built (
cargo test --lib --no-run, EXIT=0), run and reverted at 207bdde, andgit statuswas clean before and after. The patches and the runners are posted as a comment on this PR.The per-host decisions,
cargo test --lib -- --exact ci::tests::an_app_is_judged_for_every_host_its_fails_does_not_name:attempted,if fails.contains(&os)becomesif !fails.is_empty(): EXIT=101, "left: (["calc"], ["doom"]) right: (["calc", "doom"], [])";verb,==becomes!=: EXIT=101, "left: ["check", "build", "build"] right: ["build", "check", "check"]".The gate end to end. A measurement test calls
apps_forfor each host on the real tree, with this host's triple:exempt.owns: EXIT=101. It is red on all three hosts with "userland/console fails for and declares neitherfailsthere norexempt: cargo check … exited exit status: 101". For macOS the message readscargo build.failsskip removed (if false && fails.contains(&os)): EXIT=101, red for doom, proctest, shell, terminal and toybox on all three hosts.The reader,
cargo test --lib -- userlandhost every_modes_crates_and_init_ship, each run EXIT=101:programskeeps only programs underuserland/:every_program_the_images_ship_declares_what_it_is_to_a_hostfails, with toyos-ld on one side only;Shipped::programstakes workspace members alone:every_modes_crates_and_init_shipfails;a_host_declaration_is_read_whole_and_refused_by_namefails onissue = "notes.md";issues/build/asked.md,status: owner;exempt.serves.Independent oracle: cargo's own exit for each app on each host's triple, and rustc's own warning for the fork. The gate reads nothing else but the manifests.
Unsure
issues/build/a-hosts-apps-are-judged-on-other-hosts-runners.md.pkg-configcannot pass a check of another host's triple. Today only doom and toybox are like that, and both are declared failing on all three hosts. Recorded in the same issue.failsentry that has gone stale is not caught. Recorded in the same issue, with the reason a ratchet is unsound while two runners judge one host.cfgthat compiles an app's work out of a host checks green there. That was the calc incident's first shape. Only review holds it (.claude/agents/reviewer.md, Hosts), and the module doc says so.issues/build/the-build-system-does-not-compile-on-windows.mdhas no owner and no exit, and holds an owner's question. It is not this PR's file. Until it changes, the new issue's Windows exit and doom'swindowsentry have no reachable end.Size
Net lines,
git diff --shortstat origin/main...HEAD: +512 −38 across 28 files.src/userlandhost.rs: +184 −2, of which 31 lines are module doc.src/ci.rs: +81 −1.src/build.rs: +10 −2.userland/README.mdincluded.🤖 Generated with Claude Code
https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L