Files
sausage/spikes/s4-builder/src/main.rs
T
surma 9e9cde5241 spikes: six v3 spikes (V8 harness, WASI shell, git, builder, http hook, esbuild)
Throwaway prototypes for Sausage v3. Plan and decisions:
~/research/sausage-v3/PLAN.md. Results and evidence: the spike READMEs.
2026-11-06 14:03:20 +00:00

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, &registry, &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", &registry))?;
// 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(&registry)
.arg(&registry)
.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(&registry)
.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]))
}