//! Post-build correctness check: runs the freshly built binary and //! confirms it reports the version pkgwatch believes it just built. Not a //! security control — see docs/SPEC.md > Verification trust tiers. use crate::config::SanityCheck; use anyhow::{Context, Result, bail}; use regex::Regex; use std::path::Path; use std::process::Command; /// Runs `check.command` with `pkg_bin_dir` prepended to `PATH`, so it /// exercises the binary pkgwatch just built (still sitting in makepkg's /// package staging directory, not installed system-wide) rather than /// whatever's already on the system. Confirms `check.version_regex`'s /// capture group matches `expected_version`. /// /// Correctness check only, not a security control — see docs/SPEC.md > /// Verification trust tiers. Catches checker bugs and mangled/wrong-asset /// downloads, not malicious releases. pub fn run(check: &SanityCheck, pkg_bin_dir: &Path, expected_version: &str) -> Result<()> { let path_env = format!( "{}:{}", pkg_bin_dir.display(), std::env::var("PATH").unwrap_or_default() ); let output = Command::new("sh") .arg("-c") .arg(&check.command) .env("PATH", path_env) .output() .with_context(|| format!("running sanity check command '{}'", check.command))?; if !output.status.success() { bail!( "sanity check command '{}' exited with {}: {}", check.command, output.status, String::from_utf8_lossy(&output.stderr).trim() ); } let combined = format!( "{}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let re = Regex::new(&check.version_regex) .with_context(|| format!("invalid version_regex '{}'", check.version_regex))?; let found = re .captures(&combined) .and_then(|caps| caps.get(1)) .with_context(|| { format!( "version_regex '{}' did not match sanity check output: {combined:?}", check.version_regex ) })? .as_str(); if found != expected_version { bail!( "sanity check reported version '{found}', pkgwatch built '{expected_version}' — mismatch" ); } Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::test_support::write_executable_script as write_fake_binary; #[test] fn run_passes_when_reported_version_matches() { let dir = tempfile::tempdir().unwrap(); write_fake_binary(dir.path(), "uv", "echo 'uv 0.12.15 (abc 2026-09-01)'"); let check = SanityCheck { command: "uv --version".to_string(), version_regex: r"uv (\d+\.\d+\.\d+)".to_string(), }; assert!(run(&check, dir.path(), "0.12.15").is_ok()); } #[test] fn run_fails_when_reported_version_differs() { let dir = tempfile::tempdir().unwrap(); write_fake_binary(dir.path(), "uv", "echo 'uv 0.12.14 (abc 2026-08-01)'"); let check = SanityCheck { command: "uv --version".to_string(), version_regex: r"uv (\d+\.\d+\.\d+)".to_string(), }; let err = run(&check, dir.path(), "0.12.15").unwrap_err(); assert!(err.to_string().contains("mismatch")); } #[test] fn run_fails_when_command_exits_nonzero() { let dir = tempfile::tempdir().unwrap(); write_fake_binary(dir.path(), "uv", "exit 1"); let check = SanityCheck { command: "uv --version".to_string(), version_regex: r"uv (\d+\.\d+\.\d+)".to_string(), }; let err = run(&check, dir.path(), "0.12.15").unwrap_err(); assert!(err.to_string().contains("exited with")); } #[test] fn run_fails_when_output_does_not_match_regex() { let dir = tempfile::tempdir().unwrap(); write_fake_binary(dir.path(), "uv", "echo 'not a version'"); let check = SanityCheck { command: "uv --version".to_string(), version_regex: r"uv (\d+\.\d+\.\d+)".to_string(), }; let err = run(&check, dir.path(), "0.12.15").unwrap_err(); assert!(err.to_string().contains("did not match")); } }