Throwaway prototypes for Sausage v3. Plan and decisions: ~/research/sausage-v3/PLAN.md. Results and evidence: the spike READMEs.
134 lines
4.3 KiB
Rust
134 lines
4.3 KiB
Rust
//! Copies sources, fetches dependencies, and builds WASI artifacts in Linux namespaces.
|
|
use std::{env, error::Error, fs, path::Path, process::Command};
|
|
|
|
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
|
const BASE: &str = "/tmp/s4-builder";
|
|
const KACHE: &str = "/nix/store/s4zxxn1y3kzvi33yl0gny5y30ri14h4a-kache-1.2.2/bin/kache";
|
|
|
|
fn run(command: &mut Command) -> Result<()> {
|
|
let status = command.status()?;
|
|
if !status.success() {
|
|
return Err(format!("{command:?}: {status}").into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn copy_tree(source: &Path, dest: &Path) -> Result<()> {
|
|
fs::create_dir_all(dest)?;
|
|
for entry in fs::read_dir(source)? {
|
|
let entry = entry?;
|
|
if ["target", ".git"].contains(&entry.file_name().to_str().unwrap_or("")) {
|
|
continue;
|
|
}
|
|
let path = dest.join(entry.file_name());
|
|
if entry.file_type()?.is_dir() {
|
|
copy_tree(&entry.path(), &path)?;
|
|
} else if entry.file_type()?.is_file() {
|
|
fs::copy(entry.path(), path)?;
|
|
} else {
|
|
return Err("source symlinks are not supported by this spike".into());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn artifacts(source: &Path, out: &Path) -> Result<()> {
|
|
for entry in fs::read_dir(source)? {
|
|
let path = entry?.path();
|
|
if path.is_dir() {
|
|
artifacts(&path, out)?;
|
|
} else if path.extension().is_some_and(|ext| ext == "wasm") {
|
|
fs::copy(&path, out.join(path.file_name().unwrap()))?;
|
|
println!("artifact: {}", path.display());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let args: Vec<_> = env::args().collect();
|
|
if args.len() != 4 || args[1] != "build" {
|
|
return Err("usage: builder build <source dir> <out dir>".into());
|
|
}
|
|
let base = Path::new(BASE);
|
|
let source = base.join("source");
|
|
let target = base.join("target");
|
|
let registry = base.join("cargo-home");
|
|
let cache = base.join("kache");
|
|
for dir in [&target, ®istry, &cache, &target.join("tmp")] {
|
|
fs::create_dir_all(dir)?;
|
|
}
|
|
if source.exists() {
|
|
fs::remove_dir_all(&source)?;
|
|
}
|
|
copy_tree(Path::new(&args[2]), &source)?;
|
|
run(Command::new("cargo")
|
|
.arg("fetch")
|
|
.current_dir(&source)
|
|
.env("CARGO_HOME", ®istry))?;
|
|
|
|
// A private PID namespace also removes the on-demand kache daemon after cargo exits.
|
|
// It must never reuse a daemon outside the mount/network namespaces.
|
|
for action in ["build", "test"] {
|
|
let mut command = Command::new("bwrap");
|
|
command
|
|
.args([
|
|
"--unshare-user",
|
|
"--unshare-pid",
|
|
"--unshare-net",
|
|
"--die-with-parent",
|
|
])
|
|
.args(["--ro-bind", "/nix/store", "/nix/store"])
|
|
.arg("--bind")
|
|
.arg(target.join("tmp"))
|
|
.arg("/tmp")
|
|
.arg("--bind")
|
|
.arg(&source)
|
|
.arg(&source)
|
|
.arg("--bind")
|
|
.arg(&target)
|
|
.arg(&target)
|
|
.arg("--bind")
|
|
.arg(&cache)
|
|
.arg(&cache)
|
|
.arg("--ro-bind")
|
|
.arg(®istry)
|
|
.arg(®istry)
|
|
.args([
|
|
"--proc",
|
|
"/proc",
|
|
"--dev",
|
|
"/dev",
|
|
"--remount-ro",
|
|
"/",
|
|
"--chdir",
|
|
])
|
|
.arg(&source)
|
|
.args(["--clearenv", "--setenv", "PATH"])
|
|
.arg(env::var("PATH")?)
|
|
.args(["--setenv", "HOME", "/nonexistent"])
|
|
.args(["--setenv", "CARGO_HOME"])
|
|
.arg(®istry)
|
|
.args(["--setenv", "CARGO_TARGET_DIR"])
|
|
.arg(&target)
|
|
.args(["--setenv", "RUSTC_WRAPPER", KACHE])
|
|
.args(["--setenv", "KACHE_CACHE_DIR"])
|
|
.arg(&cache)
|
|
.args(["--setenv", "CARGO_BUILD_JOBS", "2"])
|
|
.args([
|
|
"cargo",
|
|
action,
|
|
"--release",
|
|
"--offline",
|
|
"--target",
|
|
"wasm32-wasip2",
|
|
]);
|
|
if action == "test" {
|
|
command.arg("--no-run");
|
|
}
|
|
run(&mut command)?;
|
|
}
|
|
fs::create_dir_all(&args[3])?;
|
|
artifacts(&target.join("wasm32-wasip2/release"), Path::new(&args[3]))
|
|
}
|