Merge v3-t15: shared compile cache, precompile, /bin/*.wasm and /src/sausage copies (T15)

This commit is contained in:
2026-11-06 22:18:53 +00:00
94 changed files with 991 additions and 270 deletions
+8 -1
View File
@@ -5,6 +5,9 @@ use std::path::PathBuf;
#[derive(Parser)]
#[command(version, about)]
struct Cli {
/// Shared host-user compile cache (otherwise SAUSAGE_CACHE or the XDG cache folder).
#[arg(long, global = true)]
cache: Option<PathBuf>,
#[command(subcommand)]
command: Command,
}
@@ -29,7 +32,11 @@ enum Command {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
match Cli::parse().command {
let cli = Cli::parse();
if let Some(cache) = cli.cache {
sausage::process::configure_cache(cache)?;
}
match cli.command {
Command::Run { world, userland } => {
let bundle =
userland.or_else(|| std::env::var_os("SAUSAGE_USERLAND").map(PathBuf::from));
+236
View File
@@ -0,0 +1,236 @@
//! Shares compiled programs and single-flight compilation across worlds in one host process.
use super::{Compiled, State, adapter, bindings};
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{Arc, Mutex, OnceLock},
};
use tokio::sync::watch;
type Compilation = watch::Receiver<Option<Result<Compiled, String>>>;
use wasmtime::{
Engine,
component::{Component, Linker},
};
use wasmtime_wasi::p2::bindings::CommandPre;
static DIRECTORY: OnceLock<PathBuf> = OnceLock::new();
static SHARED: Mutex<Option<Arc<Compiler>>> = Mutex::new(None);
/// Select the host user's cache before opening a world.
pub fn configure(directory: PathBuf) -> anyhow::Result<()> {
DIRECTORY
.set(directory)
.map_err(|_| anyhow::anyhow!("cache directory already configured"))
}
fn directory() -> anyhow::Result<PathBuf> {
if let Some(dir) = DIRECTORY.get() {
return Ok(dir.clone());
}
if let Some(dir) = std::env::var_os("SAUSAGE_CACHE") {
return Ok(dir.into());
}
if let Some(dir) = std::env::var_os("XDG_CACHE_HOME") {
return Ok(PathBuf::from(dir).join("sausage"));
}
Ok(
PathBuf::from(std::env::var_os("HOME").ok_or_else(|| anyhow::anyhow!("HOME is not set"))?)
.join(".cache/sausage"),
)
}
pub(super) fn shared() -> anyhow::Result<Arc<Compiler>> {
let mut shared = SHARED.lock().unwrap();
if shared.is_none() {
*shared = Some(Compiler::new(&directory()?)?);
}
Ok(shared.as_ref().unwrap().clone())
}
pub(super) struct Compiler {
pub engine: Engine,
programs: Mutex<HashMap<Vec<u8>, Compilation>>,
#[cfg(test)]
before_compile: Mutex<Option<Box<dyn FnOnce() + Send>>>,
#[cfg(test)]
compilations: std::sync::atomic::AtomicUsize,
}
impl Compiler {
pub fn new(directory: &Path) -> anyhow::Result<Arc<Self>> {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let mut config = wasmtime::Config::new();
config.epoch_interruption(true);
// Keep Cranelift's default optimization: compiles are cached per content, and guests run
// many times. OptLevel::None saved only 12.7% of compile time (T15) but slows every run.
let mut cache = wasmtime::CacheConfig::new();
cache.with_directory(directory);
config.cache(Some(wasmtime::Cache::new(cache)?));
Ok(Arc::new(Self {
engine: Engine::new(&config)?,
programs: Mutex::new(HashMap::new()),
#[cfg(test)]
before_compile: Mutex::new(None),
#[cfg(test)]
compilations: std::sync::atomic::AtomicUsize::new(0),
}))
}
pub async fn get(self: &Arc<Self>, bytes: Vec<u8>) -> anyhow::Result<Compiled> {
let mut result = self
.programs
.lock()
.unwrap()
.entry(bytes.clone())
.or_insert_with(|| {
let (tx, rx) = watch::channel(None);
let compiler = self.clone();
// The job owns completion even if a command or a precompile waiter is cancelled.
tokio::spawn(async move {
let result =
tokio::task::spawn_blocking(move || compiler.compile(&bytes)).await;
tx.send_replace(Some(
result
.map_err(anyhow::Error::from)
.and_then(|r| r)
.map_err(|e| format!("{e:#}")),
));
});
rx
})
.clone();
loop {
if let Some(result) = result.borrow().clone() {
return result.map_err(|e| anyhow::anyhow!(e));
}
result.changed().await?;
}
}
fn compile(&self, bytes: &[u8]) -> anyhow::Result<Compiled> {
#[cfg(test)]
{
self.compilations
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if let Some(hook) = self.before_compile.lock().unwrap().take() {
hook();
}
}
if bytes.get(..8) == Some(b"\0asm\x01\0\0\0") {
let module = wasmtime::Module::new(&self.engine, bytes)?;
let mut linker = wasmtime::Linker::new(&self.engine);
sausage_vfs::preview1::add_to_linker(&mut linker, |s: &mut State| {
s.preview1.as_mut().unwrap()
})?;
Ok(Compiled::Module(linker.instantiate_pre(&module)?))
} else {
let component = Component::new(&self.engine, bytes)?;
let mut linker = Linker::new(&self.engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?;
adapter::add_to_linker(&mut linker, |s: &mut State| adapter::View {
adapter: &mut s.fs,
table: &mut s.table,
})?;
bindings::sausage::process::host::add_to_linker::<_, wasmtime::component::HasSelf<_>>(
&mut linker,
|s| s,
)?;
Ok(Compiled::Component(CommandPre::new(
linker.instantiate_pre(&component)?,
)?))
}
}
}
/// Compile programs in both command folders without blocking world startup.
pub(super) fn precompile(
compiler: Arc<Compiler>,
root: Arc<dyn sausage_vfs::Layer>,
) -> tokio::task::JoinHandle<anyhow::Result<usize>> {
tokio::spawn(async move {
let mut tasks = tokio::task::JoinSet::new();
// Leave CPU and file bandwidth for commands during startup.
let slots = Arc::new(tokio::sync::Semaphore::new(2));
for folder in ["/bin", "/usr/local/bin"] {
let entries = match root.list(folder).await {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => return Err(e.into()),
};
for entry in entries.into_iter().filter(|entry| !entry.directory) {
let root = root.clone();
let compiler = compiler.clone();
let path = format!("{folder}/{}", entry.name);
let slots = slots.clone();
tasks.spawn(async move {
let _permit = slots.acquire().await?;
let file = root
.open(
&path,
sausage_vfs::Open {
follow: true,
..Default::default()
},
)
.await?;
let bytes = file.read(0, file.stat().await?.size as usize).await?;
if bytes.get(..4) != Some(b"\0asm") {
return Ok::<_, anyhow::Error>(false);
}
compiler.get(bytes).await?;
Ok(true)
});
}
}
let mut count = 0;
while let Some(result) = tasks.join_next().await {
if result?? {
count += 1;
}
}
Ok(count)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::{Command, Exec, Options};
use std::{future::Future, sync::atomic::Ordering};
#[tokio::test(flavor = "multi_thread")]
async fn command_joins_precompile_even_after_waiter_cancellation() {
let temp = tempfile::tempdir().unwrap();
let compiler = Compiler::new(&temp.path().join("cache")).unwrap();
let world = crate::world::World::open(&temp.path().join("world")).unwrap();
let bytes =
wat::parse_str(r#"(module (memory (export "memory") 1) (func (export "_start")))"#)
.unwrap();
std::fs::create_dir_all(temp.path().join("world/world/bin")).unwrap();
std::fs::write(temp.path().join("world/world/bin/program.wasm"), &bytes).unwrap();
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
let (finish_tx, finish_rx) = std::sync::mpsc::channel();
*compiler.before_compile.lock().unwrap() = Some(Box::new(move || {
started_tx.send(()).unwrap();
finish_rx.recv().unwrap();
}));
// Startup returns while the compiler remains blocked behind the test gate.
let runtime = super::super::Runtime::with_compiler(world, compiler.clone()).unwrap();
started_rx.await.unwrap();
let background = compiler.clone();
let precompile = tokio::spawn(async move { background.get(bytes).await });
let mut output = |_: crate::exec::Stream, _: &[u8]| {};
let command = runtime.exec(
Command::Argv(vec!["/bin/program".into()]),
Options::default(),
&mut output,
);
tokio::pin!(command);
let pending = std::future::poll_fn(|cx| {
std::task::Poll::Ready(command.as_mut().poll(cx).is_pending())
})
.await;
assert!(pending);
assert_eq!(compiler.compilations.load(Ordering::Relaxed), 1);
precompile.abort();
let _ = precompile.await;
finish_tx.send(()).unwrap();
assert_eq!(command.await.unwrap(), 0);
assert_eq!(compiler.compilations.load(Ordering::Relaxed), 1);
assert!(!temp.path().join("world/host/compile-cache").exists());
}
}
+60 -47
View File
@@ -5,7 +5,7 @@ use crate::{
};
use sausage_vfs::{
Mounts,
adapter::{self, Adapter, View},
adapter::{self, Adapter},
};
use std::{
collections::HashMap,
@@ -22,7 +22,7 @@ use tokio::{
};
use wasmtime::{
Engine, Store,
component::{Component, Linker, Resource, ResourceTable},
component::{Resource, ResourceTable},
};
use wasmtime_wasi::{
WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView,
@@ -45,6 +45,8 @@ mod bindings {
});
}
use bindings::sausage::process::host::{Host, HostChild, Request, Stdio};
mod cache;
pub use cache::configure as configure_cache;
mod services;
mod stdio;
pub use services::supervise;
@@ -77,7 +79,8 @@ struct Entry {
pub struct Runtime {
world: World,
engine: Engine,
cache: Mutex<HashMap<Vec<u8>, Compiled>>,
compiler: Arc<cache::Compiler>,
cache: Mutex<std::collections::HashSet<Vec<u8>>>,
entries: Mutex<HashMap<u32, Entry>>,
next: AtomicU32,
alive: Arc<AtomicBool>,
@@ -138,14 +141,14 @@ impl Runtime {
Self::new(World::open(path)?)
}
pub fn new(world: World) -> anyhow::Result<Arc<Self>> {
// Deno and WASI HTTP enable different providers. Select one before HTTPS creates a client.
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let mut config = wasmtime::Config::new();
config.epoch_interruption(true);
let mut cache = wasmtime::CacheConfig::new();
cache.with_directory(world.host_dir.join("compile-cache"));
config.cache(Some(wasmtime::Cache::new(cache)?));
let engine = Engine::new(&config)?;
Self::with_compiler(world, cache::shared()?)
}
/// Open an independent engine using an explicit shared disk cache.
pub fn new_with_cache(world: World, directory: &Path) -> anyhow::Result<Arc<Self>> {
Self::with_compiler(world, cache::Compiler::new(directory)?)
}
fn with_compiler(world: World, compiler: Arc<cache::Compiler>) -> anyhow::Result<Arc<Self>> {
let engine = compiler.engine.clone();
let alive = Arc::new(AtomicBool::new(true));
let ticking = alive.clone();
let clock = engine.clone();
@@ -155,19 +158,57 @@ impl Runtime {
clock.increment_epoch();
}
});
Ok(Arc::new(Self {
let runtime = Arc::new(Self {
world,
engine,
alive,
cache: Mutex::new(HashMap::new()),
compiler,
cache: Mutex::new(Default::default()),
entries: Mutex::new(HashMap::new()),
next: AtomicU32::new(1),
}))
});
cache::precompile(runtime.compiler.clone(), runtime.world.root.clone());
Ok(runtime)
}
/// Number of distinct program contents compiled by this runtime.
pub fn compiled_programs(&self) -> usize {
self.cache.lock().unwrap().len()
}
async fn resolve_program(
&self,
cwd: &str,
name: &str,
env: &[(String, String)],
) -> anyhow::Result<String> {
let paths = if name.contains('/') {
vec![sausage_vfs::resolve(cwd, name)]
} else {
let path = env
.iter()
.find(|(key, _)| key == "PATH")
.map(|(_, value)| value.as_str())
.unwrap_or("/usr/local/bin:/bin");
path.split(':')
.map(|folder| {
sausage_vfs::resolve(
cwd,
&std::path::Path::new(folder).join(name).to_string_lossy(),
)
})
.collect()
};
for path in paths {
for candidate in [path.clone(), format!("{path}.wasm")] {
match self.world.root.stat(&candidate, true).await {
Ok(stat) if !stat.directory => return Ok(candidate),
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
}
}
anyhow::bail!("program not found: {name}")
}
fn kill_command(&self, command: u32) {
for entry in self.entries.lock().unwrap().values() {
if entry.command == command {
@@ -222,7 +263,9 @@ impl Runtime {
request.cwd = sausage_vfs::resolve("/", &request.cwd);
request.env.retain(|(name, _)| name != "PWD");
request.env.push(("PWD".into(), request.cwd.clone()));
let executable = sausage_vfs::resolve(&request.cwd, &request.program);
let executable = self
.resolve_program(&request.cwd, &request.program, &request.env)
.await?;
let file = self
.world
.root
@@ -237,38 +280,8 @@ impl Runtime {
let bytes = file
.read(0, usize::try_from(file.stat().await?.size)?)
.await?;
let pre = {
let mut cache = self.cache.lock().unwrap();
if let Some(pre) = cache.get(&bytes) {
pre.clone()
} else {
let pre = if bytes.get(..8) == Some(b"\0asm\x01\0\0\0") {
let module = wasmtime::Module::new(&self.engine, &bytes)?;
let mut linker = wasmtime::Linker::new(&self.engine);
sausage_vfs::preview1::add_to_linker(&mut linker, |s: &mut State| {
s.preview1.as_mut().unwrap()
})?;
Compiled::Module(linker.instantiate_pre(&module)?)
} else {
let component = Component::new(&self.engine, &bytes)?;
let mut linker = Linker::new(&self.engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
// The library linker includes exit-with-code. Only Wasmtime's CLI gates it.
wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?;
adapter::add_to_linker(&mut linker, |s: &mut State| View {
adapter: &mut s.fs,
table: &mut s.table,
})?;
bindings::sausage::process::host::add_to_linker::<
_,
wasmtime::component::HasSelf<_>,
>(&mut linker, |s| s)?;
Compiled::Component(CommandPre::new(linker.instantiate_pre(&component)?)?)
};
cache.insert(bytes, pre.clone());
pre
}
};
let pre = self.compiler.get(bytes.clone()).await?;
self.cache.lock().unwrap().insert(bytes);
let pid = self.next.fetch_add(1, Ordering::Relaxed);
let command = if command == 0 { pid } else { command };
let group = match request.group {
+23 -9
View File
@@ -19,17 +19,37 @@ pub struct World {
pub processes: crate::process::Processes,
}
fn copy_tree(source: &Path, destination: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(destination)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
let target = destination.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_tree(&entry.path(), &target)?;
} else {
std::fs::copy(entry.path(), target)?;
}
}
Ok(())
}
impl World {
/// Open (and create if missing) the world directory `dir`, including `/proc` and `/run` mounts.
pub fn open(dir: &Path) -> anyhow::Result<Self> {
Self::open_with_userland(dir, None)
}
/// Mount an optional host bundle read-only at `/bin` (D43).
/// Copy an optional bundle into a new world. Existing worlds retain their programs.
pub fn open_with_userland(dir: &Path, userland: Option<&Path>) -> anyhow::Result<Self> {
let root_dir = dir.join("world");
let host_dir = dir.join("host");
let fresh = !root_dir.exists();
std::fs::create_dir_all(root_dir.join("usr/local/bin"))?;
if fresh && let Some(bundle) = userland {
for folder in ["bin", "src"] {
copy_tree(&bundle.join(folder), &root_dir.join(folder))?;
}
}
std::fs::create_dir_all(&host_dir)?;
let mut mounts = Mounts::default();
mounts.mount(
@@ -55,17 +75,11 @@ impl World {
}
Ok(text.into_bytes())
});
if let Some(bundle) = userland {
mounts.mount(
"/bin",
Arc::new(Host::new(bundle, Process::default())?),
true,
);
}
mounts.mount("/proc", Arc::new(proc), true);
mounts.mount("/run", Arc::new(Memory::default()), false);
let root: Arc<dyn Layer> = Arc::new(mounts);
Ok(World {
root: Arc::new(mounts),
root,
host_dir,
processes,
})
+114 -8
View File
@@ -1,13 +1,13 @@
//! P1 integration: a read-only bundle and real shell calls from the durable harness.
//! P1 integration: independent bundle copies and real shell calls from the durable harness.
use sausage::{harness::run_module, world::World};
use sausage_vfs::Open;
use std::path::PathBuf;
#[tokio::test(flavor = "current_thread")]
async fn bundle_mount_and_real_shell() -> anyhow::Result<()> {
async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
let bundle = repo.join("target/userland/bin");
if !bundle.join("sh").exists() {
let bundle = repo.join("target/userland/bundle");
if !bundle.join("bin/sh.wasm").exists() {
assert!(
std::process::Command::new("nu")
.env_remove("CARGO_TARGET_DIR")
@@ -18,9 +18,16 @@ async fn bundle_mount_and_real_shell() -> anyhow::Result<()> {
}
let dir = tempfile::tempdir()?;
let world = World::open_with_userland(dir.path(), Some(&bundle))?;
assert!(world.root.stat("/bin/sh", true).await?.size > 0);
assert!(world.root.stat("/bin/sh.wasm", true).await?.size > 0);
use std::os::unix::fs::MetadataExt;
for entry in std::fs::read_dir(dir.path().join("world/bin"))? {
let entry = entry?;
assert_eq!(entry.path().extension().unwrap(), "wasm");
assert_eq!(entry.metadata()?.nlink(), 1);
assert!(!entry.file_type()?.is_symlink());
}
assert!(world.root.stat("/usr/local/bin", true).await?.directory);
let denied = world
let writable = world
.root
.open(
"/bin/forbidden",
@@ -31,17 +38,72 @@ async fn bundle_mount_and_real_shell() -> anyhow::Result<()> {
},
)
.await;
assert!(matches!(denied, Err(e) if e.kind() == std::io::ErrorKind::ReadOnlyFilesystem));
assert!(writable.is_ok());
let exec = sausage::process::exec(World {
root: world.root.clone(),
host_dir: world.host_dir.clone(),
processes: world.processes.clone(),
})?;
use sausage::exec::{Command, Options, Stream};
for (script, expected) in [
("command -v ls", "/bin/ls.wasm\n"),
("ls", "bin"),
("git --version", "git version"),
("printf 'a\\n' | xargs echo", "a\n"),
("env FOO=1 printenv FOO", "1\n"),
] {
let mut out = Vec::new();
let code = exec
.exec(
Command::Shell(script.into()),
Options::default(),
&mut |stream, bytes| {
if matches!(stream, Stream::Stdout) {
out.extend_from_slice(bytes);
}
},
)
.await?;
assert_eq!(code, 0, "{script}");
assert!(String::from_utf8(out)?.contains(expected), "{script}");
}
let mut history = Vec::new();
assert_eq!(
exec.exec(
Command::Shell("cd /src/sausage && git log --oneline".into()),
Options::default(),
&mut |stream, bytes| {
if matches!(stream, Stream::Stdout) {
history.extend_from_slice(bytes);
}
},
)
.await?,
0,
);
let history = String::from_utf8(history)?;
assert_eq!(history.lines().count(), 1);
assert!(history.contains("Bundle source "));
let mut status = Vec::new();
assert_eq!(
exec.exec(
Command::Shell("cd /src/sausage && git status --short".into()),
Options::default(),
&mut |stream, bytes| {
if matches!(stream, Stream::Stdout) {
status.extend_from_slice(bytes);
}
},
)
.await?,
0,
);
assert!(status.is_empty(), "{}", String::from_utf8_lossy(&status));
let source = format!(
"globalThis.testP1 = true;\n{}",
include_str!("../js/test-bundle.js")
);
run_module(world, exec, &source).await?;
run_module(world, exec.clone(), &source).await?;
assert_eq!(
std::fs::read_to_string(dir.path().join("world/test.txt"))?,
"after\n"
@@ -59,6 +121,50 @@ async fn bundle_mount_and_real_shell() -> anyhow::Result<()> {
let actual = std::fs::read_to_string(spills[0].path())?;
let expected: String = (1..=10000).map(|n| format!("{n}\n")).collect();
assert_eq!(actual, expected);
let source_cat = std::fs::read(bundle.join("bin/cat.wasm"))?;
let head = std::fs::read(dir.path().join("world/bin/head.wasm"))?;
let mut out = Vec::new();
assert_eq!(
exec.exec(
Command::Shell(
"cp /bin/true.wasm /bin/cat.wasm; cat /test.txt; head -n 1 /test.txt".into()
),
Options::default(),
&mut |stream, bytes| {
if matches!(stream, Stream::Stdout) {
out.extend_from_slice(bytes);
}
}
)
.await?,
0
);
assert_eq!(out, b"after\n");
assert_eq!(std::fs::read(bundle.join("bin/cat.wasm"))?, source_cat);
assert_eq!(std::fs::read(dir.path().join("world/bin/head.wasm"))?, head);
assert_eq!(
exec.exec(
Command::Shell("cp /bin/true.wasm /usr/local/bin/head.wasm; head /test.txt".into()),
Options::default(),
&mut |_, bytes| assert!(bytes.is_empty())
)
.await?,
0
);
let changed = std::fs::read(dir.path().join("world/bin/cat.wasm"))?;
let reopened = World::open_with_userland(
dir.path(),
Some(std::path::Path::new("/nonexistent-bundle")),
)?;
assert_eq!(
reopened
.root
.open("/bin/cat.wasm", Open::default())
.await?
.read(0, changed.len())
.await?,
changed
);
// An unbundled world remains usable by process tests and file-only callers.
let unbundled = tempfile::tempdir()?;
let world = World::open(unbundled.path())?;
+56 -11
View File
@@ -9,6 +9,39 @@ use std::{
sync::{Arc, Once},
time::Duration,
};
#[test]
fn world_copies_programs_and_nested_source_only_once() -> anyhow::Result<()> {
let temp = tempfile::tempdir()?;
let bundle = temp.path().join("bundle");
std::fs::create_dir_all(bundle.join("bin"))?;
std::fs::create_dir_all(bundle.join("src/sausage/.git"))?;
std::fs::write(bundle.join("bin/probe.wasm"), b"program")?;
std::fs::write(
bundle.join("src/sausage/.git/HEAD"),
b"ref: refs/heads/main\n",
)?;
std::fs::write(bundle.join("src/sausage/README.md"), b"source")?;
let path = temp.path().join("world");
let _world = World::open_with_userland(&path, Some(&bundle))?;
assert_eq!(
std::fs::read(path.join("world/bin/probe.wasm"))?,
b"program"
);
assert_eq!(
std::fs::read(path.join("world/src/sausage/README.md"))?,
b"source"
);
assert!(path.join("world/src/sausage/.git/HEAD").is_file());
std::fs::write(path.join("world/src/sausage/README.md"), b"agent patch")?;
let _world = World::open_with_userland(&path, Some(Path::new("/missing-bundle")))?;
assert_eq!(
std::fs::read(path.join("world/src/sausage/README.md"))?,
b"agent patch"
);
assert!(!path.join("host/compile-cache").exists());
Ok(())
}
mod features {
//! Acceptance coverage for preview1, identity, inherited IO, services, HTTP, and bundle aliases.
use super::*;
@@ -260,28 +293,32 @@ mod features {
empty(&processes).await;
}
#[tokio::test(flavor = "multi_thread")]
async fn relative_aliases_share_compilation() {
async fn independent_copies_share_compilation() {
let _serial = SERIAL.lock().await;
let (temp, runtime, _) = world();
let bin = temp.path().join("world/bin");
for name in ["true", "echo"] {
std::os::unix::fs::symlink("cat", bin.join(name)).unwrap();
for name in ["read-a.wasm", "read-b.wasm"] {
std::fs::copy(bin.join("cat.wasm"), bin.join(name)).unwrap();
}
assert_eq!(
run(&runtime, Command::Argv(vec!["/bin/true".into()]), "/")
.await
.0,
run(
&runtime,
Command::Argv(vec!["/bin/read-a".into(), "/f".into()]),
"/"
)
.await
.0,
0
);
assert_eq!(runtime.compiled_programs(), 1);
let (code, out, _) = run(
&runtime,
Command::Argv(vec!["/bin/echo".into(), "shared".into()]),
Command::Argv(vec!["/bin/read-b".into(), "/f".into()]),
"/",
)
.await;
assert_eq!(code, 0);
assert_eq!(out, b"shared\n");
assert_eq!(out, b"root\n");
assert_eq!(runtime.compiled_programs(), 1);
}
#[tokio::test(flavor = "multi_thread")]
@@ -464,13 +501,17 @@ fn world() -> (tempfile::TempDir, Arc<Runtime>, sausage::process::Processes) {
let world = World::open(temp.path()).unwrap();
let cache = Path::new(env!("CARGO_TARGET_TMPDIR")).join("process-host/compile-cache");
std::fs::create_dir_all(&cache).unwrap();
std::os::unix::fs::symlink(cache, world.host_dir.join("compile-cache")).unwrap();
let processes = world.processes.clone();
fixture(&temp.path().join("world"));
let artifacts = repo().join("userland/target/wasm32-wasip2/release");
let bin = temp.path().join("world/bin");
for tool in ["cat", "tr", "ls", "wc", "yes", "head", "sleep"] {
std::fs::copy(artifacts.join("sausage-coreutils.wasm"), bin.join(tool)).unwrap();
std::fs::copy(
artifacts.join(format!("{tool}.wasm")),
bin.join(format!("{tool}.wasm")),
)
.unwrap();
}
for tool in ["ps", "kill"] {
std::fs::copy(artifacts.join("sausage-procps.wasm"), bin.join(tool)).unwrap();
@@ -491,7 +532,11 @@ fn world() -> (tempfile::TempDir, Arc<Runtime>, sausage::process::Processes) {
bin.join("git"),
)
.unwrap();
(temp, Runtime::new(world).unwrap(), processes)
(
temp,
Runtime::new_with_cache(world, &cache).unwrap(),
processes,
)
}
async fn run(runtime: &Arc<Runtime>, command: Command, cwd: &str) -> (i32, Vec<u8>, Vec<u8>) {
let (mut stdout, mut stderr) = (Vec::new(), Vec::new());