Support Cargo scaffolding and human output, and find boolean expressions

This commit is contained in:
2026-11-11 21:57:37 +00:00
parent c7606b8ae9
commit 652ea7960f
7 changed files with 267 additions and 116 deletions
+5 -1
View File
@@ -7,7 +7,11 @@ The response contains newline-delimited JSON events.
The final `exit` event contains a code and component locations.
Each location contains an executable path and working directory.
The CLI forwards the bytes immediately.
The CLI forwards human output immediately.
The builder captures compiler JSON for artifact discovery without forwarding it to stdout.
Compiler diagnostics remain on stderr. `cargo run` reserves stdout for the program.
`cargo new` and `cargo init` use native Cargo, including `--lib`, `--name`, and `--edition`.
They need no existing manifest. Subsequent builds use the world's default WASI target.
For `test` and `run`, it executes the artifacts through the process interface.
Cargo selects run binaries through a capture runner, so `--bin` and `default-run` retain their normal behavior.
The test runner uses each package's root directory, even when the caller runs Cargo from a subdirectory.
+21 -9
View File
@@ -66,7 +66,7 @@ impl Builder {
ensure!(
[
"build", "check", "clippy", "test", "run", "fmt", "fetch", "add", "remove",
"update", "tree", "metadata", "clean"
"update", "tree", "metadata", "clean", "new", "init"
]
.contains(&action),
"unsupported Cargo command"
@@ -96,7 +96,14 @@ impl Builder {
.checked_add(Duration::from_secs(seconds))
.context("timeout too large")?;
let world_cwd = sausage_vfs::resolve("/", &request.cwd);
let workspace = files::workspace(self.root.as_ref(), &world_cwd).await?;
let scaffold = matches!(action, "new" | "init");
let workspace = if scaffold {
files::workspace(self.root.as_ref(), &world_cwd)
.await
.unwrap_or_else(|_| world_cwd.clone())
} else {
files::workspace(self.root.as_ref(), &world_cwd).await?
};
let world_target = sausage_vfs::resolve(&workspace, "target");
use std::hash::{Hash, Hasher};
let mut hash = std::collections::hash_map::DefaultHasher::new();
@@ -140,10 +147,10 @@ impl Builder {
fetch.arg("fetch");
}
fetch.current_dir(&cwd).env("CARGO_HOME", &registry);
let mut code = if matches!(action, "clean" | "fmt" | "remove") {
let mut code = if scaffold || matches!(action, "clean" | "fmt" | "remove") {
0
} else {
run(fetch, tx, deadline).await?.0
run(fetch, tx, deadline, false).await?.0
};
if code == 0 && action == "add" {
let mut fetch = Command::new("cargo");
@@ -151,7 +158,7 @@ impl Builder {
.arg("fetch")
.current_dir(&cwd)
.env("CARGO_HOME", &registry);
code = run(fetch, tx, deadline).await?.0;
code = run(fetch, tx, deadline, false).await?.0;
}
let mut executables = Vec::new();
if code == 0 {
@@ -191,7 +198,7 @@ impl Builder {
)?;
let mut startup = Command::new("nu");
startup.arg(tools.join("build.nu"));
let (startup_code, _) = run(startup, tx, deadline).await?;
let (startup_code, _) = run(startup, tx, deadline, false).await?;
if startup_code != 0 {
files::copy_back(self.root.as_ref(), &source, &workspace, Some(&before)).await?;
self.evict(20 * 1024 * 1024 * 1024)?;
@@ -303,7 +310,7 @@ impl Builder {
]);
}
cmd.args(&args[split..]);
let (status, stdout) = run(cmd, tx, deadline).await?;
let (status, stdout) = run(cmd, tx, deadline, compiles).await?;
code = status;
if action == "clean" && code == 0 {
files::clean_artifacts(self.root.as_ref(), &target, &world_target).await?;
@@ -439,6 +446,7 @@ async fn pump(
mut reader: impl tokio::io::AsyncRead + Unpin,
tx: &mpsc::Sender<Event>,
stderr: bool,
silent_stdout: bool,
) -> Result<Vec<u8>> {
let mut all = Vec::new();
let mut bytes = vec![0; 8192];
@@ -450,6 +458,9 @@ async fn pump(
if !stderr {
all.extend_from_slice(&bytes[..n]);
}
if !stderr && silent_stdout {
continue;
}
tx.send(if stderr {
Event::Stderr {
bytes: bytes[..n].to_vec(),
@@ -466,6 +477,7 @@ async fn run(
mut cmd: Command,
tx: &mpsc::Sender<Event>,
deadline: Instant,
silent_stdout: bool,
) -> Result<(i32, Vec<u8>)> {
cmd.stdin(Stdio::null())
.stdout(Stdio::piped())
@@ -479,8 +491,8 @@ async fn run(
let collect = async {
let (status, out, _) = tokio::try_join!(
async { Ok::<_, anyhow::Error>(child.wait().await?) },
pump(out, tx, false),
pump(err, tx, true)
pump(out, tx, false, silent_stdout),
pump(err, tx, true, false)
)?;
Ok::<_, anyhow::Error>((status.code().unwrap_or(1), out))
};
+6 -6
View File
@@ -191,10 +191,9 @@ async fn timeout_kills_before_output_can_drain() -> Result<()> {
let output = tx.clone();
let mut cmd = Command::new("sh");
cmd.args(["-c", "echo $$; exec sleep 1000"]);
let task =
tokio::spawn(
async move { run(cmd, &output, Instant::now() + Duration::from_secs(1)).await },
);
let task = tokio::spawn(async move {
run(cmd, &output, Instant::now() + Duration::from_secs(1), false).await
});
let Some(Event::Stdout { bytes }) =
tokio::time::timeout(Duration::from_secs(2), rx.recv()).await?
else {
@@ -219,8 +218,9 @@ async fn closed_receiver_cancels_command() -> Result<()> {
let (tx, mut rx) = mpsc::channel(1);
let mut cmd = Command::new("sh");
cmd.args(["-c", "printf ready; exec sleep 1000"]);
let task =
tokio::spawn(async move { run(cmd, &tx, Instant::now() + Duration::from_secs(60)).await });
let task = tokio::spawn(async move {
run(cmd, &tx, Instant::now() + Duration::from_secs(60), false).await
});
// Output must arrive before the command exits, not after collection completes.
let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
.await?
+75
View File
@@ -54,6 +54,81 @@ async fn cargo_at(
.await?;
Ok((code, String::from_utf8_lossy(&bytes).into_owned()))
}
#[tokio::test(flavor = "multi_thread")]
async fn cargo_scaffold_and_human_output() -> anyhow::Result<()> {
let binaries = programs();
let temp = tempfile::tempdir()?;
let world = World::open(temp.path())?;
let bin = temp.path().join("world/bin");
std::fs::create_dir_all(&bin)?;
std::fs::copy(binaries.join("cargo.wasm"), bin.join("cargo.wasm"))?;
std::fs::copy(binaries.join("brush.wasm"), bin.join("sh.wasm"))?;
let runtime = Runtime::new(world)?;
for (script, cwd, expected, success) in [
(
"cargo new hello && cd hello && cargo run",
"/",
"Hello, world!\n",
true,
),
("cargo build", "/hello", "", true),
(
"cargo new library --lib --name named_lib --edition 2021",
"/",
"",
true,
),
("cargo check", "/library", "", true),
(
"cargo init --lib --name initialized --edition 2024",
"/empty",
"",
true,
),
("cargo check", "/empty", "", true),
("cargo new broken --edition 1900", "/", "", false),
] {
std::fs::create_dir_all(temp.path().join("world/empty"))?;
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let code = runtime
.exec(
Command::Argv(vec!["/bin/sh.wasm".into(), "-c".into(), script.into()]),
Options {
cwd: cwd.into(),
timeout: Some(Duration::from_secs(120)),
..Default::default()
},
&mut |stream, bytes| match stream {
sausage::exec::Stream::Stdout => stdout.extend_from_slice(bytes),
sausage::exec::Stream::Stderr => stderr.extend_from_slice(bytes),
},
)
.await?;
let stderr = String::from_utf8_lossy(&stderr);
assert_eq!(code == 0, success, "{script}: {stderr}");
assert_eq!(
String::from_utf8_lossy(&stdout),
expected,
"{script}: {stderr}"
);
assert!(
!stderr.contains("\"reason\":\"compiler-artifact\""),
"{stderr}"
);
if script == "cargo build" {
assert!(stderr.contains("Finished"), "{stderr}");
}
}
let root = temp.path().join("world");
let manifest: toml::Value =
toml::from_str(&std::fs::read_to_string(root.join("library/Cargo.toml"))?)?;
assert_eq!(manifest["package"]["name"].as_str(), Some("named_lib"));
assert_eq!(manifest["package"]["edition"].as_str(), Some("2021"));
assert!(root.join("library/src/lib.rs").is_file());
assert!(root.join("empty/src/lib.rs").is_file());
Ok(())
}
fn cache_hits(base: &Path) -> anyhow::Result<u64> {
let stats = std::process::Command::new("kache")
.arg("stats")