Support Cargo scaffolding and human output, and find boolean expressions
This commit is contained in:
@@ -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
@@ -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", ®istry);
|
||||
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", ®istry);
|
||||
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))
|
||||
};
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user