use std::collections::BTreeMap;
use anyhow::Result;
use crate::config::{Config, CrateConfig};
use crate::context::Context;
use crate::target::map_target;
const SEEDED_VARS: &[&str] = &[
"Os", "Arch", "Target", "Arm", "Arm64", "Amd64", "Mips", "I386",
];
pub fn render_asset_case_table(ctx: &mut Context) -> Result<String> {
let Some(crate_cfg) = installer_crate(&ctx.config) else {
return Ok(String::new());
};
let default_targets = ctx.config.effective_default_targets();
let prior: Vec<(&str, Option<String>)> = SEEDED_VARS
.iter()
.map(|k| (*k, ctx.template_vars().get(k).cloned()))
.collect();
let assets = crate::binstall::crate_archive_asset_names(&crate_cfg, &default_targets, ctx);
for (key, value) in prior {
match value {
Some(v) => ctx.template_vars_mut().set(key, &v),
None => {
ctx.template_vars_mut().unset(key);
}
}
}
let Some(assets) = assets? else {
return Ok(String::new());
};
let mut arms: BTreeMap<String, String> = BTreeMap::new();
for (target, asset) in &assets {
let (os, arch) = map_target(target);
arms.entry(format!("{os}-{arch}"))
.or_insert_with(|| asset.asset_name.clone());
}
let lines: Vec<String> = arms
.iter()
.map(|(key, asset)| format!(" {key})\n ARCHIVE=\"{asset}\"\n ;;"))
.collect();
Ok(lines.join("\n"))
}
fn installer_crate(config: &Config) -> Option<CrateConfig> {
let project = config.project_name.as_str();
let produces_project_binary = |c: &CrateConfig| -> bool {
crate::build_plan::planned_builds(c)
.map(|builds| builds.iter().any(|b| b.binary.as_deref() == Some(project)))
.unwrap_or(false)
};
config
.crates
.iter()
.chain(
config
.workspaces
.iter()
.flatten()
.flat_map(|w| w.crates.iter()),
)
.find(|c| produces_project_binary(c) && crate::binstall::binstallable_archive(c).is_some())
.cloned()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::archive_name::render_archive_asset_name;
use crate::config::{
ArchiveConfig, ArchivesConfig, BuildConfig, Config, Defaults, FormatOverride,
};
use crate::context::{Context, ContextOptions};
const ANODIZE_TARGETS: &[&str] = &[
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
];
fn anodize_ctx(name_template: Option<&str>) -> Context {
let primary = ArchiveConfig {
id: Some("default".to_string()),
name_template: name_template.map(str::to_string),
formats: Some(vec!["tar.gz".to_string()]),
format_overrides: Some(vec![FormatOverride {
os: "windows".to_string(),
formats: Some(vec!["zip".to_string()]),
}]),
ids: Some(vec!["anodizer".to_string()]),
..Default::default()
};
let extra = ArchiveConfig {
id: Some("extra".to_string()),
name_template: Some(
"{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}-extra".to_string(),
),
formats: Some(vec!["tar.xz".to_string(), "tar.zst".to_string()]),
ids: Some(vec!["anodizer".to_string()]),
..Default::default()
};
let crate_cfg = CrateConfig {
name: "anodizer".to_string(),
path: "crates/cli".to_string(),
builds: Some(vec![BuildConfig {
id: Some("anodizer".to_string()),
binary: Some("anodizer".to_string()),
..Default::default()
}]),
archives: ArchivesConfig::Configs(vec![primary, extra]),
..Default::default()
};
let config = Config {
project_name: "anodizer".to_string(),
defaults: Some(Defaults {
targets: Some(ANODIZE_TARGETS.iter().map(|s| s.to_string()).collect()),
..Default::default()
}),
crates: vec![crate_cfg],
..Default::default()
};
let mut ctx = Context::new(config, ContextOptions::default());
ctx.template_vars_mut().set("ProjectName", "anodizer");
ctx.template_vars_mut().set("Version", "0.13.0");
ctx
}
fn parse_arms(table: &str) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
let mut key: Option<String> = None;
for line in table.lines() {
let t = line.trim();
if let Some(k) = t.strip_suffix(')') {
key = Some(k.to_string());
} else if let Some(rest) = t.strip_prefix("ARCHIVE=\"") {
let asset = rest.trim_end_matches('"');
if let Some(k) = key.take() {
out.insert(k, asset.to_string());
}
}
}
out
}
#[test]
fn installer_arms_match_engine_asset_names_hyphen_template() {
let name_template = "{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}";
let mut ctx = anodize_ctx(Some(name_template));
let table = render_asset_case_table(&mut ctx).unwrap();
let arms = parse_arms(&table);
assert_eq!(arms.len(), ANODIZE_TARGETS.len(), "one arm per target");
for target in ANODIZE_TARGETS {
let (os, arch) = map_target(target);
let key = format!("{os}-{arch}");
let format = if os == "windows" { "zip" } else { "tar.gz" };
let expected =
render_archive_asset_name(&mut ctx, name_template, target, format).unwrap();
assert_eq!(
arms.get(&key),
Some(&expected),
"installer arm '{key}' must equal the archive stage's asset name"
);
}
assert_eq!(
arms.get("linux-amd64").map(String::as_str),
Some("anodizer-0.13.0-linux-amd64.tar.gz")
);
assert_eq!(
arms.get("windows-amd64").map(String::as_str),
Some("anodizer-0.13.0-windows-amd64.zip")
);
}
#[test]
fn installer_arms_follow_engine_default_underscore_template() {
let mut ctx = anodize_ctx(None);
let table = render_asset_case_table(&mut ctx).unwrap();
let arms = parse_arms(&table);
for target in ANODIZE_TARGETS {
let (os, arch) = map_target(target);
let key = format!("{os}-{arch}");
let format = if os == "windows" { "zip" } else { "tar.gz" };
let expected = render_archive_asset_name(
&mut ctx,
crate::archive_name::DEFAULT_NAME_TEMPLATE,
target,
format,
)
.unwrap();
assert_eq!(arms.get(&key), Some(&expected));
}
assert_eq!(
arms.get("linux-amd64").map(String::as_str),
Some("anodizer_0.13.0_linux_amd64.tar.gz")
);
}
#[test]
fn render_restores_seed_vars() {
let mut ctx = anodize_ctx(Some("{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}"));
ctx.template_vars_mut().set("Os", "sentinel-os");
let _ = render_asset_case_table(&mut ctx).unwrap();
assert_eq!(
ctx.template_vars().get("Os").map(String::as_str),
Some("sentinel-os"),
"Os must be restored after rendering the case table"
);
assert!(
ctx.template_vars().get("Target").is_none(),
"Target must be cleared back to unset after rendering"
);
}
#[test]
fn no_installer_crate_yields_empty_table() {
let config = Config {
project_name: "anodizer".to_string(),
crates: vec![CrateConfig {
name: "anodizer-core".to_string(),
path: "crates/core".to_string(),
..Default::default()
}],
..Default::default()
};
let mut ctx = Context::new(config, ContextOptions::default());
ctx.template_vars_mut().set("ProjectName", "anodizer");
ctx.template_vars_mut().set("Version", "0.13.0");
assert_eq!(render_asset_case_table(&mut ctx).unwrap(), "");
}
}