//! Shared fixture copies and builds. A mutex protects cwd-init and the shell build. use std::{ hash::{Hash, Hasher}, os::unix::{ffi::OsStrExt, fs::PermissionsExt}, path::{Path, PathBuf}, process::{Command, ExitStatus}, sync::{Mutex, OnceLock}, }; pub static BUILD: Mutex<()> = Mutex::new(()); pub fn runtime_options() -> sausage::process::RuntimeOptions { sausage::process::RuntimeOptions { no_builder_sandbox: std::env::var("SAUSAGE_TEST_NO_BUILDER_SANDBOX").as_deref() == Ok("1"), } } /// Keep the real shared prompt in every roster fixture, with an ephemeral approval port. pub fn write_config( dir: &Path, main_agent_name: &str, roster: serde_json::Value, ) -> anyhow::Result<()> { let mut config: serde_json::Value = serde_yaml_ng::from_str(include_str!("../../../config/config.yaml"))?; config["main_agent_name"] = serde_json::json!(main_agent_name); let folder = dir.join("world/etc/sausage"); std::fs::create_dir_all(folder.join("roster"))?; for entry in roster.as_array().unwrap() { let mut fields = entry.as_object().unwrap().clone(); let name = fields.remove("name").unwrap().as_str().unwrap().to_owned(); let prompt = fields .remove("prompt") .unwrap() .as_str() .unwrap() .to_owned(); std::fs::write( folder.join("roster").join(format!("{name}.md")), format!( "---\n{}---\n\n{prompt}\n", serde_yaml_ng::to_string(&fields)? ), )?; } std::fs::write( folder.join("config.yaml"), serde_yaml_ng::to_string(&config)?, )?; Ok(()) } pub fn copy_tree(source: &Path, destination: &Path) -> anyhow::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(()) } pub fn target_dir() -> PathBuf { std::env::var_os("CARGO_TARGET_DIR") .map(PathBuf::from) .unwrap_or_else(|| { Path::new(env!("CARGO_TARGET_TMPDIR")) .parent() .unwrap() .to_owned() }) } /// Fingerprint the snapshot inputs and their current working-tree contents. pub fn bundle_key(repo: &Path) -> anyhow::Result { const INPUTS: &[&str] = &[ "host", "vfs", "userland", "config", "skills", "extensions", "Cargo.toml", "Cargo.lock", "flake.nix", "flake.lock", "README.md", "AGENTS.md", ".gitignore", ]; let tree = Command::new("git") .current_dir(repo) .args(["ls-tree", "HEAD", "--"]) .args(INPUTS) .output()?; anyhow::ensure!( tree.status.success(), "git ls-tree: {}", String::from_utf8_lossy(&tree.stderr) ); let files = Command::new("git") .current_dir(repo) .args([ "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ]) .args(INPUTS) .output()?; anyhow::ensure!( files.status.success(), "git ls-files: {}", String::from_utf8_lossy(&files.stderr) ); let mut hash = std::collections::hash_map::DefaultHasher::new(); tree.stdout.hash(&mut hash); let mut names: Vec<_> = files .stdout .split(|byte| *byte == 0) .filter(|name| !name.is_empty()) .collect(); names.sort_unstable(); for name in names { name.hash(&mut hash); let path = repo.join(std::ffi::OsStr::from_bytes(name)); match std::fs::symlink_metadata(&path) { Ok(metadata) => { metadata.permissions().mode().hash(&mut hash); if metadata.file_type().is_symlink() { std::fs::read_link(path)?.hash(&mut hash); } else { std::fs::read(path)?.hash(&mut hash); } } Err(error) if error.kind() == std::io::ErrorKind::NotFound => { "deleted".hash(&mut hash); } Err(error) => return Err(error.into()), } } Ok(format!("{:016x}\n", hash.finish())) } pub fn cwd_init() { static INIT: OnceLock<()> = OnceLock::new(); INIT.get_or_init(|| { let _guard = BUILD.lock().unwrap(); let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland"); if !userland.join("cwd-init/init.o").is_file() { assert!( Command::new("nu") .args(["--no-config-file", "cwd-init/build.nu"]) .current_dir(userland) .status() .unwrap() .success() ); } }); } pub fn build_brush() -> std::io::Result { let _guard = BUILD.lock().unwrap(); Command::new("nu") .args(["--no-config-file", "brush/build.nu"]) .env("CARGO_TARGET_DIR", target_dir()) .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland")) .status() } pub fn build_git() -> &'static Path { static GIT: OnceLock = OnceLock::new(); GIT.get_or_init(|| { let userland = Path::new(env!("CARGO_MANIFEST_DIR")).join("../userland"); let target = target_dir(); cwd_init(); assert!( Command::new("cargo") .args([ "build", "--release", "-p", "git", "-p", "sausage-archive-tools", "--manifest-path" ]) .arg(userland.join("../Cargo.toml")) .args(["--target", "wasm32-wasip2", "--target-dir"]) .arg(&target) .args(["-j", "2", "--locked"]) .current_dir(&userland) .status() .unwrap() .success() ); target }) }