[pitboss] phase 04: M4 — Rust harness (second-language validation)

This commit is contained in:
pitboss 2026-05-12 00:57:45 -04:00
parent e875aa1208
commit 3ffe480660
37 changed files with 1872 additions and 54 deletions

View file

@ -19,6 +19,125 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
// ── Rust build sandbox ────────────────────────────────────────────────────────
/// Prepare a compiled Rust binary for `spec`.
///
/// Checks a build cache keyed on `(Cargo.lock hash, "rust", toolchain_id)`.
/// On a cache hit returns immediately; otherwise runs `cargo build --release`
/// in `workdir` and caches the resulting binary.
///
/// The compiled binary is at `cache_path/nyx_harness` on success.
///
/// Build isolation is NOT yet implemented (deferred to Phase 05). `cargo build`
/// runs as a plain subprocess on the host with `env_clear()` plus a minimal
/// inherited env (PATH/HOME/CARGO_HOME/RUSTUP_HOME). A malicious `build.rs`
/// runs with host privileges. Vendoring / network sandboxing comes later (§19.2).
pub fn prepare_rust(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, BuildError> {
let lockfile_hash = compute_rust_lockfile_hash(workdir);
let cache_path = build_cache_path(&lockfile_hash, "rust", &spec.toolchain_id)?;
// Cache hit: binary already compiled and stored.
let binary = cache_path.join("nyx_harness");
if binary.exists() {
return Ok(BuildResult { venv_path: cache_path, cache_hit: true, duration: Duration::ZERO });
}
let start = Instant::now();
const MAX_ATTEMPTS: u32 = 2;
const BACKOFF: [u64; 2] = [1, 4];
let mut last_err = String::new();
for attempt in 0..MAX_ATTEMPTS {
if attempt > 0 {
std::thread::sleep(Duration::from_secs(BACKOFF[attempt as usize - 1]));
}
let _ = std::fs::remove_dir_all(&cache_path);
std::fs::create_dir_all(&cache_path)?;
match try_build_rust_binary(workdir, &binary) {
Ok(()) => {
return Ok(BuildResult {
venv_path: cache_path,
cache_hit: false,
duration: start.elapsed(),
});
}
Err(e) => {
last_err = e;
let _ = std::fs::remove_file(&binary);
}
}
}
Err(BuildError::BuildFailed { stderr: last_err, attempts: MAX_ATTEMPTS })
}
fn try_build_rust_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String> {
let cargo = cargo_binary();
// Run `cargo build --release` in the workdir.
let output = Command::new(&cargo)
.args(["build", "--release"])
.current_dir(workdir)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
// Inherit CARGO_HOME so the local registry cache is reused.
.env("CARGO_HOME", std::env::var("CARGO_HOME").unwrap_or_else(|_| {
dirs_next_cargo_home()
}))
.env("RUSTUP_HOME", std::env::var("RUSTUP_HOME").unwrap_or_default())
.output()
.map_err(|e| format!("cargo build: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
return Err(stderr);
}
// Copy binary to cache location.
let compiled = workdir.join("target").join("release").join("nyx_harness");
if compiled.exists() {
std::fs::copy(&compiled, binary_dest)
.map_err(|e| format!("copy binary: {e}"))?;
}
Ok(())
}
fn cargo_binary() -> String {
// Respect NYX_CARGO_BIN for testing.
std::env::var("NYX_CARGO_BIN").unwrap_or_else(|_| "cargo".to_owned())
}
fn dirs_next_cargo_home() -> String {
// ~/.cargo is the default CARGO_HOME.
std::env::var("HOME")
.map(|h| format!("{h}/.cargo"))
.unwrap_or_else(|_| ".cargo".to_owned())
}
fn compute_rust_lockfile_hash(workdir: &Path) -> String {
let mut h = Hasher::new();
// Cargo manifest and lock determine dependency graph.
for fname in &["Cargo.lock", "Cargo.toml"] {
if let Ok(content) = std::fs::read(workdir.join(fname)) {
h.update(fname.as_bytes());
h.update(&content);
}
}
// Entry file is compiled into the binary, so it must be part of the cache key.
// Without this, two fixtures with the same Cargo.toml but different entry.rs
// would collide and the second would receive the wrong cached binary.
if let Ok(content) = std::fs::read(workdir.join("src").join("entry.rs")) {
h.update(b"src/entry.rs");
h.update(&content);
}
let out = h.finalize();
format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()))
}
/// Result of a successful build.
#[derive(Debug, Clone)]
pub struct BuildResult {