mod aggregate;
mod artifacts;
mod docker;
mod drift;
mod env;
mod installer_detect;
mod preserve;
mod report;
mod run;
mod stage_id;
pub use installer_detect::installer_stages;
pub use stage_id::StageId;
use anodizer_core::determinism::AggregateKind;
use anodizer_core::git::worktree::Worktree;
use anodizer_core::harness_signing::EphemeralSigningKeys;
use anodizer_core::log::{StageLogger, Verbosity};
use anodizer_core::{AllowList, ArtifactRow, CURRENT_SCHEMA_VERSION, DeterminismReport, DriftRow};
use anyhow::{Context, Result};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use strum::{EnumIter, IntoEnumIterator};
use artifacts::{
ArtifactInfo, copy_artifacts_to_dump, discover_artifacts, hash_artifacts,
infer_stage_from_path, prune_dump_to_drifted,
};
use drift::{inject_drift_byte, pick_first_artifact_for_stage, summarize_drift};
use env::{BuildSubprocessEnv, build_subprocess_env};
use preserve::{
ContextInputs, preserve_dist_tree, preserve_raw_binaries, remove_preserved_on_drift,
write_preserved_dist_context,
};
const PRESERVE_SET: &[&str] = &["validate", "before", "templatefiles"];
fn stage_requires_binary(stage: StageId) -> bool {
match stage {
StageId::Source | StageId::Srpm | StageId::CargoPackage => false,
StageId::InstallScript => false,
StageId::Build => false,
StageId::Upx
| StageId::Archive
| StageId::Nfpm
| StageId::Makeself
| StageId::Snapcraft
| StageId::Sbom
| StageId::Sign
| StageId::Checksum
| StageId::Docker
| StageId::Msi
| StageId::Nsis
| StageId::Dmg
| StageId::Pkg
| StageId::Appbundle
| StageId::Appimage
| StageId::Flatpak => true,
}
}
fn compute_extra_skip(requested: &[StageId]) -> Vec<String> {
use anodizer_core::context::VALID_RELEASE_SKIPS;
use anodizer_core::determinism_runner::SIDE_EFFECT_STAGES;
let requested_names: BTreeSet<&str> = requested.iter().map(|s| s.as_str()).collect();
let force_build = requested.iter().copied().any(stage_requires_binary);
VALID_RELEASE_SKIPS
.iter()
.copied()
.filter(|name| !requested_names.contains(name))
.filter(|name| !(force_build && *name == "build"))
.filter(|name| !PRESERVE_SET.contains(name))
.filter(|name| !SIDE_EFFECT_STAGES.contains(name))
.map(str::to_string)
.collect()
}
fn matches_artifact_pattern(pattern: &str, artifact: &str) -> bool {
if let Some(suffix) = pattern.strip_prefix('*') {
return artifact.ends_with(suffix);
}
pattern == artifact
}
fn stage_docker_context(
worktree_path: &Path,
context_dir: &Path,
docker_cfg: &ResolvedDockerConfig,
log: &StageLogger,
) -> Result<usize> {
use anodizer_core::target::map_target;
let _ = std::fs::remove_dir_all(context_dir);
std::fs::create_dir_all(context_dir)
.with_context(|| format!("creating docker staging dir {}", context_dir.display()))?;
let mut staged = 0usize;
for (triple, bin_path) in discover_per_triple_binaries(worktree_path)? {
let (os, arch) = map_target(&triple);
let file_name = bin_path
.file_name()
.map(std::ffi::OsStr::to_os_string)
.unwrap_or_else(|| std::ffi::OsString::from("anodizer"));
let dest_dir = context_dir.join(&os).join(&arch);
std::fs::create_dir_all(&dest_dir)
.with_context(|| format!("creating staging platform dir {}", dest_dir.display()))?;
let dest = dest_dir.join(&file_name);
std::fs::copy(&bin_path, &dest)
.with_context(|| format!("staging {} → {}", bin_path.display(), dest.display()))?;
log.verbose(&format!(
"staged {} → {}",
bin_path.display(),
dest.display()
));
staged += 1;
}
let dockerfile_abs = worktree_path.join(&docker_cfg.dockerfile);
anodizer_stage_docker::copy_dockerfile(
&dockerfile_abs.to_string_lossy(),
context_dir,
false,
log,
"determinism docker",
)?;
if !docker_cfg.extra_files.is_empty() {
anodizer_stage_docker::stage_extra_files(
&docker_cfg.extra_files,
context_dir,
Some(worktree_path),
false,
log,
"determinism docker",
)?;
}
Ok(staged)
}
fn discover_per_triple_binaries(worktree_path: &Path) -> Result<Vec<(String, PathBuf)>> {
let target_root = worktree_path.join(".det-tmp").join("target");
let entries = match std::fs::read_dir(&target_root) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(e).with_context(|| format!("reading {}", target_root.display())),
};
let mut out = Vec::new();
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let name = entry.file_name();
let triple = name.to_string_lossy();
if triple == "release" || triple == "debug" || triple.starts_with('.') {
continue;
}
let release_dir = entry.path().join("release");
if !release_dir.is_dir() {
continue;
}
for bin in std::fs::read_dir(&release_dir)
.with_context(|| format!("reading {}", release_dir.display()))?
{
let bin = bin?;
if !bin.file_type()?.is_file() {
continue;
}
let path = bin.path();
if path
.file_name()
.and_then(|s| s.to_str())
.is_some_and(|n| n.starts_with('.'))
{
continue;
}
match path.extension().and_then(|s| s.to_str()) {
None | Some("exe") => out.push((triple.to_string(), path)),
_ => continue,
}
}
}
out.sort();
Ok(out)
}
#[derive(Debug, Clone)]
pub struct ResolvedDockerConfig {
pub dockerfile: String,
pub extra_files: Vec<String>,
pub build_args: Vec<(String, String)>,
}
pub struct Harness {
pub repo_root: PathBuf,
pub commit: String,
pub stages: Vec<StageId>,
pub explicit_stages: Vec<StageId>,
pub require_tools: bool,
pub runs: u32,
pub sde: i64,
pub allowlist: AllowList,
pub report_path: PathBuf,
pub inject_drift: Option<String>,
pub targets: Option<Vec<String>>,
pub preserve_dist: Option<PathBuf>,
pub version_hint: String,
pub child_snapshot: bool,
pub verbosity: Verbosity,
pub docker_backend_hint: Option<String>,
pub docker_configs: Vec<ResolvedDockerConfig>,
pub docker_declared: bool,
pub crate_name: Option<String>,
pub config_tools: BTreeMap<StageId, Vec<String>>,
pub disk_abs_floor_bytes: u64,
pub disk_safety_factor: f64,
}
fn require_c_toolchain<P>(targets: &[String], host_is_windows_msvc: bool, probe: P) -> Result<()>
where
P: Fn(&str) -> bool,
{
let needs_clang_cl = if targets.is_empty() {
host_is_windows_msvc
} else {
targets
.iter()
.any(|t| anodizer_core::target::is_windows_msvc(t))
};
if needs_clang_cl && !probe("clang-cl") {
anyhow::bail!(
"windows-msvc determinism requires `clang-cl` (LLVM) on PATH for byte-reproducible \
C objects (zstd-sys/ring/aws-lc-sys/…); install LLVM or add its bin dir to PATH"
);
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Classification {
Aggregate,
Sidecar,
Primary,
Unclassified,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AggregateVerdict {
Excused(String),
Regression(Vec<String>),
FailClosed(String),
}
fn strip_sidecar_suffix(name: &str) -> Option<&str> {
name.strip_suffix(".sha256")
.or_else(|| name.strip_suffix(".sig"))
}
fn basename(name: &str) -> &str {
name.rsplit(['/', '\\']).next().unwrap_or(name)
}
#[cfg(test)]
mod tests;