use anyhow::{bail, Context, Result};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn audit(dir: &Path, release: bool, verbose: bool, target: Option<&str>) -> Result<()> {
let exe = std::env::current_exe()?;
let base = PathBuf::from("/tmp/corgi-audit");
fs::create_dir_all(&base)?;
let canonical = PathBuf::from("/Users/Shared/corgi-audit");
if canonical.symlink_metadata().is_ok() {
fs::remove_file(&canonical).or_else(|_| fs::remove_dir_all(&canonical)).ok();
}
let stores = [base.join("store-a"), base.join("store-b")];
for (i, s) in stores.iter().enumerate() {
eprintln!("corgi-audit: ===== build {}/2 (store {}) =====", i + 1, s.display());
if s.exists() {
fs::rename(s, &canonical).context("unparking audit store")?;
}
let mut c = Command::new(&exe);
c.arg("build").arg("--no-incremental").arg("--dir").arg(dir);
if release {
c.arg("--release");
}
if verbose {
c.arg("-v");
}
if let Some(t) = target {
c.args(["--target", t]);
}
c.env("CORGI_STORE", &canonical);
c.env_remove("CORGI_ALIAS");
let st = c.status().context("spawning audit build")?;
fs::rename(&canonical, s).context("parking audit store")?;
if !st.success() {
bail!("audit build {}/2 failed (see errors above)", i + 1);
}
}
let pool = |s: &Path| -> Result<BTreeMap<String, String>> {
let mut m = BTreeMap::new();
for e in fs::read_dir(s.join("pool"))? {
let p = e?.path();
let name = p.file_name().unwrap().to_string_lossy().into_owned();
m.insert(name, crate::store::sha256_file(&p)?);
}
Ok(m)
};
let a = pool(&stores[0])?;
let b = pool(&stores[1])?;
let mut identical = 0usize;
let mut diffs: Vec<String> = Vec::new();
let mut only: Vec<String> = Vec::new();
for (k, ha) in &a {
match b.get(k) {
Some(hb) if hb == ha => identical += 1,
Some(_) => diffs.push(k.clone()),
None => only.push(format!("{k} (store A only)")),
}
}
for k in b.keys() {
if !a.contains_key(k) {
only.push(format!("{k} (store B only)"));
}
}
eprintln!("corgi-audit: {identical} artifacts bit-identical across independent builds");
for k in &only {
eprintln!("corgi-audit: KEY INSTABILITY: {k} — action keys differed between runs");
}
for k in &diffs {
eprintln!("corgi-audit: NONDETERMINISTIC OUTPUT: {k}");
for (s, tag) in stores.iter().zip(["A", "B"]) {
let data = fs::read(s.join("pool").join(k))?;
let needle = s.display().to_string();
if data.windows(needle.len()).any(|w| w == needle.as_bytes()) {
eprintln!("corgi-audit: hint: store {tag}'s artifact embeds its own store path");
}
}
}
if only.is_empty() && diffs.is_empty() {
eprintln!("corgi-audit: PASS");
Ok(())
} else {
bail!(
"audit FAILED: {} unstable keys, {} nondeterministic artifacts",
only.len(),
diffs.len()
)
}
}