use super::*;
pub(super) fn commit_short(commit: &str) -> String {
commit.get(..7).unwrap_or(commit).to_string()
}
pub(super) fn emit_run_summary(
log: &StageLogger,
targets: Option<&[String]>,
stages: &[StageId],
runs: u32,
preserve_dist: Option<&std::path::Path>,
crate_name: Option<&str>,
) {
let targets_value = match targets {
Some(t) if !t.is_empty() => t.join(", "),
_ => "all (from config)".to_string(),
};
let stages_value = stages
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ");
let preserve_value = preserve_dist.map(|p| p.display().to_string());
let mut rows: Vec<(&str, &str)> = vec![("targets", targets_value.as_str())];
rows.push(("stages", stages_value.as_str()));
let runs_value = runs.to_string();
rows.push(("runs", runs_value.as_str()));
if let Some(ref v) = preserve_value {
rows.push(("preserve-dist", v.as_str()));
}
if let Some(name) = crate_name {
rows.push(("crate", name));
}
let key_width = rows.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
for (key, value) in rows {
log.kv(key, value, key_width);
}
}
pub(super) fn resolve_child_snapshot(snapshot: bool, no_snapshot: bool, head_at_tag: bool) -> bool {
if snapshot {
true
} else if no_snapshot {
false
} else {
!head_at_tag
}
}
pub(super) fn signature_allowlist_entries_from_config(
cfg: &anodizer_core::config::Config,
) -> Vec<AllowListEntry> {
anodizer_core::signature_assets::signature_asset_suffixes(cfg)
.into_iter()
.map(|suffix| AllowListEntry {
reason: format!(
"signature/certificate artifact ({suffix}): bytes vary by signer \
(cosign signs with a random ECDSA nonce / mints a fresh keyless cert); \
validate cryptographically via `cosign verify-blob` / `gpg --verify`, \
not byte-equality"
),
artifact: format!("*{suffix}"),
})
.collect()
}
pub(super) fn detect_docker_backend_hint(cfg: &anodizer_core::config::Config) -> Option<String> {
let mut saw_buildx = false;
let mut iter: Vec<&Option<String>> = Vec::new();
if let Some(ref defaults) = cfg.defaults
&& let Some(ref v2) = defaults.dockers_v2
{
iter.push(&v2.use_backend);
}
for c in cfg.crate_universe() {
if let Some(ref v2s) = c.dockers_v2 {
for v in v2s {
iter.push(&v.use_backend);
}
}
}
for opt in iter {
match opt.as_deref() {
Some("podman") => return Some("podman".to_string()),
Some("buildx") | None => saw_buildx = true,
Some(_) => {}
}
}
if saw_buildx {
Some("buildx".to_string())
} else {
None
}
}
pub(super) fn crate_universe<'a>(
cfg: &'a anodizer_core::config::Config,
crate_name: Option<&'a str>,
) -> impl Iterator<Item = &'a anodizer_core::config::CrateConfig> {
cfg.crate_universe()
.into_iter()
.filter(move |k| crate_name.is_none_or(|n| k.name == n))
}
pub(super) fn crate_declares_docker(
repo_config: Option<&anodizer_core::config::Config>,
crate_name: Option<&str>,
) -> bool {
repo_config.is_some_and(|cfg| {
crate_universe(cfg, crate_name)
.any(|k| k.dockers_v2.as_ref().is_some_and(|v| !v.is_empty()))
})
}
pub(super) fn resolve_docker_configs(
repo_config: Option<&anodizer_core::config::Config>,
crate_name: Option<&str>,
child_snapshot: bool,
log: &StageLogger,
) -> Result<Vec<ResolvedDockerConfig>> {
let Some(cfg) = repo_config else {
return Ok(Vec::new());
};
let opts = anodizer_core::context::ContextOptions {
snapshot: child_snapshot,
selected_crates: crate_name.map(|n| vec![n.to_string()]).unwrap_or_default(),
skip_stages: anodizer_core::determinism_runner::SIDE_EFFECT_STAGES
.iter()
.map(|s| (*s).to_string())
.collect(),
..Default::default()
};
let mut ctx = anodizer_core::context::Context::new(cfg.clone(), opts);
ctx.populate_time_vars();
ctx.populate_runtime_vars();
ctx.populate_metadata_var()?;
crate::commands::helpers::setup_env(&mut ctx, cfg, log)?;
crate::commands::helpers::resolve_git_context(&mut ctx, cfg, log)?;
if child_snapshot {
crate::commands::release::apply_snapshot_template_vars(&mut ctx, cfg, log)?;
}
let mut out: Vec<ResolvedDockerConfig> = Vec::new();
for krate in crate_universe(cfg, crate_name) {
let Some(entries) = krate.dockers_v2.as_ref() else {
continue;
};
for (idx, entry) in entries.iter().enumerate() {
if anodizer_stage_docker::is_docker_v2_skipped(&entry.skip, &ctx).with_context(
|| format!("dockers_v2[{idx}]: evaluate skip for crate {}", krate.name),
)? {
continue;
}
let dockerfile = ctx.render_template(&entry.dockerfile).with_context(|| {
format!(
"dockers_v2[{idx}]: render dockerfile path '{}' for crate {}",
entry.dockerfile, krate.name
)
})?;
if dockerfile.trim().is_empty() {
continue;
}
let build_args = anodizer_stage_docker::render_v2_kv_map(
&mut ctx,
entry.build_args.as_ref(),
"build_arg",
)?;
out.push(ResolvedDockerConfig {
dockerfile,
extra_files: entry.extra_files.clone().unwrap_or_default(),
build_args,
});
}
}
Ok(out)
}
pub(super) fn classify_docker_stage_state(
resolved: Result<Vec<ResolvedDockerConfig>>,
declared: bool,
explicitly_requested: bool,
log: &StageLogger,
) -> Result<(Vec<ResolvedDockerConfig>, bool)> {
match resolved {
Ok(v) => Ok((v, declared)),
Err(e) if explicitly_requested => Err(e).context(
"resolving dockers_v2 for the determinism docker stage (--require-tools / \
explicit --stages=docker)",
),
Err(e) => {
log.warn(&format!(
"skipping docker stage — could not resolve dockers_v2 for this run: {e:#}"
));
Ok((Vec::new(), false))
}
}
}
pub(super) fn resolve_msi_tools(
repo_config: Option<&anodizer_core::config::Config>,
) -> Vec<String> {
let Some(cfg) = repo_config else {
return Vec::new();
};
let ctx = anodizer_core::context::Context::new(
cfg.clone(),
anodizer_core::context::ContextOptions::default(),
);
anodizer_stage_msi::required_msi_tools(&ctx)
}
pub(super) fn resolve_upx_tools(
repo_config: Option<&anodizer_core::config::Config>,
) -> Vec<String> {
let Some(cfg) = repo_config else {
return Vec::new();
};
let ctx = anodizer_core::context::Context::new(
cfg.clone(),
anodizer_core::context::ContextOptions::default(),
);
let mut tools = anodizer_stage_upx::required_upx_tools(&ctx);
for entry in &cfg.upx {
if entry.enabled.as_ref().is_some_and(|e| e.is_template()) && !tools.contains(&entry.binary)
{
tools.push(entry.binary.clone());
}
}
tools
}
pub(super) fn read_project_version(repo_root: &std::path::Path) -> Option<String> {
let manifest = repo_root.join("Cargo.toml");
let text = std::fs::read_to_string(&manifest).ok()?;
let doc: toml::Value = toml::from_str(&text).ok()?;
doc.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.map(str::to_string)
.or_else(|| {
doc.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.map(str::to_string)
})
}