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
3 changes: 2 additions & 1 deletion .claude/agents/implementer.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ Fork sources live outside this repository: a search for callers must also cover
`git commit -F <file>`, never `-m`. No `--amend`, no rebase, no force: merge `origin/main`, never
rebase onto it. Never run `git submodule` in a linked worktree: it writes `core.worktree` into the
fork's shared config and breaks git in the primary checkout's `rust/`. Never touch `toyos-abi/src`, `toyos/src` or `userland/libc/src` unless the brief is
an ABI brief. No new dependency.
an ABI brief. A new dependency is taken where it is the cleanest path: a general, widely used
crate (root `CLAUDE.md`, "Dependencies"), and the pull request says why.

Push from your branch, never `main`, with `git status --porcelain` empty: `git push -u origin
<branch>`, and `gh pr create --draft` at the first push. The pull request body is the handoff the reviewer reads,
Expand Down
3 changes: 2 additions & 1 deletion .claude/agents/orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ a poll loop. Every agent's transcript records its usage: a claim about cost is r

The reviewer reports, you judge, and a judge who upholds everything is not judging. Only a BLOCKER
sends a branch back. When a reviewer and an implementer disagree, ask for the measurement that
settles it and decide.
settles it and decide. Before ruling on a "blocked" report, read the code and check it against the
product's principles; never change what a product is to make a check green.

## Land

Expand Down
11 changes: 9 additions & 2 deletions .claude/agents/reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,21 @@ above; otherwise it is a NOTE.
- **Fit.** Does the tree already do this? Is each new thing where it belongs: a pure decision in a
pure crate, the user/kernel boundary in `toyos-userbound`, a device claim in a userland server?
One declaration read by every reader, refusal by name, authority moved in by the parent. Zero
legacy: no shim, no workaround, no silent default. No new
dependency or fetch. Nothing outside the brief's fence.
legacy: no shim, no workaround, no silent default. A new dependency only where it is the
cleanest path, a general and widely used crate the pull request says why it takes; no new
fetch. Nothing outside the brief's fence.
Assembly, a naked function and a `core::arch` or `std::arch` path live only in an
architecture's own module; `target_arch` only there, in its selector, in `src/arch.rs` and in
`src/licence.rs`, which evaluates a dependency's `cfg` as data; `arch::x86_64` and
`arch::aarch64` in no generic kernel code; and none of them in a crate whose manifest
`description` says pure.
A `4096` in the kernel that means a page is a private copy of `mm::PAGE_SIZE`.
- **Hosts.** A BLOCKER: a change that makes an app build on fewer of Linux under Wayland, macOS
and Windows, or answers a host build failure by making the app ToyOS-only, by a split, a `cfg`
that compiles what it does out of a host or an `exempt` in its manifest, instead of fixing it in
the app or its dependencies. `exempt` is for a program whose job exists only on ToyOS: it owns
ToyOS devices or kernel objects, or it manages ToyOS itself. An X11 backend is legacy, and a
BLOCKER too.
- **Instructions.** A change that removes or renames a command, flag or step an agent runs updates
every prompt that names it — each `CLAUDE.md` and `.claude/agents/*.md` — in the same diff,
saying what to do instead. Such an instruction is not the prose "Prose is removed, never
Expand Down
33 changes: 33 additions & 0 deletions issues/build/a-hosts-apps-are-judged-on-other-hosts-runners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
status: open
kind: tooling
opened: 2026-10-01
---

# A host's apps are judged on other hosts' runners

`cargo run -- --ci host` builds each host's apps where it runs on that host's
triple, and checks them against it elsewhere (`src/userlandhost.rs`). `ci.yml`
runs it on Linux alone and the nightly on Linux and macOS, so on a pull
request macOS's and Windows's apps are only checked, and Windows's are built
nowhere. These go unseen:

- on a host whose apps are only checked, a link failure, and an error only code
generation raises;
- a build script that answers differently on the host it is judged for: one
that runs `cc` or `pkg-config` probes the host that checks, so an app that
compiles C or links a system library cannot pass a check of another host's
triple;
- a declared failure that outlives its fix. A host an app's `fails` names is
attempted on no runner, so the declaration keeps that host unjudged while it
claims the app fails there. Attempting it, red when the app builds, is sound
only where one runner judges the host: while two do, a fix that passes on one
and fails on the other is red whether its `fails` entry stays or goes.

Owner: the host gate, `src/userlandhost.rs` and `src/ci.rs`'s `apps_for`.

**Exit:** on each pull request, a runner of each host builds that host's apps,
judges no other host's, and attempts each app whose `fails` names its host, red
when one builds. Windows waits on
`issues/build/the-build-system-does-not-compile-on-windows.md`, macOS on a
macOS runner in `ci.yml`.

This file was deleted.

31 changes: 31 additions & 0 deletions issues/build/apps-that-build-or-run-on-toyos-alone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
status: open
kind: defect
opened: 2026-10-01
---

# Apps that build or run on ToyOS alone

An app builds and runs on Linux under Wayland, macOS and Windows from the same
source as on ToyOS (`userland/CLAUDE.md`). These build on none of the three:
each manifest's `[package.metadata.toyos.host] fails` names all three, so
`cargo run -- --ci host` checks none of them there.

| app | what stops it |
|---|---|
| `doom` | its `build.rs` compiles doomgeneric's C with the compiler the build system names in `CC_<target>`, and panics without one; a check of another host's triple has no C library for it. On Linux, cpal's `alsa-sys` links ALSA's C library, found through `pkg-config`, and neither is a declared host tool (`issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md`) |
| `toybox` | `std::os::toyos` in its `stats` and `locale` applets; on Linux, cpal's `alsa-sys`, as doom |
| `terminal` | `std::os::toyos`: it starts its shell with `CommandExt::provide` |
| `shell` | `std::os::toyos`: raw stdin, and `CommandExt::provide` to hand each program the connectors the shell was given |
| `proctest` | `std::os::toyos::io::set_stdin_raw`, and it spawns `/system/bin/echo` and itself by their ToyOS paths |

`editor`, `files`, `paint` and `filepicker` build on all three and run on none:
`toyos-window` reaches no display but ToyOS's compositor, because
`toyos-abi/src/syscall.rs` issues ToyOS's `syscall` on every OS. A program
reaches `filepicker` only through the `filepicker` port, which
`filepicker-api` opens with `toyos::endow`, so off ToyOS a pick needs a
transport the same source carries.

**Exit:** no manifest names this file, and each app above opens its window and
works on each host. The gate reads the first; nobody but a person on each host
judges the second.
15 changes: 13 additions & 2 deletions src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,8 @@ impl Boot {
/// is a test image and is not here.
pub struct Shipped {
pub crates: BTreeSet<(PathBuf, Features)>,
/// `crates` but the kernel and the loader: every program an image runs.
pub programs: BTreeSet<(PathBuf, Features)>,
pub assets: BTreeSet<PathBuf>,
}

Expand All @@ -1096,6 +1098,7 @@ pub struct Shipped {
/// the rust fork's `compiler/` workspace, which no reader of this answer walks.
pub fn shipped(root: &Path) -> Result<Shipped, String> {
let mut crates = BTreeSet::new();
let mut programs = BTreeSet::new();
let mut assets = BTreeSet::new();
for boot in [Boot::shipped(root), Boot::diag(root), Boot::console(root)] {
let config = parse_config(&boot.config);
Expand All @@ -1105,10 +1108,15 @@ pub fn shipped(root: &Path) -> Result<Shipped, String> {
boot.config.display()
));
}
crates.extend(config_crates(root, &config).into_iter().map(|c| (c.dir, c.features)));
for c in config_crates(root, &config) {
if matches!(c.built, Built::Member | Built::Standalone) {
programs.insert((c.dir.clone(), c.features));
}
crates.insert((c.dir, c.features));
}
assets.extend(config.assets.iter().map(|dir| root.join(dir)));
}
Ok(Shipped { crates, assets })
Ok(Shipped { crates, programs, assets })
}

/// The parameters an image built for flashing may carry: the kernel's own boot
Expand Down Expand Up @@ -2251,6 +2259,9 @@ mod tests {
shipped.crates
);
}
let (kernel, loader) = (root.join("kernel"), root.join("bootloader"));
let programs = shipped.crates.iter().filter(|(dir, _)| *dir != kernel && *dir != loader);
assert_eq!(shipped.programs, programs.cloned().collect());
}

/// **A standalone crate's clean takes all its guest build wrote — the
Expand Down
107 changes: 106 additions & 1 deletion src/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;

use crate::arch::{Accel, Arch};
use crate::userlandhost::{Host, Os, Program};
use crate::{flags, release, sdkversion, sync};

const USAGE: &str = "cargo run -- --ci <job>, where <job> is one of:
Expand Down Expand Up @@ -452,7 +453,7 @@ fn run_control(root: &Path, control: &Control) -> Result<String, String> {
/// member of the host workspace, clippy with warnings denied, the concurrency
/// models' negative controls, every userland crate with a host test
/// ([`crate::userlandhost`], which also reds on a userland test none of them
/// runs), and the SDK.
/// runs), every app the images ship for each host ([`apps_for`]), and the SDK.
///
/// **Every step runs against a `$TMPDIR` of this job's own, and the last step
/// reds on anything left in it** but the lock `toyos_tmpdir` keeps there: a test
Expand Down Expand Up @@ -528,6 +529,16 @@ fn host(root: &Path) -> Vec<Step> {
}
Err(why) => steps.push(Step { label: "the userland host crates".into(), verdict: Err(why) }),
}
match crate::userlandhost::programs(root) {
Ok(programs) => {
for os in Os::ALL {
steps.push(step(&format!("the apps for {}", os.name()), || {
apps_for(root, &programs, os, &host_triple)
}));
}
}
Err(why) => steps.push(Step { label: "the apps".into(), verdict: Err(why) }),
}
// The SDK compiles against the ToyOS sysroot everywhere but here, and this
// build links no syscall.
steps.push(step("the toyos SDK", || {
Expand All @@ -537,6 +548,75 @@ fn host(root: &Path) -> Vec<Step> {
steps
}

/// Every app the images ship, judged for `os` with the features its image
/// builds it with ([`crate::userlandhost`]).
///
/// One cargo per app: features unify across the packages of one invocation, and
/// an app that builds only beside another's features is what this gate is for.
fn apps_for(
root: &Path,
programs: &[Program],
os: Os,
host_triple: &str,
) -> Result<String, String> {
let triple = os.triple();
let status = Command::new("rustup")
.args(["target", "add", triple])
.status()
.map_err(|e| format!("rustup: {e}"))?;
if !status.success() {
return Err(format!("rustup target add {triple} exited {status}"));
}
let verb = verb(os, host_triple);
let (attempted, declared) = attempted(programs, os);
let mut red = Vec::new();
for program in &attempted {
let manifest = format!("{}/Cargo.toml", program.dir);
let mut args = vec![verb, "--manifest-path", manifest.as_str(), "--target", triple];
args.extend(program.features.args());
if let Err(exit) = cargo(root, &args) {
red.push(format!(
"{} fails for {} and declares neither `fails` there nor `exempt`: {exit}",
program.dir,
os.name()
));
}
}
if !red.is_empty() {
return Err(red.join("; "));
}
let said = format!("{} app(s) pass `cargo {verb} --target {triple}`", attempted.len());
if declared.is_empty() {
Ok(said)
} else {
Ok(format!("{said}; {} not attempted, as their manifests declare", declared.join(", ")))
}
}

/// `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 {
"build"
} else {
"check"
}
}

/// The apps judged for `os`, and those whose manifests declare they fail there;
/// an exempt program is in neither.
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) {
declared.push(program.dir.as_str());
} else {
attempted.push(program);
}
}
(attempted, declared)
}

/// What `tmp` holds but the lock `toyos_tmpdir` keeps in it, and every root
/// under `short` whose process is gone that `before` does not name.
///
Expand Down Expand Up @@ -852,6 +932,31 @@ mod tests {
line.split_whitespace().map(String::from).collect()
}

/// **An app is judged for every host its `fails` does not name**, built
/// where the gate runs on that host and checked elsewhere; an exempt
/// program is judged for none.
#[test]
fn an_app_is_judged_for_every_host_its_fails_does_not_name() {
let program = |dir: &str, host| Program {
dir: dir.into(),
features: crate::build::Features::Default,
host,
};
let programs = [
program("calc", Host::App(Vec::new())),
program("doom", Host::App(vec![Os::Windows])),
program("init", Host::Exempt),
];
let judged = |os| {
let (attempted, declared) = attempted(&programs, os);
(attempted.iter().map(|p| p.dir.as_str()).collect::<Vec<_>>(), declared)
};
assert_eq!(judged(Os::Linux), (vec!["calc", "doom"], vec![]));
assert_eq!(judged(Os::Macos), (vec!["calc", "doom"], vec![]));
assert_eq!(judged(Os::Windows), (vec!["calc"], vec!["doom"]));
assert_eq!(Os::ALL.map(|os| verb(os, Os::Linux.triple())), ["build", "check", "check"]);
}

#[test]
fn a_job_is_named_and_takes_nothing_after_it() {
assert_eq!(parse(&words("host")), Ok(Job::Host));
Expand Down
Loading
Loading