use super::StageId;
use anodizer_core::tool_detect::tool_available;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(super) struct InstallerToolGate {
pub available: Vec<StageId>,
pub skipped: Vec<(StageId, &'static str)>,
}
fn stage_primary_tool(stage: StageId) -> Option<&'static str> {
match stage {
StageId::Nfpm => Some("nfpm"),
StageId::Makeself => Some("makeself"),
StageId::Srpm => Some("rpmbuild"),
StageId::Msi => Some("wix"),
StageId::Nsis => Some("makensis"),
StageId::Dmg => Some(if cfg!(target_os = "macos") {
"hdiutil"
} else {
"mkisofs"
}),
StageId::Pkg => Some("pkgbuild"),
_ => None,
}
}
pub fn installer_stages() -> Vec<StageId> {
vec![
StageId::Nfpm,
StageId::Makeself,
StageId::Srpm,
StageId::Msi,
StageId::Nsis,
StageId::Dmg,
StageId::Pkg,
]
}
#[cfg(test)]
pub(super) fn is_installer_stage(stage: StageId) -> bool {
stage_primary_tool(stage).is_some()
}
pub(super) fn filter_available_installer_stages(requested: &[StageId]) -> InstallerToolGate {
filter_available_with_probe(requested, |tool| tool_available(tool).unwrap_or(false))
}
fn filter_available_with_probe<P>(requested: &[StageId], probe: P) -> InstallerToolGate
where
P: Fn(&str) -> bool,
{
let mut gate = InstallerToolGate::default();
for &stage in requested {
match stage_primary_tool(stage) {
None => gate.available.push(stage),
Some(tool) => {
if probe(tool) {
gate.available.push(stage);
} else {
gate.skipped.push((stage, tool));
}
}
}
}
gate
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn installer_stages_covers_every_installer_family() {
let stages = installer_stages();
assert_eq!(stages.len(), 7);
for stage in stages {
assert!(
stage_primary_tool(stage).is_some(),
"installer_stages() emitted non-installer stage {:?}",
stage
);
}
}
#[test]
fn non_installer_stages_pass_through() {
let req = vec![StageId::Build, StageId::Archive, StageId::Checksum];
let gate = filter_available_installer_stages(&req);
assert_eq!(gate.available, req);
assert!(gate.skipped.is_empty());
}
#[test]
fn well_formed_partition_on_every_requested_stage() {
let req = installer_stages();
let gate = filter_available_installer_stages(&req);
assert_eq!(
gate.available.len() + gate.skipped.len(),
req.len(),
"every requested stage must land in exactly one bucket"
);
for (stage, tool) in &gate.skipped {
assert!(
is_installer_stage(*stage),
"skipped entry references non-installer stage {:?}",
stage
);
assert!(!tool.is_empty(), "missing-tool name must be non-empty");
}
}
#[test]
fn missing_tool_routes_every_installer_to_skipped() {
let req = vec![
StageId::Build, StageId::Nfpm,
StageId::Makeself,
StageId::Msi,
StageId::Dmg,
StageId::Archive, ];
let gate = filter_available_with_probe(&req, |_| false);
assert_eq!(
gate.available,
vec![StageId::Build, StageId::Archive],
"non-installer stages must pass through even with missing probe"
);
let skipped_stages: Vec<StageId> = gate.skipped.iter().map(|(s, _)| *s).collect();
assert_eq!(
skipped_stages,
vec![StageId::Nfpm, StageId::Makeself, StageId::Msi, StageId::Dmg],
"installer stages must land in `skipped` when their tool is missing"
);
let dmg_tool = if cfg!(target_os = "macos") {
"hdiutil"
} else {
"mkisofs"
};
assert_eq!(
gate.skipped.iter().map(|(_, t)| *t).collect::<Vec<_>>(),
vec!["nfpm", "makeself", "wix", dmg_tool],
"each skipped entry must carry its primary-tool name"
);
}
#[test]
fn present_tool_routes_every_installer_to_available() {
let req = installer_stages();
let gate = filter_available_with_probe(&req, |_| true);
assert_eq!(gate.available, req);
assert!(gate.skipped.is_empty());
}
#[test]
fn is_installer_stage_matches_primary_tool_table() {
for stage in installer_stages() {
assert!(is_installer_stage(stage));
}
for stage in [
StageId::Build,
StageId::Source,
StageId::Archive,
StageId::Sbom,
StageId::Sign,
StageId::Checksum,
StageId::CargoPackage,
StageId::Docker,
StageId::Snapcraft,
StageId::Upx,
] {
assert!(
!is_installer_stage(stage),
"is_installer_stage({:?}) should be false",
stage
);
}
}
}