1use std::collections::{BTreeMap, BTreeSet};
16
17use anyhow::Result;
18
19use crate::config::{Config, CrateConfig};
20use crate::context::Context;
21use crate::target::map_target;
22
23const UNAME_OS_CASES: &[(&str, &str)] = &[
33 ("Linux*", "linux"),
34 ("Darwin*", "darwin"),
35 ("MINGW*|MSYS*|CYGWIN*", "windows"),
36 ("FreeBSD*", "freebsd"),
37 ("NetBSD*", "netbsd"),
38 ("OpenBSD*", "openbsd"),
39 ("SunOS*", "solaris"),
40 ("AIX*", "aix"),
41];
42
43const UNAME_ARCH_CASES: &[(&str, &str)] = &[
51 ("x86_64|amd64", "amd64"),
52 ("aarch64|arm64", "arm64"),
53 ("armv7l|armv7", "armv7"),
54 ("armv6l|armv6", "armv6"),
55 ("i686|i586|i386", "386"),
56 ("riscv64", "riscv64"),
57 ("ppc64le", "ppc64le"),
58 ("ppc64", "ppc64"),
59 ("s390x", "s390x"),
60 ("loongarch64", "loong64"),
61 ("sparc64", "sparc64"),
62];
63
64const SEEDED_VARS: &[&str] = &[
70 "Os", "Arch", "Target", "Arm", "Arm64", "Amd64", "Mips", "I386",
71];
72
73pub const INSTALLER_TEMPLATE_VARS: &[&str] = &[
77 "InstallerAssetCases",
78 "InstallerDetectOsCases",
79 "InstallerDetectArchCases",
80 "InstallerSupportedPlatforms",
81];
82
83pub struct InstallerCases {
88 pub asset_cases: String,
91 pub detect_os_cases: String,
94 pub detect_arch_cases: String,
97 pub supported_platforms: String,
103}
104
105impl InstallerCases {
106 pub fn bind(&self, vars: &mut crate::template::TemplateVars) {
110 vars.set("InstallerAssetCases", &self.asset_cases);
111 vars.set("InstallerDetectOsCases", &self.detect_os_cases);
112 vars.set("InstallerDetectArchCases", &self.detect_arch_cases);
113 vars.set("InstallerSupportedPlatforms", &self.supported_platforms);
114 }
115}
116
117pub fn template_files_consume_installer_vars(ctx: &mut Context) -> bool {
133 let Some(entries) = ctx.config.template_files.clone() else {
134 return false;
135 };
136 entries
137 .iter()
138 .any(|entry| entry_reads_installer_vars(ctx, entry))
139}
140
141fn entry_reads_installer_vars(
142 ctx: &mut Context,
143 entry: &crate::config::TemplateFileConfig,
144) -> bool {
145 const PROBE: &str = "\u{1}anodizer-installer-consumption-probe\u{1}";
146 let prior: Vec<(&str, Option<String>)> = INSTALLER_TEMPLATE_VARS
147 .iter()
148 .map(|k| (*k, ctx.template_vars().get(k).cloned()))
149 .collect();
150 let render_with = |ctx: &mut Context, value: &str| {
151 for k in INSTALLER_TEMPLATE_VARS {
152 ctx.template_vars_mut().set(k, value);
153 }
154 crate::template_file_render::render_templated_file_entry(
155 ctx,
156 entry,
157 "installer consumption probe",
158 )
159 };
160 let base = render_with(ctx, "");
161 let probe = render_with(ctx, PROBE);
162 for (k, value) in prior {
163 match value {
164 Some(v) => ctx.template_vars_mut().set(k, &v),
165 None => {
166 ctx.template_vars_mut().unset(k);
167 }
168 }
169 }
170 match (base, probe) {
171 (Ok(Some(a)), Ok(Some(b))) => {
172 a.rendered_contents != b.rendered_contents || a.rendered_dst != b.rendered_dst
173 }
174 (Ok(a), Ok(b)) => a.is_some() != b.is_some(),
175 _ => true,
176 }
177}
178
179pub fn render_installer_cases(ctx: &mut Context) -> Result<InstallerCases> {
210 let empty = || InstallerCases {
211 asset_cases: String::new(),
212 detect_os_cases: String::new(),
213 detect_arch_cases: String::new(),
214 supported_platforms: String::new(),
215 };
216 let Some(crate_cfg) = installer_crate(&ctx.config) else {
217 return Ok(empty());
218 };
219 let default_targets = ctx.config.effective_default_targets();
220
221 let prior: Vec<(&str, Option<String>)> = SEEDED_VARS
224 .iter()
225 .map(|k| (*k, ctx.template_vars().get(k).cloned()))
226 .collect();
227 let assets = crate::binstall::crate_archive_asset_names(&crate_cfg, &default_targets, ctx);
228 for (key, value) in prior {
229 match value {
230 Some(v) => ctx.template_vars_mut().set(key, &v),
231 None => {
232 ctx.template_vars_mut().unset(key);
233 }
234 }
235 }
236 let Some(assets) = assets? else {
237 return Ok(empty());
238 };
239
240 let mut arms: BTreeMap<String, (u8, String)> = BTreeMap::new();
252 let mut released_os: BTreeSet<String> = BTreeSet::new();
253 let mut released_arch: BTreeSet<String> = BTreeSet::new();
254 for (target, asset) in &assets {
255 let (os, arch) = map_target(target);
256 if arch == "all" {
257 continue;
258 }
259 released_os.insert(os.clone());
260 released_arch.insert(arch.clone());
261 record_arm(
262 &mut arms,
263 format!("{os}-{arch}"),
264 installer_arm_rank(target),
265 &asset.asset_name,
266 );
267 }
268 for (target, asset) in &assets {
269 let (os, arch) = map_target(target);
270 if arch != "all" {
271 continue;
272 }
273 released_os.insert(os.clone());
274 for cpu in ["amd64", "arm64"] {
275 released_arch.insert(cpu.to_string());
276 record_arm(
277 &mut arms,
278 format!("{os}-{cpu}"),
279 RANK_UNIVERSAL,
280 &asset.asset_name,
281 );
282 }
283 }
284
285 let detectable_os: BTreeSet<&str> = UNAME_OS_CASES.iter().map(|(_, t)| *t).collect();
290 let detectable_arch: BTreeSet<&str> = UNAME_ARCH_CASES.iter().map(|(_, t)| *t).collect();
291 let key_is_reachable = |key: &str| -> bool {
292 key.split_once('-')
294 .is_some_and(|(os, arch)| detectable_os.contains(os) && detectable_arch.contains(arch))
295 };
296
297 let stranded: Vec<&str> = assets
298 .iter()
299 .filter(|(target, _)| {
300 let (os, arch) = map_target(target);
301 arch != "all" && !key_is_reachable(&format!("{os}-{arch}"))
302 })
303 .map(|(target, _)| target.as_str())
304 .collect();
305 if !stranded.is_empty() {
306 ctx.logger("templatefiles").warn(&format!(
307 "installer script cannot detect released target(s) {}: their uname \
308 tokens are ambiguous or unmapped (`uname -m` reports the same \
309 token for both mips endiannesses; illumos reports `SunOS`), so \
310 their asset arms are unreachable — matching hosts get the \
311 unsupported-platform error",
312 stranded.join(", ")
313 ));
314 }
315
316 let supported: Vec<&str> = arms
317 .keys()
318 .filter(|key| key_is_reachable(key))
319 .map(String::as_str)
320 .collect();
321
322 let lines: Vec<String> = arms
323 .iter()
324 .map(|(key, (_, asset))| format!(" {key})\n ARCHIVE=\"{asset}\"\n ;;"))
325 .collect();
326 Ok(InstallerCases {
327 asset_cases: lines.join("\n"),
328 detect_os_cases: render_uname_cases(UNAME_OS_CASES, &released_os),
329 detect_arch_cases: render_uname_cases(UNAME_ARCH_CASES, &released_arch),
330 supported_platforms: supported.join(" "),
331 })
332}
333
334fn render_uname_cases(table: &[(&str, &str)], released: &BTreeSet<String>) -> String {
337 table
338 .iter()
339 .filter(|(_, token)| released.contains(*token))
340 .map(|(pattern, token)| format!(" {pattern}) echo \"{token}\" ;;"))
341 .collect::<Vec<_>>()
342 .join("\n")
343}
344
345const RANK_UNIVERSAL: u8 = 2;
350
351fn installer_arm_rank(target: &str) -> u8 {
365 if target.contains("musl") { 0 } else { 1 }
366}
367
368fn record_arm(arms: &mut BTreeMap<String, (u8, String)>, key: String, rank: u8, asset: &str) {
374 match arms.entry(key) {
375 std::collections::btree_map::Entry::Vacant(slot) => {
376 slot.insert((rank, asset.to_string()));
377 }
378 std::collections::btree_map::Entry::Occupied(mut slot) if rank < slot.get().0 => {
379 slot.insert((rank, asset.to_string()));
380 }
381 std::collections::btree_map::Entry::Occupied(_) => {}
382 }
383}
384
385pub fn installer_crate(config: &Config) -> Option<CrateConfig> {
396 let project = config.project_name.as_str();
397 let produces_project_binary = |c: &CrateConfig| -> bool {
398 crate::build_plan::planned_builds(c)
399 .map(|builds| builds.iter().any(|b| b.binary.as_deref() == Some(project)))
400 .unwrap_or(false)
401 };
402
403 config
404 .crate_universe()
405 .into_iter()
406 .find(|c| produces_project_binary(c) && crate::binstall::binstallable_archive(c).is_some())
407 .cloned()
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::archive_name::render_archive_asset_name;
414 use crate::config::{
415 ArchiveConfig, ArchivesConfig, BuildConfig, Config, Defaults, FormatOverride,
416 };
417 use crate::context::{Context, ContextOptions};
418
419 const ANODIZE_TARGETS: &[&str] = &[
422 "x86_64-unknown-linux-gnu",
423 "aarch64-unknown-linux-gnu",
424 "x86_64-apple-darwin",
425 "aarch64-apple-darwin",
426 "x86_64-pc-windows-msvc",
427 "aarch64-pc-windows-msvc",
428 ];
429
430 fn anodize_ctx(name_template: Option<&str>) -> Context {
436 let primary = ArchiveConfig {
437 id: Some("default".to_string()),
438 name_template: name_template.map(str::to_string),
439 formats: Some(vec!["tar.gz".to_string()]),
440 format_overrides: Some(vec![FormatOverride {
441 os: "windows".to_string(),
442 formats: Some(vec!["zip".to_string()]),
443 }]),
444 ids: Some(vec!["anodizer".to_string()]),
445 ..Default::default()
446 };
447 let extra = ArchiveConfig {
448 id: Some("extra".to_string()),
449 name_template: Some(
450 "{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}-extra".to_string(),
451 ),
452 formats: Some(vec!["tar.xz".to_string(), "tar.zst".to_string()]),
453 ids: Some(vec!["anodizer".to_string()]),
454 ..Default::default()
455 };
456 let crate_cfg = CrateConfig {
457 name: "anodizer".to_string(),
458 path: "crates/cli".to_string(),
459 builds: Some(vec![BuildConfig {
460 id: Some("anodizer".to_string()),
461 binary: Some("anodizer".to_string()),
462 ..Default::default()
463 }]),
464 archives: ArchivesConfig::Configs(vec![primary, extra]),
465 ..Default::default()
466 };
467 let config = Config {
468 project_name: "anodizer".to_string(),
469 defaults: Some(Defaults {
470 targets: Some(ANODIZE_TARGETS.iter().map(|s| s.to_string()).collect()),
471 ..Default::default()
472 }),
473 crates: vec![crate_cfg],
474 ..Default::default()
475 };
476 let mut ctx = Context::new(config, ContextOptions::default());
477 ctx.set_env_source(crate::MapEnvSource::new());
480 ctx.template_vars_mut().set("ProjectName", "anodizer");
481 ctx.template_vars_mut().set("Version", "0.13.0");
482 ctx
483 }
484
485 #[test]
490 fn bind_writes_exactly_the_installer_template_vars() {
491 let cases = InstallerCases {
492 asset_cases: "a".to_string(),
493 detect_os_cases: "b".to_string(),
494 detect_arch_cases: "c".to_string(),
495 supported_platforms: "d".to_string(),
496 };
497 let mut vars = crate::template::TemplateVars::new();
498 cases.bind(&mut vars);
499 for k in INSTALLER_TEMPLATE_VARS {
500 assert!(
501 vars.get(k).is_some_and(|v| !v.is_empty()),
502 "{k} must be bound"
503 );
504 }
505 assert_eq!(
506 vars.all().len(),
507 INSTALLER_TEMPLATE_VARS.len(),
508 "bind must write no keys beyond INSTALLER_TEMPLATE_VARS"
509 );
510 }
511
512 #[test]
517 fn template_files_consumption_probe_detects_real_bindings() {
518 use crate::config::TemplateFileConfig;
519 let tmp = tempfile::tempdir().unwrap();
520 let installer = tmp.path().join("install.sh.tera");
521 std::fs::write(&installer, "{{ InstallerAssetCases }}\n").unwrap();
522 let plain = tmp.path().join("notes.md.tera");
523 std::fs::write(&plain, "release {{ Version }} of {{ ProjectName }}\n").unwrap();
524
525 let entry = |src: &std::path::Path| TemplateFileConfig {
526 src: src.to_string_lossy().into_owned(),
527 dst: "out.txt".to_string(),
528 ..Default::default()
529 };
530
531 let mut ctx = anodize_ctx(None);
532 ctx.template_vars_mut()
533 .set("InstallerAssetCases", "stale-from-stage");
534 ctx.config.template_files = Some(vec![entry(&plain)]);
535 assert!(
536 !template_files_consume_installer_vars(&mut ctx),
537 "a template binding no Installer* var must not count as a consumer"
538 );
539
540 ctx.config.template_files = Some(vec![entry(&plain), entry(&installer)]);
541 assert!(
542 template_files_consume_installer_vars(&mut ctx),
543 "a template interpolating an installer var is a consumer"
544 );
545
546 ctx.config.template_files = Some(vec![entry(&tmp.path().join("missing.tera"))]);
547 assert!(
548 template_files_consume_installer_vars(&mut ctx),
549 "an unreadable entry cannot prove non-consumption — stay loud"
550 );
551
552 assert_eq!(
553 ctx.template_vars()
554 .get("InstallerAssetCases")
555 .map(String::as_str),
556 Some("stale-from-stage"),
557 "the probe must restore the caller's bindings"
558 );
559 }
560
561 fn parse_arms(table: &str) -> BTreeMap<String, String> {
564 let mut out = BTreeMap::new();
565 let mut key: Option<String> = None;
566 for line in table.lines() {
567 let t = line.trim();
568 if let Some(k) = t.strip_suffix(')') {
569 key = Some(k.to_string());
570 } else if let Some(rest) = t.strip_prefix("ARCHIVE=\"") {
571 let asset = rest.trim_end_matches('"');
572 if let Some(k) = key.take() {
573 out.insert(k, asset.to_string());
574 }
575 }
576 }
577 out
578 }
579
580 #[test]
585 fn installer_arms_match_engine_asset_names_hyphen_template() {
586 let name_template = "{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}";
587 let mut ctx = anodize_ctx(Some(name_template));
588 let table = render_installer_cases(&mut ctx).unwrap().asset_cases;
589 let arms = parse_arms(&table);
590
591 assert_eq!(arms.len(), ANODIZE_TARGETS.len(), "one arm per target");
592 for target in ANODIZE_TARGETS {
593 let (os, arch) = map_target(target);
594 let key = format!("{os}-{arch}");
595 let format = if os == "windows" { "zip" } else { "tar.gz" };
596 let expected =
597 render_archive_asset_name(&mut ctx, name_template, target, format).unwrap();
598 assert_eq!(
599 arms.get(&key),
600 Some(&expected),
601 "installer arm '{key}' must equal the archive stage's asset name"
602 );
603 }
604 assert_eq!(
606 arms.get("linux-amd64").map(String::as_str),
607 Some("anodizer-0.13.0-linux-amd64.tar.gz")
608 );
609 assert_eq!(
610 arms.get("windows-amd64").map(String::as_str),
611 Some("anodizer-0.13.0-windows-amd64.zip")
612 );
613 }
614
615 #[test]
620 fn installer_arms_follow_engine_default_underscore_template() {
621 let mut ctx = anodize_ctx(None);
622 let table = render_installer_cases(&mut ctx).unwrap().asset_cases;
623 let arms = parse_arms(&table);
624
625 for target in ANODIZE_TARGETS {
626 let (os, arch) = map_target(target);
627 let key = format!("{os}-{arch}");
628 let format = if os == "windows" { "zip" } else { "tar.gz" };
629 let expected = render_archive_asset_name(
630 &mut ctx,
631 crate::archive_name::DEFAULT_NAME_TEMPLATE,
632 target,
633 format,
634 )
635 .unwrap();
636 assert_eq!(arms.get(&key), Some(&expected));
637 }
638 assert_eq!(
640 arms.get("linux-amd64").map(String::as_str),
641 Some("anodizer_0.13.0_linux_amd64.tar.gz")
642 );
643 }
644
645 #[test]
650 fn installer_arms_carry_config_declared_amd64_variant() {
651 let mut ctx = anodize_ctx(None);
652 let mut env = std::collections::HashMap::new();
653 env.insert(
654 "x86_64-unknown-linux-gnu".to_string(),
655 std::collections::HashMap::from([(
656 "RUSTFLAGS".to_string(),
657 "-Ctarget-cpu=x86-64-v3".to_string(),
658 )]),
659 );
660 ctx.config.crates[0].builds.as_mut().unwrap()[0].env = Some(env);
661
662 let table = render_installer_cases(&mut ctx).unwrap().asset_cases;
663 let arms = parse_arms(&table);
664 assert_eq!(
665 arms.get("linux-amd64").map(String::as_str),
666 Some("anodizer_0.13.0_linux_amd64v3.tar.gz"),
667 "tuned target's arm must carry the micro-arch suffix"
668 );
669 assert_eq!(
670 arms.get("darwin-amd64").map(String::as_str),
671 Some("anodizer_0.13.0_darwin_amd64.tar.gz"),
672 "untuned amd64 target stays baseline"
673 );
674 }
675
676 #[test]
679 fn render_restores_seed_vars() {
680 let mut ctx = anodize_ctx(Some("{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}"));
681 ctx.template_vars_mut().set("Os", "sentinel-os");
682 let _ = render_installer_cases(&mut ctx).unwrap();
683 assert_eq!(
684 ctx.template_vars().get("Os").map(String::as_str),
685 Some("sentinel-os"),
686 "Os must be restored after rendering the case table"
687 );
688 assert!(
690 ctx.template_vars().get("Target").is_none(),
691 "Target must be cleared back to unset after rendering"
692 );
693 }
694
695 #[test]
698 fn no_installer_crate_yields_empty_table() {
699 let config = Config {
700 project_name: "anodizer".to_string(),
701 crates: vec![CrateConfig {
702 name: "anodizer-core".to_string(),
703 path: "crates/core".to_string(),
704 ..Default::default()
705 }],
706 ..Default::default()
707 };
708 let mut ctx = Context::new(config, ContextOptions::default());
709 ctx.template_vars_mut().set("ProjectName", "anodizer");
710 ctx.template_vars_mut().set("Version", "0.13.0");
711 let cases = render_installer_cases(&mut ctx).unwrap();
712 assert_eq!(cases.asset_cases, "");
713 assert_eq!(cases.detect_os_cases, "");
714 assert_eq!(cases.detect_arch_cases, "");
715 assert_eq!(cases.supported_platforms, "");
716 }
717
718 #[test]
724 fn uname_arch_aliases_round_trip_through_map_target() {
725 for (pattern, token) in UNAME_ARCH_CASES {
726 for alias in pattern.split('|') {
727 let (_, arch) = map_target(&format!("{alias}-unknown-linux-gnu"));
728 assert_eq!(
729 &arch, token,
730 "uname alias '{alias}' must map_target to its case token '{token}'"
731 );
732 }
733 }
734 }
735
736 #[test]
739 fn uname_os_tokens_round_trip_through_map_target() {
740 for (_, token) in UNAME_OS_CASES {
741 let triple = match *token {
742 "darwin" => "x86_64-apple-darwin".to_string(),
743 "windows" => "x86_64-pc-windows-msvc".to_string(),
744 "aix" => "powerpc64-ibm-aix".to_string(),
745 other => format!("x86_64-unknown-{other}"),
746 };
747 let (os, _) = map_target(&triple);
748 assert_eq!(
749 &os, token,
750 "uname OS token '{token}' must equal map_target's OS for {triple}"
751 );
752 }
753 }
754
755 #[test]
759 fn detect_cases_cover_every_asset_arm_key() {
760 let mut ctx = anodize_ctx(None);
761 let cases = render_installer_cases(&mut ctx).unwrap();
762
763 assert_eq!(
764 cases.detect_os_cases,
765 " Linux*) echo \"linux\" ;;\n\
766 \x20 Darwin*) echo \"darwin\" ;;\n\
767 \x20 MINGW*|MSYS*|CYGWIN*) echo \"windows\" ;;"
768 );
769 assert_eq!(
770 cases.detect_arch_cases,
771 " x86_64|amd64) echo \"amd64\" ;;\n\
772 \x20 aarch64|arm64) echo \"arm64\" ;;"
773 );
774
775 let os_tokens: Vec<&str> = cases
776 .detect_os_cases
777 .lines()
778 .filter_map(|l| l.split('"').nth(1))
779 .collect();
780 let arch_tokens: Vec<&str> = cases
781 .detect_arch_cases
782 .lines()
783 .filter_map(|l| l.split('"').nth(1))
784 .collect();
785 for key in parse_arms(&cases.asset_cases).keys() {
786 let (os, arch) = key.split_once('-').expect("key is os-arch");
787 assert!(os_tokens.contains(&os), "asset arm OS '{os}' undetectable");
788 assert!(
789 arch_tokens.contains(&arch),
790 "asset arm arch '{arch}' undetectable"
791 );
792 }
793 }
794
795 #[test]
799 fn universal_asset_fans_out_to_amd64_and_arm64_keys() {
800 let mut ctx = anodize_ctx(Some("{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}"));
801 ctx.config.defaults.as_mut().unwrap().targets = Some(vec![
802 "darwin-universal".to_string(),
803 "aarch64-apple-darwin".to_string(),
804 ]);
805 let cases = render_installer_cases(&mut ctx).unwrap();
806 let arms = parse_arms(&cases.asset_cases);
807
808 assert!(!arms.contains_key("darwin-all"), "no unmatchable 'all' key");
809 assert_eq!(
810 arms.get("darwin-amd64").map(String::as_str),
811 Some("anodizer-0.13.0-darwin-all.tar.gz"),
812 "amd64 hosts fall back to the universal asset"
813 );
814 assert_eq!(
815 arms.get("darwin-arm64").map(String::as_str),
816 Some("anodizer-0.13.0-darwin-arm64.tar.gz"),
817 "the arch-specific asset wins its own key over the universal"
818 );
819 }
820
821 #[test]
828 fn gnu_and_musl_same_arch_prefers_static_musl() {
829 let name_template = "{{ ProjectName }}-{{ Version }}-{{ Target }}";
830 let mut ctx = anodize_ctx(Some(name_template));
831 ctx.config.defaults.as_mut().unwrap().targets = Some(vec![
832 "x86_64-unknown-linux-gnu".to_string(),
833 "x86_64-unknown-linux-musl".to_string(),
834 ]);
835 let cases = render_installer_cases(&mut ctx).unwrap();
836 let arms = parse_arms(&cases.asset_cases);
837
838 assert_eq!(
839 arms.len(),
840 1,
841 "both libcs collapse onto the single linux-amd64 key: {arms:?}"
842 );
843 assert_eq!(
844 arms.get("linux-amd64").map(String::as_str),
845 Some("anodizer-0.13.0-x86_64-unknown-linux-musl.tar.gz"),
846 "the static musl build must win the shared linux-amd64 arm, not be dropped"
847 );
848 }
849
850 #[test]
856 fn gnu_and_musl_collision_resolves_under_workspaces_config() {
857 let name_template = "{{ ProjectName }}-{{ Version }}-{{ Target }}";
858 let mut ctx = anodize_ctx(Some(name_template));
859 ctx.config.defaults.as_mut().unwrap().targets = Some(vec![
860 "x86_64-unknown-linux-gnu".to_string(),
861 "x86_64-unknown-linux-musl".to_string(),
862 ]);
863 let moved: Vec<CrateConfig> = ctx.config.crates.drain(..).collect();
864 ctx.config.workspaces = Some(vec![crate::config::WorkspaceConfig {
865 name: "cli".to_string(),
866 crates: moved,
867 ..Default::default()
868 }]);
869
870 let cases = render_installer_cases(&mut ctx).unwrap();
871 let arms = parse_arms(&cases.asset_cases);
872 assert_eq!(
873 arms.get("linux-amd64").map(String::as_str),
874 Some("anodizer-0.13.0-x86_64-unknown-linux-musl.tar.gz"),
875 "per-crate (workspaces) config must resolve the libc collision musl-first too"
876 );
877 }
878
879 #[test]
882 fn supported_platforms_lists_reachable_keys() {
883 let mut ctx = anodize_ctx(None);
884 let cases = render_installer_cases(&mut ctx).unwrap();
885 assert_eq!(
886 cases.supported_platforms,
887 "darwin-amd64 darwin-arm64 linux-amd64 linux-arm64 \
888 windows-amd64 windows-arm64"
889 );
890 }
891
892 #[test]
896 fn undetectable_mips_target_warns_and_is_not_listed_supported() {
897 let mut ctx = anodize_ctx(None);
898 ctx.config.defaults.as_mut().unwrap().targets = Some(vec![
899 "x86_64-unknown-linux-gnu".to_string(),
900 "mips64el-unknown-linux-gnuabi64".to_string(),
901 ]);
902 let capture = crate::log::LogCapture::new();
903 ctx.with_log_capture(capture.clone());
904
905 let cases = render_installer_cases(&mut ctx).unwrap();
906
907 assert!(
909 parse_arms(&cases.asset_cases).contains_key("linux-mips64el"),
910 "asset arm for the mips target must still be emitted"
911 );
912 assert_eq!(cases.supported_platforms, "linux-amd64");
914 assert_eq!(capture.warn_count(), 1, "exactly one stranded-target warn");
916 assert!(
917 capture
918 .warn_messages()
919 .iter()
920 .any(|m| m.contains("mips64el-unknown-linux-gnuabi64")),
921 "warn must name the stranded target: {:?}",
922 capture.warn_messages()
923 );
924 }
925
926 #[test]
929 fn detectable_targets_render_no_stranded_warning() {
930 let mut ctx = anodize_ctx(None);
931 let capture = crate::log::LogCapture::new();
932 ctx.with_log_capture(capture.clone());
933 let _ = render_installer_cases(&mut ctx).unwrap();
934 assert_eq!(
935 capture.warn_count(),
936 0,
937 "no warning for detectable targets: {:?}",
938 capture.warn_messages()
939 );
940 }
941}