builder: delegate run selection and preserve test working directories
This commit is contained in:
@@ -1,21 +1,33 @@
|
||||
# Rust builder
|
||||
|
||||
The world `cargo` sends `POST http://host.sausage/cargo` with `argv`, `cwd`, and optional `timeout_secs`.
|
||||
The world `cargo` calls `POST http://host.sausage/cargo` with `argv` and `cwd`.
|
||||
The request also accepts optional `timeout_secs`.
|
||||
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.
|
||||
`stdout` and `stderr` events contain byte arrays.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
Each workspace owns a source copy and target directory.
|
||||
It also owns a registry and kache cache.
|
||||
These directories live under `<dir>/host/builds/`.
|
||||
|
||||
Cargo fetches dependencies with network access. The command then runs offline inside bubblewrap namespaces.
|
||||
Only the source copy, target, and private kache cache permit writes.
|
||||
The registry and Nix store remain read-only. HOME and the host network are absent.
|
||||
Cargo fetches dependencies with network access.
|
||||
The command then runs offline inside bubblewrap namespaces.
|
||||
|
||||
The source copy and target directory permit writes.
|
||||
The private kache cache also permits writes.
|
||||
The registry and Nix store remain read-only.
|
||||
Host home directories and the host network are absent.
|
||||
The PID namespace removes compiler descendants and the private kache daemon after Cargo exits.
|
||||
|
||||
The builder copies changed and new source files back through the VFS.
|
||||
|
||||
+55
-15
@@ -24,12 +24,24 @@ pub struct Request {
|
||||
pub cwd: String,
|
||||
pub timeout_secs: Option<u64>,
|
||||
}
|
||||
#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Executable {
|
||||
pub path: String,
|
||||
pub cwd: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "event", rename_all = "snake_case")]
|
||||
pub enum Event {
|
||||
Stdout { bytes: Vec<u8> },
|
||||
Stderr { bytes: Vec<u8> },
|
||||
Exit { code: i32, executables: Vec<String> },
|
||||
Stdout {
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
Stderr {
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
Exit {
|
||||
code: i32,
|
||||
executables: Vec<Executable>,
|
||||
},
|
||||
}
|
||||
pub struct Builder {
|
||||
root: Arc<dyn Layer>,
|
||||
@@ -150,6 +162,7 @@ impl Builder {
|
||||
"link.nu",
|
||||
include_bytes!("../../../userland/cwd-init/link.nu").as_slice(),
|
||||
),
|
||||
("run.nu", include_bytes!("run.nu").as_slice()),
|
||||
] {
|
||||
std::fs::write(tools.join(name), bytes)?;
|
||||
}
|
||||
@@ -178,6 +191,10 @@ impl Builder {
|
||||
return Ok(());
|
||||
}
|
||||
let kache = executable("kache")?;
|
||||
let run_path = target_mount.join("run-path");
|
||||
if action == "run" && run_path.exists() {
|
||||
std::fs::remove_file(&run_path)?;
|
||||
}
|
||||
let mut cmd = Command::new("bwrap");
|
||||
cmd.args([
|
||||
"--unshare-user",
|
||||
@@ -220,6 +237,8 @@ impl Builder {
|
||||
.arg(&cache)
|
||||
.args(["--setenv", "SAUSAGE_CWD_INIT"])
|
||||
.arg(tools.join("init.o"))
|
||||
.args(["--setenv", "SAUSAGE_RUN_PATH"])
|
||||
.arg(&run_path)
|
||||
.args(["--setenv", "CARGO_BUILD_JOBS", "2"]);
|
||||
for name in [
|
||||
"CC_wasm32_wasip2",
|
||||
@@ -242,9 +261,6 @@ impl Builder {
|
||||
"target.wasm32-wasip2.rustflags=['-C','link-arg=sausage-cwd-init']",
|
||||
]);
|
||||
let mut args = request.argv.clone();
|
||||
if action == "run" {
|
||||
args[0] = "build".into();
|
||||
}
|
||||
if matches!(action, "run" | "test")
|
||||
&& let Some(index) = args.iter().position(|a| a == "--")
|
||||
{
|
||||
@@ -259,6 +275,13 @@ impl Builder {
|
||||
}
|
||||
let split = args.iter().position(|a| a == "--").unwrap_or(args.len());
|
||||
cmd.args(&args[..split]);
|
||||
if action == "run" {
|
||||
cmd.arg("--config").arg(format!(
|
||||
"target.wasm32-wasip2.runner=[{:?},{:?}]",
|
||||
executable("nu")?,
|
||||
tools.join("run.nu")
|
||||
));
|
||||
}
|
||||
if compiles {
|
||||
cmd.args([
|
||||
"--target",
|
||||
@@ -282,23 +305,40 @@ impl Builder {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if code == 0 && action == "run" && run_path.exists() {
|
||||
let path = std::fs::read_to_string(&run_path)?;
|
||||
let relative = Path::new(&path).strip_prefix(&target)?;
|
||||
executables.push(Executable {
|
||||
path: format!("{world_target}/{}", relative.display()),
|
||||
cwd: world_cwd.clone(),
|
||||
});
|
||||
}
|
||||
if code == 0
|
||||
&& matches!(action, "test" | "run")
|
||||
&& !(action == "test"
|
||||
&& request
|
||||
.argv
|
||||
.iter()
|
||||
.take_while(|a| a.as_str() != "--")
|
||||
.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)
|
||||
&& value["reason"] == "compiler-artifact"
|
||||
&& (action == "run" || value["profile"]["test"] == true)
|
||||
&& value["profile"]["test"] == true
|
||||
&& let Some(path) = value["executable"].as_str()
|
||||
&& let Ok(relative) = Path::new(path).strip_prefix(&target)
|
||||
{
|
||||
executables.push(format!("{world_target}/{}", relative.display()));
|
||||
let manifest = Path::new(
|
||||
value["manifest_path"]
|
||||
.as_str()
|
||||
.context("missing test manifest")?,
|
||||
);
|
||||
let package = manifest.parent().unwrap().strip_prefix(&source)?;
|
||||
let cwd = sausage_vfs::resolve(&workspace, &package.to_string_lossy());
|
||||
executables.push(Executable {
|
||||
path: format!("{world_target}/{}", relative.display()),
|
||||
cwd,
|
||||
});
|
||||
}
|
||||
}
|
||||
executables.sort();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Record Cargo's selected component without executing it on the host.
|
||||
def main [executable: string] {
|
||||
$executable | save --raw --force $env.SAUSAGE_RUN_PATH
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
//! Cargo executes each integration test component through the world process interface.
|
||||
#[test]
|
||||
fn pass_integration() {
|
||||
assert!(
|
||||
std::fs::read_to_string("Cargo.toml")
|
||||
.unwrap()
|
||||
.contains("builder-fixture")
|
||||
);
|
||||
println!("INTEGRATION_CASE");
|
||||
}
|
||||
|
||||
+35
-1
@@ -41,6 +41,13 @@ fn programs() -> PathBuf {
|
||||
.clone()
|
||||
}
|
||||
async fn cargo(runtime: &Arc<Runtime>, args: &[&str]) -> anyhow::Result<(i32, String)> {
|
||||
cargo_at(runtime, args, "/project").await
|
||||
}
|
||||
async fn cargo_at(
|
||||
runtime: &Arc<Runtime>,
|
||||
args: &[&str],
|
||||
cwd: &str,
|
||||
) -> anyhow::Result<(i32, String)> {
|
||||
let mut bytes = Vec::new();
|
||||
let code = runtime
|
||||
.exec(
|
||||
@@ -50,7 +57,7 @@ async fn cargo(runtime: &Arc<Runtime>, args: &[&str]) -> anyhow::Result<(i32, St
|
||||
.collect(),
|
||||
),
|
||||
Options {
|
||||
cwd: "/project".into(),
|
||||
cwd: cwd.into(),
|
||||
timeout: Some(Duration::from_secs(120)),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -156,12 +163,39 @@ async fn cargo_build_test_and_source_changes() -> anyhow::Result<()> {
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(out.contains("1 passed"), "{out}");
|
||||
assert!(out.contains("INTEGRATION_CASE"), "{out}");
|
||||
let (code, out) = cargo_at(
|
||||
&runtime,
|
||||
&["test", "pass", "--", "--nocapture"],
|
||||
"/project/src",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(out.contains("INTEGRATION_CASE"), "{out}");
|
||||
let (code, out) = cargo(&runtime, &["test", "fail"]).await?;
|
||||
assert_eq!(code, 101, "{out}");
|
||||
assert!(out.contains("intentional fixture failure"), "{out}");
|
||||
let (code, out) = cargo(&runtime, &["test", "--no-run"]).await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(!out.contains("intentional fixture failure"), "{out}");
|
||||
std::fs::create_dir_all(source.join("src/bin"))?;
|
||||
std::fs::write(
|
||||
source.join("src/bin/other.rs"),
|
||||
"fn main() { println!(\"OTHER_BIN\"); }\n",
|
||||
)?;
|
||||
let (code, out) = cargo(&runtime, &["run"]).await?;
|
||||
assert_eq!(code, 101, "{out}");
|
||||
assert!(out.contains("--bin"), "{out}");
|
||||
let manifest = std::fs::read_to_string(source.join("Cargo.toml"))?;
|
||||
std::fs::write(
|
||||
source.join("Cargo.toml"),
|
||||
manifest.replace("[package]\n", "[package]\ndefault-run = \"other\"\n"),
|
||||
)?;
|
||||
let (code, out) = cargo(&runtime, &["run"]).await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(out.contains("OTHER_BIN"), "{out}");
|
||||
let (code, out) = cargo(&runtime, &["run", "--bin", "builder-fixture"]).await?;
|
||||
assert_eq!(code, 0, "{out}");
|
||||
assert!(out.contains("builder fixture cwd: /project"), "{out}");
|
||||
for action in ["check", "clippy", "fetch", "tree", "metadata", "update"] {
|
||||
let (code, out) = cargo(&runtime, &[action]).await?;
|
||||
assert_eq!(code, 0, "{action}: {out}");
|
||||
|
||||
@@ -4,9 +4,21 @@ use wasi::http::{outgoing_handler, types::*};
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(tag = "event", rename_all = "snake_case")]
|
||||
enum Event {
|
||||
Stdout { bytes: Vec<u8> },
|
||||
Stderr { bytes: Vec<u8> },
|
||||
Exit { code: i32, executables: Vec<String> },
|
||||
Stdout {
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
Stderr {
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
Exit {
|
||||
code: i32,
|
||||
executables: Vec<Executable>,
|
||||
},
|
||||
}
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Executable {
|
||||
path: String,
|
||||
cwd: String,
|
||||
}
|
||||
fn error(value: impl std::fmt::Debug) -> io::Error {
|
||||
io::Error::other(format!("{value:?}"))
|
||||
@@ -86,7 +98,10 @@ pub fn run() -> io::Result<i32> {
|
||||
let action = argv.first().map(String::as_str).unwrap_or("");
|
||||
let args = execution_args(&argv);
|
||||
for executable in executables {
|
||||
let code = spawn::Command::new(executable).args(&args).status()?;
|
||||
let code = spawn::Command::new(executable.path)
|
||||
.current_dir(executable.cwd)
|
||||
.args(&args)
|
||||
.status()?;
|
||||
if code != 0 {
|
||||
return Ok(if action == "test" { 101 } else { code });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user