mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-24 20:28:06 +02:00
[pitboss] phase 04: M4 — Rust harness (second-language validation)
This commit is contained in:
parent
e875aa1208
commit
3ffe480660
37 changed files with 1872 additions and 54 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue