builder: preserve forwarded arguments and clean world artifacts
This commit is contained in:
@@ -4,6 +4,8 @@ The world `cargo` sends `POST http://host.sausage/cargo` with `argv`, `cwd`, and
|
||||
The response contains newline-delimited JSON events.
|
||||
`stdout` and `stderr` events contain byte arrays. The final `exit` event contains a code and executable paths.
|
||||
The CLI forwards the bytes immediately. For `test` and `run`, it executes the artifacts through the process interface.
|
||||
For `test` and `run`, arguments after `--` belong to the executable, not the host build.
|
||||
Other commands retain their `--` arguments, such as `fmt -- --check` and Clippy lint options.
|
||||
|
||||
The host finds the Cargo workspace above the requested directory and copies its sources through the shared VFS.
|
||||
It excludes `target` and `.git`. Each workspace owns a source copy, target directory, registry, and kache cache.
|
||||
@@ -15,6 +17,7 @@ The PID namespace removes compiler descendants and the private kache daemon afte
|
||||
|
||||
The builder copies changed and new source files back through the VFS.
|
||||
It publishes WASI executables into the world's workspace target directory.
|
||||
After `cargo clean`, it removes the published artifacts that Cargo removed from the native target.
|
||||
Every target build links the shared cwd-init object, including its numeric exit-code interface.
|
||||
The command deadline defaults to 30 minutes. A closed response cancels the native command and its process group.
|
||||
The host retains at most 20 GiB of build directories and evicts the oldest workspace first.
|
||||
|
||||
@@ -113,6 +113,25 @@ pub async fn copy_back(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove published artifacts that Cargo clean removed from the native target.
|
||||
pub async fn clean_artifacts(root: &dyn Layer, native: &Path, world: &str) -> Result<()> {
|
||||
let entries = match root.list(world).await {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
for entry in entries {
|
||||
let path = sausage_vfs::resolve(world, &entry.name);
|
||||
let native = native.join(&entry.name);
|
||||
if entry.directory {
|
||||
Box::pin(clean_artifacts(root, &native, &path)).await?;
|
||||
} else if entry.name.ends_with(".wasm") && !native.exists() {
|
||||
root.remove_file(&path).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn size(path: &Path) -> Result<u64> {
|
||||
let mut total = 0;
|
||||
for entry in std::fs::read_dir(path)? {
|
||||
|
||||
+18
-5
@@ -66,6 +66,7 @@ impl Builder {
|
||||
.context("timeout too large")?;
|
||||
let world_cwd = sausage_vfs::resolve("/", &request.cwd);
|
||||
let workspace = 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();
|
||||
workspace.hash(&mut hash);
|
||||
@@ -223,7 +224,9 @@ impl Builder {
|
||||
if action == "run" {
|
||||
args[0] = "build".into();
|
||||
}
|
||||
if let Some(index) = args.iter().position(|a| a == "--") {
|
||||
if matches!(action, "run" | "test")
|
||||
&& let Some(index) = args.iter().position(|a| a == "--")
|
||||
{
|
||||
args.truncate(index);
|
||||
}
|
||||
if action == "test" {
|
||||
@@ -233,7 +236,8 @@ impl Builder {
|
||||
args.push("--no-run".into());
|
||||
}
|
||||
}
|
||||
cmd.args(args);
|
||||
let split = args.iter().position(|a| a == "--").unwrap_or(args.len());
|
||||
cmd.args(&args[..split]);
|
||||
if ["build", "check", "clippy", "test", "run"].contains(&action) {
|
||||
cmd.args([
|
||||
"--target",
|
||||
@@ -241,21 +245,30 @@ impl Builder {
|
||||
"--message-format=json-render-diagnostics",
|
||||
]);
|
||||
}
|
||||
cmd.args(&args[split..]);
|
||||
let (status, stdout) = run(cmd, tx, deadline).await?;
|
||||
code = status;
|
||||
if action == "clean" && code == 0 {
|
||||
files::clean_artifacts(self.root.as_ref(), &target, &world_target).await?;
|
||||
}
|
||||
let artifacts = target.join("wasm32-wasip2");
|
||||
if artifacts.exists() {
|
||||
files::copy_back(
|
||||
self.root.as_ref(),
|
||||
&artifacts,
|
||||
&format!("{workspace}/target/wasm32-wasip2"),
|
||||
&format!("{world_target}/wasm32-wasip2"),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if code == 0
|
||||
&& matches!(action, "test" | "run")
|
||||
&& !request.argv.iter().any(|a| a == "--no-run")
|
||||
&& !(action == "test"
|
||||
&& request
|
||||
.argv
|
||||
.iter()
|
||||
.take_while(|a| a.as_str() != "--")
|
||||
.any(|a| a == "--no-run"))
|
||||
{
|
||||
for line in stdout.split(|b| *b == b'\n') {
|
||||
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(line)
|
||||
@@ -264,7 +277,7 @@ impl Builder {
|
||||
&& let Some(path) = value["executable"].as_str()
|
||||
&& let Ok(relative) = Path::new(path).strip_prefix(&target)
|
||||
{
|
||||
executables.push(format!("{workspace}/target/{}", relative.display()));
|
||||
executables.push(format!("{world_target}/{}", relative.display()));
|
||||
}
|
||||
}
|
||||
executables.sort();
|
||||
|
||||
@@ -32,6 +32,35 @@ async fn discovers_workspace_and_skips_target() -> Result<()> {
|
||||
assert!(!copy.join("target").exists());
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn clean_removes_only_deleted_artifacts() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let world = crate::world::World::open(temp.path())?;
|
||||
let native = temp.path().join("native");
|
||||
std::fs::create_dir_all(&native)?;
|
||||
std::fs::write(native.join("keep.wasm"), b"keep")?;
|
||||
for name in ["keep.wasm", "remove.wasm", "note.txt"] {
|
||||
files::write(
|
||||
world.root.as_ref(),
|
||||
&format!("/target/{name}"),
|
||||
b"data".to_vec(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
files::clean_artifacts(world.root.as_ref(), &native, "/target").await?;
|
||||
assert!(
|
||||
files::read(world.root.as_ref(), "/target/remove.wasm")
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
for name in ["keep.wasm", "note.txt"] {
|
||||
assert_eq!(
|
||||
files::read(world.root.as_ref(), &format!("/target/{name}")).await?,
|
||||
b"data"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn relative_host_directory_uses_absolute_namespace_paths() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
@@ -69,17 +98,21 @@ fn disk_cap_removes_workspaces() -> Result<()> {
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn closed_receiver_cancels_command() -> Result<()> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
drop(rx);
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
let mut cmd = Command::new("sh");
|
||||
cmd.args(["-c", "exec sleep 1000"]);
|
||||
assert!(
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
run(cmd, &tx, Instant::now() + Duration::from_secs(60))
|
||||
)
|
||||
cmd.args(["-c", "printf ready; exec sleep 1000"]);
|
||||
let task =
|
||||
tokio::spawn(async move { run(cmd, &tx, Instant::now() + Duration::from_secs(60)).await });
|
||||
// Output must arrive before the command exits, not after collection completes.
|
||||
let event = tokio::time::timeout(Duration::from_secs(2), rx.recv())
|
||||
.await?
|
||||
.is_err()
|
||||
.context("missing streamed output")?;
|
||||
assert!(matches!(event, Event::Stdout { bytes } if bytes == b"ready"));
|
||||
drop(rx);
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_secs(2), task)
|
||||
.await??
|
||||
.is_err()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+27
-10
@@ -64,17 +64,19 @@ fn cache_hits(base: &Path) -> anyhow::Result<u64> {
|
||||
.arg("stats")
|
||||
.env("KACHE_CACHE_DIR", base.join("kache"))
|
||||
.output()?;
|
||||
assert!(stats.status.success());
|
||||
assert!(
|
||||
stats.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&stats.stderr)
|
||||
);
|
||||
let text = String::from_utf8_lossy(&stats.stdout);
|
||||
println!("PRIVATE CACHE: {text}");
|
||||
let hits = text
|
||||
.lines()
|
||||
.find(|line| line.contains("hits"))
|
||||
.and_then(|line| {
|
||||
line.split_whitespace()
|
||||
.find_map(|word| word.replace(',', "").parse::<u64>().ok())
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let words: Vec<_> = text.split_whitespace().collect();
|
||||
let hits = words.windows(2).find_map(|pair| {
|
||||
(pair[1] == "hits")
|
||||
.then(|| pair[0].replace(',', "").parse::<u64>().ok())
|
||||
.flatten()
|
||||
});
|
||||
assert!(
|
||||
std::process::Command::new("kache")
|
||||
.args(["daemon", "stop"])
|
||||
@@ -82,7 +84,7 @@ fn cache_hits(base: &Path) -> anyhow::Result<u64> {
|
||||
.status()?
|
||||
.success()
|
||||
);
|
||||
Ok(hits)
|
||||
Ok(hits.expect("kache stats must report its lifetime hits"))
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn cargo_build_test_and_source_changes() -> anyhow::Result<()> {
|
||||
@@ -145,6 +147,9 @@ async fn cargo_build_test_and_source_changes() -> anyhow::Result<()> {
|
||||
let (code, out) = cargo(&runtime, &["run"]).await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(out.contains("builder fixture cwd: /project"), "{out}");
|
||||
let (code, out) = cargo(&runtime, &["run", "--", "--no-run"]).await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(out.contains("builder fixture cwd: /project"), "{out}");
|
||||
let (code, out) = cargo(&runtime, &["run", "--", "--exit"]).await?;
|
||||
assert_eq!(code, 23, "{out}");
|
||||
let (code, out) = cargo(&runtime, &["test", "pass", "--", "--nocapture"]).await?;
|
||||
@@ -161,6 +166,9 @@ async fn cargo_build_test_and_source_changes() -> anyhow::Result<()> {
|
||||
let (code, out) = cargo(&runtime, &[action]).await?;
|
||||
assert_eq!(code, 0, "{action}: {out}");
|
||||
}
|
||||
let (code, out) = cargo(&runtime, &["clippy", "--", "-D", "clippy::print_stdout"]).await?;
|
||||
assert_ne!(code, 0, "{out}");
|
||||
assert!(out.contains("stdout"), "{out}");
|
||||
let base = std::fs::read_dir(temp.path().join("host/builds"))?
|
||||
.next()
|
||||
.unwrap()?
|
||||
@@ -168,6 +176,11 @@ async fn cargo_build_test_and_source_changes() -> anyhow::Result<()> {
|
||||
let before = cache_hits(&base)?;
|
||||
let (code, out) = cargo(&runtime, &["clean"]).await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(
|
||||
!source
|
||||
.join("target/wasm32-wasip2/debug/builder-fixture.wasm")
|
||||
.exists()
|
||||
);
|
||||
let (code, out) = cargo(&runtime, &["build"]).await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
let after = cache_hits(&base)?;
|
||||
@@ -180,6 +193,10 @@ async fn cargo_build_test_and_source_changes() -> anyhow::Result<()> {
|
||||
source.join("src/main.rs"),
|
||||
"fn main(){println!(\"format me\");}\n",
|
||||
)?;
|
||||
let unformatted = std::fs::read(source.join("src/main.rs"))?;
|
||||
let (code, out) = cargo(&runtime, &["fmt", "--", "--check"]).await?;
|
||||
assert_ne!(code, 0, "{out}");
|
||||
assert_eq!(std::fs::read(source.join("src/main.rs"))?, unformatted);
|
||||
let (code, out) = cargo(&runtime, &["fmt"]).await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(std::fs::read_to_string(source.join("src/main.rs"))?.contains("fn main() {"));
|
||||
|
||||
Reference in New Issue
Block a user