use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Duration;
pub const SIDE_EFFECT_STAGES: &[&str] = &[
"release",
"docker",
"docker-sign",
"publish",
"blob",
"snapcraft-publish",
"announce",
"verify-release",
];
pub fn compute_skip_arg(extra: &[&str]) -> String {
let mut merged: Vec<&str> = Vec::with_capacity(SIDE_EFFECT_STAGES.len() + extra.len());
for &name in SIDE_EFFECT_STAGES {
if !merged.contains(&name) {
merged.push(name);
}
}
for &name in extra {
if !merged.contains(&name) {
merged.push(name);
}
}
format!("--skip={}", merged.join(","))
}
pub fn run_build_pipeline_subprocess(spec: &ChildInvocation<'_>) -> Result<()> {
let mut cmd = build_subprocess_command(spec);
tracing::debug!(
args = ?cmd.get_args(),
worktree = %spec.worktree_path.display(),
"spawning anodize release child for determinism harness",
);
let status = cmd
.status()
.context("spawning anodize release for determinism harness")?;
anyhow::ensure!(
status.success(),
"harness build pipeline failed in worktree {} (exit {:?})",
spec.worktree_path.display(),
status.code()
);
Ok(())
}
pub struct ChildInvocation<'a> {
pub anodize_binary: &'a Path,
pub worktree_path: &'a Path,
pub env: &'a HashMap<String, String>,
pub targets: Option<&'a [String]>,
pub extra_skip: &'a [String],
pub snapshot: bool,
pub crate_name: Option<&'a str>,
pub verbosity: crate::log::Verbosity,
}
fn build_subprocess_command(spec: &ChildInvocation<'_>) -> Command {
let ChildInvocation {
anodize_binary,
worktree_path,
env,
targets,
extra_skip,
snapshot,
crate_name,
verbosity,
} = *spec;
let mut cmd = Command::new(anodize_binary);
let extra_refs: Vec<&str> = extra_skip.iter().map(String::as_str).collect();
cmd.arg("release");
if snapshot {
cmd.arg("--snapshot");
}
cmd.arg("--rollback").arg("none");
cmd.arg("--no-failure-policy");
match verbosity {
crate::log::Verbosity::Quiet => {
cmd.arg("--quiet");
}
crate::log::Verbosity::Verbose => {
cmd.arg("--verbose");
}
crate::log::Verbosity::Debug => {
cmd.arg("--debug");
}
crate::log::Verbosity::Normal => {}
}
cmd.arg(compute_skip_arg(&extra_refs));
cmd.arg("--no-preflight");
if let Some(list) = targets
&& !list.is_empty()
{
cmd.arg(format!("--targets={}", list.join(",")));
}
if let Some(name) = crate_name {
cmd.arg(format!("--crate={name}"));
}
cmd.current_dir(worktree_path);
cmd.env_clear();
for (k, v) in env {
cmd.env(k, v);
}
cmd.stdout(Stdio::null());
cmd.env(
crate::log::LOG_DEPTH_ENV,
(crate::log::current_depth() + 2).to_string(),
);
cmd
}
pub fn current_anodize_binary() -> Result<PathBuf> {
std::env::current_exe().context("locating the currently-running anodize binary")
}
const PREFETCH_ATTEMPTS: u32 = 3;
const PREFETCH_BACKOFF: Duration = Duration::from_secs(3);
pub fn prefetch_deps(manifest_dir: &Path, cargo_home: &Path) -> Result<()> {
let lock = manifest_dir.join("Cargo.lock");
let lock_committed = lock.is_file();
let res = prefetch_deps_with(
manifest_dir,
cargo_home,
PREFETCH_ATTEMPTS,
PREFETCH_BACKOFF,
);
if !lock_committed && lock.is_file() {
let _ = std::fs::remove_file(&lock);
}
res
}
fn build_fetch_command(manifest_dir: &Path, cargo_home: &Path) -> Command {
let mut cmd = Command::new("cargo");
cmd.arg("fetch");
if manifest_dir.join("Cargo.lock").is_file() {
cmd.arg("--locked");
}
cmd.arg("--manifest-path")
.arg(manifest_dir.join("Cargo.toml"));
cmd.current_dir(manifest_dir);
cmd.env("CARGO_HOME", cargo_home);
cmd.env("CARGO_NET_OFFLINE", "false");
cmd.stdout(Stdio::null());
cmd
}
fn prefetch_deps_with(
manifest_dir: &Path,
cargo_home: &Path,
attempts: u32,
backoff: Duration,
) -> Result<()> {
let attempts = attempts.max(1);
let mut last: Option<String> = None;
for attempt in 1..=attempts {
let mut cmd = build_fetch_command(manifest_dir, cargo_home);
match cmd.status() {
Ok(status) if status.success() => return Ok(()),
Ok(status) => last = Some(format!("cargo fetch exited {:?}", status.code())),
Err(e) => last = Some(format!("spawning cargo fetch: {e}")),
}
if attempt < attempts {
tracing::warn!(
attempt,
attempts,
"determinism prefetch `cargo fetch` failed; retrying"
);
std::thread::sleep(backoff);
}
}
anyhow::bail!(
"determinism prefetch `cargo fetch` failed after {attempts} attempt(s) in {}: {}",
manifest_dir.display(),
last.unwrap_or_default()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_binary_resolves_to_a_real_file() {
let p = current_anodize_binary().unwrap();
assert!(p.exists(), "current_exe should point at a real file");
}
#[test]
fn run_build_pipeline_subprocess_fails_when_binary_missing() {
let env = HashMap::new();
let worktree = std::env::temp_dir();
let bogus = PathBuf::from("/nonexistent/anodize-binary-for-tests");
let res = run_build_pipeline_subprocess(&ChildInvocation {
anodize_binary: &bogus,
worktree_path: &worktree,
env: &env,
targets: None,
extra_skip: &[],
snapshot: true,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
assert!(
res.is_err(),
"missing binary should surface as an error, not a panic"
);
}
#[test]
fn subprocess_command_omits_targets_when_none() {
let env = HashMap::new();
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: None,
extra_skip: &[],
snapshot: true,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
let args: Vec<&str> = cmd.get_args().map(|s| s.to_str().expect("ascii")).collect();
assert!(
args.iter().all(|a| !a.starts_with("--targets")),
"expected no --targets argument; got {args:?}"
);
assert!(
args.contains(&"--snapshot"),
"argv missing --snapshot: {args:?}"
);
assert!(
args.iter().any(|a| a.starts_with("--skip=")),
"argv missing --skip=...: {args:?}"
);
}
#[test]
fn subprocess_command_propagates_targets_csv() {
let env = HashMap::new();
let triples = vec![
"x86_64-apple-darwin".to_string(),
"aarch64-apple-darwin".to_string(),
];
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: Some(&triples),
extra_skip: &[],
snapshot: true,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
let args: Vec<String> = cmd
.get_args()
.map(|s| s.to_str().expect("ascii").to_string())
.collect();
assert!(
args.iter()
.any(|a| a == "--targets=x86_64-apple-darwin,aarch64-apple-darwin"),
"expected joined --targets= argument; got {args:?}"
);
}
#[test]
fn subprocess_command_drops_targets_when_list_is_empty() {
let env = HashMap::new();
let empty: Vec<String> = Vec::new();
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: Some(&empty),
extra_skip: &[],
snapshot: true,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
let args: Vec<String> = cmd
.get_args()
.map(|s| s.to_str().expect("ascii").to_string())
.collect();
assert!(
args.iter().all(|a| !a.starts_with("--targets")),
"empty target slice should omit --targets entirely; got {args:?}"
);
}
#[test]
fn subprocess_command_always_disables_env_preflight() {
let env = HashMap::new();
for snapshot in [true, false] {
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: None,
extra_skip: &[],
snapshot,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
let args: Vec<&str> = cmd.get_args().map(|s| s.to_str().expect("ascii")).collect();
assert!(
args.contains(&"--no-preflight"),
"child argv (snapshot={snapshot}) must always carry --no-preflight; got {args:?}"
);
}
}
#[test]
fn subprocess_command_always_disables_rollback() {
let env = HashMap::new();
for snapshot in [true, false] {
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: None,
extra_skip: &[],
snapshot,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
let args: Vec<&str> = cmd.get_args().map(|s| s.to_str().expect("ascii")).collect();
let pos = args
.iter()
.position(|a| *a == "--rollback")
.unwrap_or_else(|| {
panic!("child argv (snapshot={snapshot}) must carry --rollback; got {args:?}")
});
assert_eq!(
args.get(pos + 1),
Some(&"none"),
"child argv (snapshot={snapshot}) must pass `--rollback none`; got {args:?}"
);
assert!(
args.contains(&"--no-failure-policy"),
"child argv (snapshot={snapshot}) must carry --no-failure-policy; got {args:?}"
);
}
}
#[test]
fn subprocess_command_always_exports_log_depth() {
let mut env = HashMap::new();
env.insert(crate::log::LOG_DEPTH_ENV.to_string(), "99".to_string());
for snapshot in [true, false] {
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: None,
extra_skip: &[],
snapshot,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
let depth = cmd
.get_envs()
.find(|(k, _)| *k == std::ffi::OsStr::new(crate::log::LOG_DEPTH_ENV))
.and_then(|(_, v)| v)
.and_then(|v| v.to_str())
.map(str::to_string);
let parsed: usize = depth
.as_deref()
.unwrap_or_else(|| {
panic!(
"child env (snapshot={snapshot}) must carry {}",
crate::log::LOG_DEPTH_ENV
)
})
.parse()
.expect("depth var must be numeric");
assert!(
parsed >= 2,
"depth must be parent depth + 2 (>= 2); got {parsed}"
);
}
}
#[test]
fn subprocess_command_drops_snapshot_when_disabled() {
let env = HashMap::new();
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: None,
extra_skip: &[],
snapshot: false,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
let args: Vec<&str> = cmd.get_args().map(|s| s.to_str().expect("ascii")).collect();
assert!(
!args.contains(&"--snapshot"),
"snapshot=false should drop --snapshot; got {args:?}"
);
assert!(
args.iter().any(|a| a.starts_with("--skip=")),
"argv still needs --skip=...: {args:?}"
);
assert_eq!(args[0], "release", "argv must lead with `release`");
}
#[test]
fn subprocess_command_scopes_to_crate_when_named() {
let env = HashMap::new();
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: None,
extra_skip: &[],
snapshot: true,
crate_name: Some("cfgd"),
verbosity: crate::log::Verbosity::Normal,
});
let args: Vec<String> = cmd
.get_args()
.map(|s| s.to_str().expect("ascii").to_string())
.collect();
assert!(
args.iter().any(|a| a == "--crate=cfgd"),
"expected --crate=cfgd to scope the child build; got {args:?}"
);
}
#[test]
fn subprocess_command_omits_crate_when_none() {
let env = HashMap::new();
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: None,
extra_skip: &[],
snapshot: true,
crate_name: None,
verbosity: crate::log::Verbosity::Normal,
});
let args: Vec<String> = cmd
.get_args()
.map(|s| s.to_str().expect("ascii").to_string())
.collect();
assert!(
args.iter().all(|a| !a.starts_with("--crate")),
"no crate named: --crate must be omitted; got {args:?}"
);
}
#[test]
fn subprocess_command_forwards_verbosity_flag() {
let env = HashMap::new();
let argv_for = |verbosity: crate::log::Verbosity| -> Vec<String> {
let cmd = build_subprocess_command(&ChildInvocation {
anodize_binary: &PathBuf::from("/usr/bin/anodize"),
worktree_path: &std::env::temp_dir(),
env: &env,
targets: None,
extra_skip: &[],
snapshot: true,
crate_name: None,
verbosity,
});
cmd.get_args()
.map(|s| s.to_str().expect("ascii").to_string())
.collect()
};
let verbosity_flags = ["--quiet", "--verbose", "--debug"];
for (verbosity, expected) in [
(crate::log::Verbosity::Quiet, Some("--quiet")),
(crate::log::Verbosity::Verbose, Some("--verbose")),
(crate::log::Verbosity::Debug, Some("--debug")),
(crate::log::Verbosity::Normal, None),
] {
let args = argv_for(verbosity);
let present: Vec<&String> = args
.iter()
.filter(|a| verbosity_flags.contains(&a.as_str()))
.collect();
match expected {
Some(flag) => assert_eq!(
present,
vec![flag],
"{verbosity:?} must forward exactly {flag}; got {args:?}"
),
None => assert!(
present.is_empty(),
"Normal must forward no verbosity flag; got {args:?}"
),
}
}
}
#[test]
fn fetch_command_pins_locked_home_and_manifest() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("Cargo.lock"), "# lock").unwrap();
let home = PathBuf::from("/cache/cargo");
let cmd = build_fetch_command(dir.path(), &home);
let args: Vec<String> = cmd
.get_args()
.map(|s| s.to_str().expect("ascii").to_string())
.collect();
assert_eq!(
args[0], "fetch",
"argv must lead with `fetch`; got {args:?}"
);
assert!(
args.contains(&"--locked".to_string()),
"a committed Cargo.lock must yield --locked: {args:?}"
);
let mp = args
.iter()
.position(|a| a == "--manifest-path")
.expect("--manifest-path present");
assert_eq!(
args.get(mp + 1).map(String::as_str),
Some(dir.path().join("Cargo.toml").to_str().unwrap()),
"manifest path must point at the worktree's Cargo.toml; got {args:?}"
);
let env: HashMap<String, String> = cmd
.get_envs()
.filter_map(|(k, v)| Some((k.to_str()?.to_string(), v?.to_str()?.to_string())))
.collect();
assert_eq!(
env.get("CARGO_HOME").map(String::as_str),
Some("/cache/cargo")
);
assert_eq!(
env.get("CARGO_NET_OFFLINE").map(String::as_str),
Some("false"),
"prefetch must stay online; the rebuilds are what get sealed offline"
);
}
#[test]
fn fetch_command_omits_locked_without_committed_lock() {
let dir = tempfile::tempdir().unwrap();
let cmd = build_fetch_command(dir.path(), &PathBuf::from("/c"));
let args: Vec<String> = cmd
.get_args()
.map(|s| s.to_str().expect("ascii").to_string())
.collect();
assert!(
args.iter().all(|a| a != "--locked"),
"no committed lock → --locked must be omitted; got {args:?}"
);
}
#[test]
fn fetch_command_never_narrows_to_a_target() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("Cargo.lock"), "# lock").unwrap();
let cmd = build_fetch_command(dir.path(), &PathBuf::from("/c"));
let args: Vec<String> = cmd
.get_args()
.map(|s| s.to_str().expect("ascii").to_string())
.collect();
assert!(
args.iter().all(|a| a != "--target"),
"prefetch must fetch the all-platform superset (no --target); got {args:?}"
);
}
#[test]
fn prefetch_fails_after_exhausting_attempts() {
let res = prefetch_deps_with(
&PathBuf::from("/nonexistent/anodize-prefetch-test-dir"),
&PathBuf::from("/nonexistent/cargo-home"),
1,
Duration::ZERO,
);
let err = res.expect_err("fetch against a missing manifest must error");
assert!(
err.to_string().contains("determinism prefetch"),
"error must identify the prefetch step; got {err:#}"
);
}
#[test]
fn side_effect_stages_covers_every_known_publish_side_effect() {
let expected = [
"release",
"docker",
"docker-sign",
"publish",
"blob",
"snapcraft-publish",
"announce",
"verify-release",
];
for name in expected {
assert!(
SIDE_EFFECT_STAGES.contains(&name),
"SIDE_EFFECT_STAGES missing known publish-side stage `{name}`"
);
}
}
#[test]
fn compute_skip_arg_starts_with_skip_flag() {
let arg = compute_skip_arg(&[]);
assert!(
arg.starts_with("--skip="),
"expected --skip= prefix, got `{arg}`"
);
assert!(arg.len() > "--skip=".len(), "skip list must not be empty");
}
#[test]
fn compute_skip_arg_round_trips_through_comma_join() {
let arg = compute_skip_arg(&[]);
let list = arg
.trim_start_matches("--skip=")
.split(',')
.collect::<Vec<_>>();
assert_eq!(list.len(), SIDE_EFFECT_STAGES.len());
for (a, b) in list.iter().zip(SIDE_EFFECT_STAGES.iter()) {
assert_eq!(a, b);
}
}
#[test]
fn compute_skip_arg_includes_side_effects_and_extra() {
let extra = ["nfpm".to_string(), "msi".to_string(), "dmg".to_string()];
let extra_refs: Vec<&str> = extra.iter().map(String::as_str).collect();
let arg = compute_skip_arg(&extra_refs);
let list: Vec<&str> = arg.trim_start_matches("--skip=").split(',').collect();
for &name in SIDE_EFFECT_STAGES {
assert!(
list.contains(&name),
"merged skip list missing side-effect stage `{name}`: {list:?}"
);
}
for name in ["nfpm", "msi", "dmg"] {
assert!(
list.contains(&name),
"merged skip list missing extra stage `{name}`: {list:?}"
);
}
}
#[test]
fn compute_skip_arg_dedupes_overlap() {
let extra = ["release".to_string(), "nfpm".to_string()];
let extra_refs: Vec<&str> = extra.iter().map(String::as_str).collect();
let arg = compute_skip_arg(&extra_refs);
let list: Vec<&str> = arg.trim_start_matches("--skip=").split(',').collect();
let release_count = list.iter().filter(|&&s| s == "release").count();
assert_eq!(
release_count, 1,
"expected `release` exactly once in merged skip list, got {release_count} in {list:?}"
);
assert!(
list.contains(&"nfpm"),
"merged list missing extra entry `nfpm`: {list:?}"
);
}
#[test]
fn side_effect_stages_are_all_valid_release_skip_values() {
use crate::context::VALID_RELEASE_SKIPS;
for &name in SIDE_EFFECT_STAGES {
assert!(
VALID_RELEASE_SKIPS.contains(&name),
"SIDE_EFFECT_STAGES contains `{name}` but VALID_RELEASE_SKIPS does not — \
the harness would fail at `anodize release --skip=<list>` invocation. \
Add `{name}` to VALID_RELEASE_SKIPS in crates/core/src/context.rs."
);
}
}
}