1use std::collections::BTreeMap;
2use std::path::Path;
3
4use anyhow::{Context as _, Result};
5
6use crate::archive_name::{binstall_pkg_fmt, render_archive_asset_name_with_variant};
7use crate::config::{ArchiveConfig, ArchivesConfig, BinstallConfig, BinstallOverride, CrateConfig};
8use crate::context::Context;
9use crate::target::map_target;
10
11const VERSION_SENTINEL: &str = "__ANODIZE_BINSTALL_VERSION__";
16
17pub fn generate_binstall_metadata(
46 crate_cfg: &CrateConfig,
47 config: &BinstallConfig,
48 default_targets: &[String],
49 ctx: &mut Context,
50 dry_run: bool,
51) -> Result<()> {
52 let cargo_toml_path = Path::new(&crate_cfg.path).join("Cargo.toml");
53 let content = std::fs::read_to_string(&cargo_toml_path)
54 .with_context(|| format!("failed to read {}", cargo_toml_path.display()))?;
55
56 let mut doc = content
57 .parse::<toml_edit::DocumentMut>()
58 .with_context(|| format!("failed to parse {}", cargo_toml_path.display()))?;
59
60 let rendered_pkg_url =
64 match config.pkg_url {
65 Some(ref pkg_url) => Some(ctx.render_template(pkg_url).with_context(|| {
66 format!("failed to render binstall pkg_url template: {}", pkg_url)
67 })?),
68 None => None,
69 };
70
71 let user_supplied_overrides = config.overrides.as_ref().is_some_and(|o| !o.is_empty());
77 let rendered_overrides = if config.pkg_url.is_none() && !user_supplied_overrides {
78 let derived = derive_overrides(crate_cfg, default_targets, ctx)?;
79 match derived {
80 Some(map) => render_overrides_map(&map, ctx)?,
81 None => None,
82 }
83 } else {
84 render_overrides(config, ctx)?
85 };
86
87 let log = ctx.logger("build");
88 if dry_run {
89 log.status(&format!(
90 "(dry-run) would update [package.metadata.binstall] in {}",
91 cargo_toml_path.display()
92 ));
93 return Ok(());
94 }
95
96 let package = doc
98 .get_mut("package")
99 .and_then(|p| p.as_table_mut())
100 .with_context(|| format!("no [package] table in {}", cargo_toml_path.display()))?;
101
102 if !package.contains_key("metadata") {
103 package.insert("metadata", toml_edit::Item::Table(toml_edit::Table::new()));
104 }
105
106 let metadata = package
107 .get_mut("metadata")
108 .and_then(|m| m.as_table_mut())
109 .with_context(|| {
110 format!(
111 "[package].metadata is not a table in {}",
112 cargo_toml_path.display()
113 )
114 })?;
115
116 normalize_binstall_to_table(metadata).with_context(|| {
117 format!(
118 "[package.metadata.binstall] is neither a table nor an inline table in {}",
119 cargo_toml_path.display()
120 )
121 })?;
122
123 let binstall = metadata
126 .get_mut("binstall")
127 .and_then(|b| b.as_table_mut())
128 .with_context(|| {
129 format!(
130 "[package.metadata.binstall] is not a table in {}",
131 cargo_toml_path.display()
132 )
133 })?;
134
135 set_or_remove_str(binstall, "pkg-url", rendered_pkg_url.as_deref());
136 set_or_remove_str(binstall, "bin-dir", config.bin_dir.as_deref());
137 set_or_remove_str(binstall, "pkg-fmt", config.pkg_fmt.as_deref());
138
139 match rendered_overrides {
140 Some(overrides_table) => {
141 binstall.insert("overrides", toml_edit::Item::Table(overrides_table));
142 }
143 None => {
144 binstall.remove("overrides");
145 }
146 }
147
148 std::fs::write(&cargo_toml_path, doc.to_string())
149 .with_context(|| format!("failed to write {}", cargo_toml_path.display()))?;
150
151 log.status(&format!(
152 "updated [package.metadata.binstall] in {}",
153 cargo_toml_path.display()
154 ));
155
156 Ok(())
157}
158
159fn normalize_binstall_to_table(metadata: &mut toml_edit::Table) -> Result<()> {
171 match metadata.get("binstall") {
172 None => {
173 metadata.insert("binstall", toml_edit::Item::Table(toml_edit::Table::new()));
174 Ok(())
175 }
176 Some(item) if item.is_table() => Ok(()),
177 Some(item) => {
178 let inline = item.as_inline_table().with_context(|| {
179 "[package.metadata.binstall] is neither a table nor an inline table".to_string()
180 })?;
181 let mut table = toml_edit::Table::new();
184 for (k, v) in inline.iter() {
185 table.insert(k, toml_edit::Item::Value(v.clone()));
186 }
187 metadata.insert("binstall", toml_edit::Item::Table(table));
188 Ok(())
189 }
190 }
191}
192
193fn set_or_remove_str(table: &mut toml_edit::Table, key: &str, value: Option<&str>) {
197 match value {
198 Some(v) => {
199 table.insert(key, toml_edit::value(v));
200 }
201 None => {
202 table.remove(key);
203 }
204 }
205}
206
207fn render_overrides(config: &BinstallConfig, ctx: &Context) -> Result<Option<toml_edit::Table>> {
212 let Some(ref overrides) = config.overrides else {
213 return Ok(None);
214 };
215 if overrides.is_empty() {
216 return Ok(None);
217 }
218 render_overrides_map(overrides, ctx)
219}
220
221fn render_overrides_map(
227 overrides: &BTreeMap<String, BinstallOverride>,
228 ctx: &Context,
229) -> Result<Option<toml_edit::Table>> {
230 if overrides.is_empty() {
231 return Ok(None);
232 }
233 let mut overrides_table = toml_edit::Table::new();
234 overrides_table.set_implicit(true);
237 for (triple, ovr) in overrides {
239 let mut entry = toml_edit::Table::new();
240 if let Some(ref pkg_url) = ovr.pkg_url {
241 let rendered = ctx.render_template(pkg_url).with_context(|| {
242 format!("failed to render binstall overrides.{triple} pkg_url template: {pkg_url}")
243 })?;
244 entry.insert("pkg-url", toml_edit::value(rendered));
245 }
246 if let Some(ref pkg_fmt) = ovr.pkg_fmt {
247 entry.insert("pkg-fmt", toml_edit::value(pkg_fmt.as_str()));
248 }
249 if let Some(ref bin_dir) = ovr.bin_dir {
250 entry.insert("bin-dir", toml_edit::value(bin_dir.as_str()));
251 }
252 overrides_table.insert(triple, toml_edit::Item::Table(entry));
253 }
254 Ok(Some(overrides_table))
255}
256
257fn derive_overrides(
276 crate_cfg: &CrateConfig,
277 default_targets: &[String],
278 ctx: &mut Context,
279) -> Result<Option<BTreeMap<String, BinstallOverride>>> {
280 let Some((owner, repo, download_base)) = release_repo(crate_cfg, ctx) else {
281 return Ok(None);
282 };
283
284 let prior = stamp_sentinel_version(ctx);
290 let assets = crate_archive_asset_names(crate_cfg, default_targets, ctx);
291 restore_version(ctx, prior);
292 let Some(assets) = assets? else {
293 return Ok(None);
294 };
295
296 let tag_with_token = render_tag_with_version_token(crate_cfg, ctx)?;
300
301 let mut map: BTreeMap<String, BinstallOverride> = BTreeMap::new();
302 for (target, asset) in &assets {
303 let Some(pkg_fmt) = binstall_pkg_fmt(&asset.format) else {
304 continue;
308 };
309 let asset_with_token = asset.asset_name.replace(VERSION_SENTINEL, "{ version }");
310 let pkg_url = format!(
311 "{download_base}/{owner}/{repo}/releases/download/{tag_with_token}/{asset_with_token}"
312 );
313 map.insert(
314 target.clone(),
315 BinstallOverride {
316 pkg_url: Some(pkg_url),
317 pkg_fmt: Some(pkg_fmt.to_string()),
318 bin_dir: None,
319 },
320 );
321 }
322
323 if map.is_empty() {
324 return Ok(None);
325 }
326 Ok(Some(map))
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct ArchiveAssetName {
333 pub format: String,
335 pub asset_name: String,
338}
339
340pub fn crate_archive_asset_names(
356 crate_cfg: &CrateConfig,
357 default_targets: &[String],
358 ctx: &mut Context,
359) -> Result<Option<BTreeMap<String, ArchiveAssetName>>> {
360 let Some(archive) = binstallable_archive(crate_cfg) else {
361 return Ok(None);
362 };
363 let targets = derive_target_list(crate_cfg, default_targets);
364 if targets.is_empty() {
365 return Ok(None);
366 }
367
368 let name_template = archive
372 .name_template
373 .clone()
374 .unwrap_or_else(|| default_archive_name_template(ctx));
375 let global_default_format = global_default_archive_format(ctx);
376
377 let mut map: BTreeMap<String, ArchiveAssetName> = BTreeMap::new();
378 for target in &targets {
379 let format = archive_format_for_target(&archive, target, &global_default_format);
380 let amd64_variant =
385 crate::build_env::config_time_amd64_variant(crate_cfg, target, default_targets, ctx)?;
386 let asset_name = render_archive_asset_name_with_variant(
387 ctx,
388 &name_template,
389 target,
390 &format,
391 amd64_variant.as_deref(),
392 )?;
393 map.insert(target.clone(), ArchiveAssetName { format, asset_name });
394 }
395 Ok(Some(map))
396}
397
398fn release_repo(crate_cfg: &CrateConfig, ctx: &Context) -> Option<(String, String, String)> {
403 let release = crate_cfg.release.as_ref()?;
404 let (repo_cfg, base) = if let Some(gh) = release.github.as_ref() {
408 (gh, "https://github.com")
409 } else if let Some(gl) = release.gitlab.as_ref() {
410 (gl, "https://gitlab.com")
411 } else if let Some(gt) = release.gitea.as_ref() {
412 (gt, "https://gitea.com")
413 } else {
414 return None;
415 };
416 let owner = ctx.render_template(&repo_cfg.owner).ok()?;
417 let repo = ctx.render_template(&repo_cfg.name).ok()?;
418 if owner.is_empty() || repo.is_empty() {
419 return None;
420 }
421 Some((owner, repo, base.to_string()))
422}
423
424pub(crate) fn binstallable_archive(crate_cfg: &CrateConfig) -> Option<ArchiveConfig> {
429 let ArchivesConfig::Configs(configs) = &crate_cfg.archives else {
430 return None;
431 };
432 configs
433 .iter()
434 .find(|a| {
435 a.formats
436 .as_ref()
437 .and_then(|f| f.first())
438 .map(|fmt| binstall_pkg_fmt(fmt).is_some())
439 .unwrap_or(true)
440 })
441 .cloned()
442 .or_else(|| configs.first().cloned())
443}
444
445fn derive_target_list(crate_cfg: &CrateConfig, default_targets: &[String]) -> Vec<String> {
454 crate::build_plan::crate_target_list(crate_cfg, default_targets)
455}
456
457fn render_tag_with_version_token(crate_cfg: &CrateConfig, ctx: &mut Context) -> Result<String> {
461 let tag_template = crate_cfg
464 .release
465 .as_ref()
466 .and_then(|r| r.tag.clone())
467 .filter(|t| !t.is_empty())
468 .or_else(|| Some(crate_cfg.tag_template.clone()).filter(|t| !t.is_empty()))
469 .unwrap_or_else(|| "v{{ Version }}".to_string());
470
471 let prior = stamp_sentinel_version(ctx);
472 let rendered = ctx.render_template(&tag_template);
473 restore_version(ctx, prior);
474 let rendered = rendered
475 .with_context(|| format!("failed to render binstall tag template: {tag_template}"))?;
476 Ok(rendered.replace(VERSION_SENTINEL, "{ version }"))
477}
478
479fn stamp_sentinel_version(ctx: &mut Context) -> Vec<(&'static str, Option<String>)> {
484 let prior: Vec<(&'static str, Option<String>)> = ["Version", "RawVersion", "Tag"]
485 .iter()
486 .map(|k| (*k, ctx.template_vars().get(k).cloned()))
487 .collect();
488
489 let vars = ctx.template_vars_mut();
490 vars.set("Version", VERSION_SENTINEL);
491 vars.set("RawVersion", VERSION_SENTINEL);
492 vars.set("Tag", &format!("v{VERSION_SENTINEL}"));
495 prior
496}
497
498fn restore_version(ctx: &mut Context, prior: Vec<(&'static str, Option<String>)>) {
500 let vars = ctx.template_vars_mut();
501 for (key, value) in prior {
502 match value {
503 Some(v) => vars.set(key, &v),
504 None => {
505 vars.unset(key);
506 }
507 }
508 }
509}
510
511fn default_archive_name_template(ctx: &Context) -> String {
518 let producing = crate::archive_selection::archive_producing_crates(
519 &ctx.config,
520 &ctx.artifacts,
521 &ctx.options.selected_crates,
522 );
523 if producing.len() > 1 {
524 crate::archive_name::DEFAULT_NAME_TEMPLATE_MULTI_CRATE.to_string()
525 } else {
526 crate::archive_name::DEFAULT_NAME_TEMPLATE.to_string()
527 }
528}
529
530fn global_default_archive_format(ctx: &Context) -> String {
533 ctx.config
534 .defaults
535 .as_ref()
536 .and_then(|d| d.archives.as_ref())
537 .and_then(|a| a.formats.as_ref())
538 .and_then(|f| f.first())
539 .cloned()
540 .unwrap_or_else(|| "tar.gz".to_string())
541}
542
543fn archive_format_for_target(
547 archive: &ArchiveConfig,
548 target: &str,
549 global_default: &str,
550) -> String {
551 let (os, _arch) = map_target(target);
552 if let Some(overrides) = archive.format_overrides.as_ref() {
553 for ov in overrides {
554 if !ov.os.is_empty()
555 && os.starts_with(&ov.os)
556 && let Some(fmts) = ov.formats.as_ref()
557 && let Some(first) = fmts.first()
558 {
559 return first.clone();
560 }
561 }
562 }
563 archive
564 .formats
565 .as_ref()
566 .and_then(|f| f.first())
567 .cloned()
568 .unwrap_or_else(|| global_default.to_string())
569}
570
571#[cfg(test)]
572mod tests {
573 use std::collections::{BTreeMap, HashMap};
574
575 use super::*;
576 use crate::config::{
577 ArchiveConfig, ArchivesConfig, BinstallConfig, BinstallOverride, BuildConfig, Config,
578 FormatOverride, GitHubConfig, ReleaseConfig,
579 };
580 use crate::context::{Context, ContextOptions};
581
582 fn make_ctx() -> Context {
583 let config = Config {
584 project_name: "myapp".to_string(),
585 ..Default::default()
586 };
587 let mut ctx = Context::new(config, ContextOptions::default());
588 ctx.set_env_source(crate::MapEnvSource::new());
591 ctx.template_vars_mut().set("Version", "1.0.0");
592 ctx.template_vars_mut().set("ProjectName", "myapp");
593 ctx
594 }
595
596 fn gen_meta(path: &str, cfg: &BinstallConfig, ctx: &mut Context, dry_run: bool) -> Result<()> {
602 let crate_cfg = CrateConfig {
603 name: "myapp".to_string(),
604 path: path.to_string(),
605 ..Default::default()
606 };
607 generate_binstall_metadata(&crate_cfg, cfg, &[], ctx, dry_run)
608 }
609
610 fn gen_with_crate(
613 path: &str,
614 mut crate_cfg: CrateConfig,
615 cfg: &BinstallConfig,
616 default_targets: &[String],
617 ctx: &mut Context,
618 ) -> Result<()> {
619 crate_cfg.path = path.to_string();
620 generate_binstall_metadata(&crate_cfg, cfg, default_targets, ctx, false)
621 }
622
623 #[test]
624 fn test_generate_binstall_metadata_inserts_section() {
625 let tmp = tempfile::tempdir().unwrap();
626 let cargo_toml = tmp.path().join("Cargo.toml");
627 std::fs::write(
628 &cargo_toml,
629 r#"[package]
630name = "myapp"
631version = "1.0.0"
632edition = "2024"
633"#,
634 )
635 .unwrap();
636
637 let binstall_cfg = BinstallConfig {
638 enabled: Some(true),
639 pkg_url: Some(
640 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{{ .Version }}-{ target }.tar.gz"
641 .to_string(),
642 ),
643 bin_dir: Some("{ bin }{ binary-ext }".to_string()),
644 pkg_fmt: Some("tgz".to_string()),
645 overrides: None,
646 };
647
648 let mut ctx = make_ctx();
649 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
650
651 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
652 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
653
654 let binstall = &doc["package"]["metadata"]["binstall"];
655 assert!(
656 binstall.as_table().is_some(),
657 "binstall section should exist as a table"
658 );
659 assert_eq!(binstall["pkg-fmt"].as_str().unwrap(), "tgz");
660 assert_eq!(
661 binstall["bin-dir"].as_str().unwrap(),
662 "{ bin }{ binary-ext }"
663 );
664 let pkg_url = binstall["pkg-url"].as_str().unwrap();
666 assert!(
667 pkg_url.contains("1.0.0"),
668 "pkg-url should have Version rendered, got: {pkg_url}"
669 );
670 }
671
672 #[test]
673 fn test_generate_binstall_metadata_dry_run() {
674 let tmp = tempfile::tempdir().unwrap();
675 let cargo_toml = tmp.path().join("Cargo.toml");
676 let original = r#"[package]
677name = "myapp"
678version = "1.0.0"
679edition = "2024"
680"#;
681 std::fs::write(&cargo_toml, original).unwrap();
682
683 let binstall_cfg = BinstallConfig {
684 enabled: Some(true),
685 pkg_url: Some("https://example.com".to_string()),
686 bin_dir: None,
687 pkg_fmt: None,
688 overrides: None,
689 };
690
691 let mut ctx = make_ctx();
692 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, true).unwrap();
693
694 let content = std::fs::read_to_string(&cargo_toml).unwrap();
696 assert_eq!(content, original);
697 }
698
699 #[test]
700 fn test_generate_binstall_metadata_missing_cargo_toml_errors() {
701 let tmp = tempfile::tempdir().unwrap();
702 let binstall_cfg = BinstallConfig {
703 enabled: Some(true),
704 pkg_url: None,
705 bin_dir: None,
706 pkg_fmt: None,
707 overrides: None,
708 };
709
710 let mut ctx = make_ctx();
711 let result = gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false);
712 assert!(result.is_err());
713 }
714
715 #[test]
716 fn test_generate_binstall_metadata_emits_per_target_overrides() {
717 let tmp = tempfile::tempdir().unwrap();
718 let cargo_toml = tmp.path().join("Cargo.toml");
719 std::fs::write(
720 &cargo_toml,
721 r#"[package]
722name = "cfgd"
723version = "1.0.0"
724edition = "2024"
725"#,
726 )
727 .unwrap();
728
729 let mut overrides = BTreeMap::new();
730 overrides.insert(
731 "x86_64-unknown-linux-gnu".to_string(),
732 BinstallOverride {
733 pkg_url: Some(
734 "https://github.com/myorg/cfgd/releases/download/v{{ .Version }}/cfgd-{{ .Version }}-linux-amd64.tar.gz"
735 .to_string(),
736 ),
737 pkg_fmt: Some("tgz".to_string()),
738 bin_dir: Some("{ bin }{ binary-ext }".to_string()),
739 },
740 );
741 overrides.insert(
742 "aarch64-apple-darwin".to_string(),
743 BinstallOverride {
744 pkg_url: Some(
745 "https://github.com/myorg/cfgd/releases/download/v{{ .Version }}/cfgd-{ version }-darwin-arm64.tar.gz"
746 .to_string(),
747 ),
748 pkg_fmt: Some("tgz".to_string()),
749 bin_dir: None,
750 },
751 );
752
753 let binstall_cfg = BinstallConfig {
754 enabled: Some(true),
755 pkg_url: None,
756 bin_dir: None,
757 pkg_fmt: None,
758 overrides: Some(overrides),
759 };
760
761 let mut ctx = make_ctx();
762 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
763
764 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
765 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
766
767 let overrides_item = &doc["package"]["metadata"]["binstall"]["overrides"];
768 assert!(
769 overrides_item.as_table().is_some(),
770 "binstall.overrides should exist as a table"
771 );
772
773 let linux = &overrides_item["x86_64-unknown-linux-gnu"];
775 assert!(
776 linux.as_table().is_some(),
777 "override sub-table should be a real table"
778 );
779 let linux_url = linux["pkg-url"].as_str().unwrap();
780 assert!(
781 linux_url.contains("cfgd-1.0.0-linux-amd64.tar.gz"),
782 "linux pkg-url should be Version-rendered with go-arch asset name, got: {linux_url}"
783 );
784 assert!(
785 !linux_url.contains("{{ .Version }}"),
786 "anodize token should be rendered, got: {linux_url}"
787 );
788 assert_eq!(linux["pkg-fmt"].as_str().unwrap(), "tgz");
789 assert_eq!(linux["bin-dir"].as_str().unwrap(), "{ bin }{ binary-ext }");
790
791 let darwin = &overrides_item["aarch64-apple-darwin"];
793 assert!(darwin.as_table().is_some());
794 let darwin_url = darwin["pkg-url"].as_str().unwrap();
795 assert!(
798 darwin_url.contains("/v1.0.0/cfgd-{ version }-darwin-arm64.tar.gz"),
799 "darwin pkg-url should render the anodize token but leave cargo-binstall's `{{ version }}` intact, got: {darwin_url}"
800 );
801
802 assert!(
804 updated.contains("[package.metadata.binstall.overrides.x86_64-unknown-linux-gnu]"),
805 "override should render as a [...] header, got:\n{updated}"
806 );
807 }
808
809 #[test]
810 fn test_generate_binstall_metadata_preserves_user_authored_keys() {
811 let tmp = tempfile::tempdir().unwrap();
812 let cargo_toml = tmp.path().join("Cargo.toml");
813 std::fs::write(
818 &cargo_toml,
819 r#"[package]
820name = "myapp"
821version = "1.0.0"
822edition = "2024"
823
824[package.metadata.binstall]
825disabled-strategies = ["quick-install", "compile"]
826pkg-url = "https://old.example.com/stale"
827
828[package.metadata.binstall.signing]
829algorithm = "minisign"
830pubkey = "RWQABCDEF1234567890"
831"#,
832 )
833 .unwrap();
834
835 let mut overrides = BTreeMap::new();
836 overrides.insert(
837 "x86_64-unknown-linux-gnu".to_string(),
838 BinstallOverride {
839 pkg_url: Some(
840 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-linux.tar.gz"
841 .to_string(),
842 ),
843 pkg_fmt: Some("tgz".to_string()),
844 bin_dir: None,
845 },
846 );
847
848 let binstall_cfg = BinstallConfig {
849 enabled: Some(true),
850 pkg_url: Some(
851 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{ target }.tar.gz"
852 .to_string(),
853 ),
854 bin_dir: None,
855 pkg_fmt: None,
856 overrides: Some(overrides),
857 };
858
859 let mut ctx = make_ctx();
860 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
861
862 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
863 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
864 let binstall = &doc["package"]["metadata"]["binstall"];
865
866 let strategies = binstall["disabled-strategies"].as_array().unwrap();
868 let strategy_vals: Vec<&str> = strategies.iter().filter_map(|v| v.as_str()).collect();
869 assert_eq!(
870 strategy_vals,
871 vec!["quick-install", "compile"],
872 "disabled-strategies must survive the merge verbatim"
873 );
874
875 let signing = &binstall["signing"];
876 assert!(
877 signing.as_table().is_some(),
878 "signing sub-table must survive the merge"
879 );
880 assert_eq!(signing["algorithm"].as_str().unwrap(), "minisign");
881 assert_eq!(signing["pubkey"].as_str().unwrap(), "RWQABCDEF1234567890");
882
883 let pkg_url = binstall["pkg-url"].as_str().unwrap();
885 assert!(
886 pkg_url.contains("/v1.0.0/myapp-{ target }.tar.gz"),
887 "pkg-url should be rewritten with the rendered Version, got: {pkg_url}"
888 );
889 assert!(
890 !pkg_url.contains("old.example.com"),
891 "stale anodize-owned pkg-url should be replaced, got: {pkg_url}"
892 );
893
894 let linux = &binstall["overrides"]["x86_64-unknown-linux-gnu"];
896 assert_eq!(linux["pkg-fmt"].as_str().unwrap(), "tgz");
897 assert!(
898 linux["pkg-url"]
899 .as_str()
900 .unwrap()
901 .contains("/v1.0.0/myapp-linux.tar.gz")
902 );
903 }
904
905 #[test]
906 fn test_generate_binstall_metadata_clears_unset_owned_key_keeps_unknown() {
907 let tmp = tempfile::tempdir().unwrap();
908 let cargo_toml = tmp.path().join("Cargo.toml");
909 std::fs::write(
912 &cargo_toml,
913 r#"[package]
914name = "myapp"
915version = "1.0.0"
916edition = "2024"
917
918[package.metadata.binstall]
919disabled-strategies = ["compile"]
920pkg-url = "https://old.example.com/stale"
921"#,
922 )
923 .unwrap();
924
925 let binstall_cfg = BinstallConfig {
926 enabled: Some(true),
927 pkg_url: None,
928 bin_dir: None,
929 pkg_fmt: None,
930 overrides: None,
931 };
932
933 let mut ctx = make_ctx();
934 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
935
936 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
937 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
938 let binstall = &doc["package"]["metadata"]["binstall"];
939
940 assert!(
941 binstall.get("pkg-url").is_none(),
942 "unset owned key should be cleared, got:\n{updated}"
943 );
944 let strategies = binstall["disabled-strategies"].as_array().unwrap();
945 assert_eq!(strategies.len(), 1, "unknown key must survive clearing");
946 assert_eq!(strategies.get(0).unwrap().as_str().unwrap(), "compile");
947 }
948
949 #[test]
950 fn test_generate_binstall_metadata_merges_inline_table_preserving_unknown() {
951 let tmp = tempfile::tempdir().unwrap();
952 let cargo_toml = tmp.path().join("Cargo.toml");
953 std::fs::write(
957 &cargo_toml,
958 r#"[package]
959name = "myapp"
960version = "1.0.0"
961edition = "2024"
962metadata.binstall = { pkg-url = "https://old.example.com/stale", disabled-strategies = ["compile"] }
963"#,
964 )
965 .unwrap();
966
967 let binstall_cfg = BinstallConfig {
968 enabled: Some(true),
969 pkg_url: Some(
970 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{ target }.tar.gz"
971 .to_string(),
972 ),
973 bin_dir: None,
974 pkg_fmt: None,
975 overrides: None,
976 };
977
978 let mut ctx = make_ctx();
979 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
980
981 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
982 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
983 let binstall = &doc["package"]["metadata"]["binstall"];
984
985 let pkg_url = binstall["pkg-url"].as_str().unwrap();
987 assert!(
988 pkg_url.contains("/v1.0.0/myapp-{ target }.tar.gz")
989 && !pkg_url.contains("old.example.com"),
990 "inline pkg-url should be rewritten, got: {pkg_url}"
991 );
992 let strategies = binstall["disabled-strategies"].as_array().unwrap();
994 assert_eq!(strategies.len(), 1);
995 assert_eq!(strategies.get(0).unwrap().as_str().unwrap(), "compile");
996 }
997
998 fn six_targets() -> Vec<String> {
1004 vec![
1005 "x86_64-unknown-linux-gnu".to_string(),
1006 "aarch64-unknown-linux-gnu".to_string(),
1007 "x86_64-apple-darwin".to_string(),
1008 "aarch64-apple-darwin".to_string(),
1009 "x86_64-pc-windows-msvc".to_string(),
1010 "aarch64-pc-windows-msvc".to_string(),
1011 ]
1012 }
1013
1014 fn anodize_like_crate() -> CrateConfig {
1018 let archive = ArchiveConfig {
1019 name_template: Some("{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}".to_string()),
1020 formats: Some(vec!["tar.gz".to_string()]),
1021 format_overrides: Some(vec![FormatOverride {
1022 os: "windows".to_string(),
1023 formats: Some(vec!["zip".to_string()]),
1024 }]),
1025 ..Default::default()
1026 };
1027 CrateConfig {
1028 name: "anodizer".to_string(),
1029 tag_template: "v{{ Version }}".to_string(),
1030 archives: ArchivesConfig::Configs(vec![archive]),
1031 release: Some(ReleaseConfig {
1032 github: Some(GitHubConfig {
1033 owner: "tj-smith47".to_string(),
1034 name: "anodizer".to_string(),
1035 }),
1036 ..Default::default()
1037 }),
1038 ..Default::default()
1039 }
1040 }
1041
1042 fn override_asset(binstall: &toml_edit::Item, triple: &str, version: &str) -> String {
1045 let url = binstall["overrides"][triple]["pkg-url"].as_str().unwrap();
1046 let leaf = url.rsplit('/').next().unwrap();
1047 leaf.replace("{ version }", version)
1048 }
1049
1050 #[test]
1051 fn auto_derive_covers_all_six_triples_matching_archive_names() {
1052 let tmp = tempfile::tempdir().unwrap();
1053 let cargo_toml = tmp.path().join("Cargo.toml");
1054 std::fs::write(
1055 &cargo_toml,
1056 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1057 )
1058 .unwrap();
1059 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1063 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1064
1065 let cfg = BinstallConfig {
1067 enabled: Some(true),
1068 ..Default::default()
1069 };
1070 let mut ctx = make_ctx();
1071 ctx.template_vars_mut().set("ProjectName", "anodizer");
1072 let targets = six_targets();
1073 gen_with_crate(
1074 tmp.path().to_str().unwrap(),
1075 anodize_like_crate(),
1076 &cfg,
1077 &targets,
1078 &mut ctx,
1079 )
1080 .unwrap();
1081
1082 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1083 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1084 let binstall = &doc["package"]["metadata"]["binstall"];
1085
1086 let mut check_ctx = make_ctx();
1091 check_ctx.template_vars_mut().set("ProjectName", "anodizer");
1092 let name_tmpl = "{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}";
1093 for triple in &targets {
1094 let fmt = if triple.contains("windows") {
1095 "zip"
1096 } else {
1097 "tar.gz"
1098 };
1099 let expected = crate::archive_name::render_archive_asset_name(
1100 &mut check_ctx,
1101 name_tmpl,
1102 triple,
1103 fmt,
1104 )
1105 .unwrap();
1106 let actual = override_asset(binstall, triple, "1.0.0");
1107 assert_eq!(
1108 actual, expected,
1109 "override asset for {triple} must equal the archive stage's name"
1110 );
1111
1112 let pkg_fmt = binstall["overrides"][triple]["pkg-fmt"].as_str().unwrap();
1114 assert_eq!(pkg_fmt, if fmt == "zip" { "zip" } else { "tgz" });
1115
1116 let url = binstall["overrides"][triple]["pkg-url"].as_str().unwrap();
1119 assert!(
1120 url.starts_with(
1121 "https://github.com/tj-smith47/anodizer/releases/download/v{ version }/"
1122 ),
1123 "url should target the GitHub release download path with the binstall version token, got: {url}"
1124 );
1125 assert!(
1126 url.contains("{ version }"),
1127 "url must carry cargo-binstall's version token, got: {url}"
1128 );
1129 }
1130
1131 assert_eq!(
1133 override_asset(binstall, "x86_64-unknown-linux-gnu", "9.9.9"),
1134 "anodizer-9.9.9-linux-amd64.tar.gz"
1135 );
1136 assert_eq!(
1137 override_asset(binstall, "aarch64-pc-windows-msvc", "9.9.9"),
1138 "anodizer-9.9.9-windows-arm64.zip"
1139 );
1140
1141 assert!(
1143 binstall.get("pkg-url").is_none(),
1144 "auto-derivation must not write a top-level pkg-url, got:\n{updated}"
1145 );
1146 }
1147
1148 #[test]
1149 fn auto_derive_matches_real_v091_release_assets() {
1150 let tmp = tempfile::tempdir().unwrap();
1158 let cargo_toml = tmp.path().join("Cargo.toml");
1159 std::fs::write(
1160 &cargo_toml,
1161 "[package]\nname = \"anodizer\"\nversion = \"0.9.1\"\nedition = \"2024\"\n",
1162 )
1163 .unwrap();
1164 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1167 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1168
1169 let cfg = BinstallConfig {
1170 enabled: Some(true),
1171 ..Default::default()
1172 };
1173 let mut ctx = make_ctx();
1174 ctx.template_vars_mut().set("ProjectName", "anodizer");
1175 gen_with_crate(
1176 tmp.path().to_str().unwrap(),
1177 anodize_like_crate(),
1178 &cfg,
1179 &six_targets(),
1180 &mut ctx,
1181 )
1182 .unwrap();
1183
1184 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1185 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1186 let binstall = &doc["package"]["metadata"]["binstall"];
1187
1188 let expected: &[(&str, &str, &str)] = &[
1190 (
1191 "x86_64-unknown-linux-gnu",
1192 "anodizer-0.9.1-linux-amd64.tar.gz",
1193 "tgz",
1194 ),
1195 (
1196 "aarch64-unknown-linux-gnu",
1197 "anodizer-0.9.1-linux-arm64.tar.gz",
1198 "tgz",
1199 ),
1200 (
1201 "x86_64-apple-darwin",
1202 "anodizer-0.9.1-darwin-amd64.tar.gz",
1203 "tgz",
1204 ),
1205 (
1206 "aarch64-apple-darwin",
1207 "anodizer-0.9.1-darwin-arm64.tar.gz",
1208 "tgz",
1209 ),
1210 (
1211 "x86_64-pc-windows-msvc",
1212 "anodizer-0.9.1-windows-amd64.zip",
1213 "zip",
1214 ),
1215 (
1216 "aarch64-pc-windows-msvc",
1217 "anodizer-0.9.1-windows-arm64.zip",
1218 "zip",
1219 ),
1220 ];
1221
1222 for (triple, asset, fmt) in expected {
1223 let url = binstall["overrides"][*triple]["pkg-url"].as_str().unwrap();
1224 let resolved = url.replace("{ version }", "0.9.1");
1227 let want =
1228 format!("https://github.com/tj-smith47/anodizer/releases/download/v0.9.1/{asset}");
1229 assert_eq!(
1230 resolved, want,
1231 "override for {triple} must resolve to the real v0.9.1 asset URL"
1232 );
1233 assert_eq!(
1234 binstall["overrides"][*triple]["pkg-fmt"].as_str().unwrap(),
1235 *fmt,
1236 "pkg-fmt for {triple} must match the real v0.9.1 asset format"
1237 );
1238 }
1239 }
1240
1241 #[test]
1242 fn user_pkg_url_suppresses_auto_derivation() {
1243 let tmp = tempfile::tempdir().unwrap();
1244 let cargo_toml = tmp.path().join("Cargo.toml");
1245 std::fs::write(
1246 &cargo_toml,
1247 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1248 )
1249 .unwrap();
1250
1251 let cfg = BinstallConfig {
1252 enabled: Some(true),
1253 pkg_url: Some("https://example.com/custom/anodizer-{ target }.tar.gz".to_string()),
1254 ..Default::default()
1255 };
1256 let mut ctx = make_ctx();
1257 ctx.template_vars_mut().set("ProjectName", "anodizer");
1258 gen_with_crate(
1259 tmp.path().to_str().unwrap(),
1260 anodize_like_crate(),
1261 &cfg,
1262 &six_targets(),
1263 &mut ctx,
1264 )
1265 .unwrap();
1266
1267 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1268 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1269 let binstall = &doc["package"]["metadata"]["binstall"];
1270
1271 assert_eq!(
1273 binstall["pkg-url"].as_str().unwrap(),
1274 "https://example.com/custom/anodizer-{ target }.tar.gz"
1275 );
1276 assert!(
1277 binstall.get("overrides").is_none(),
1278 "a user pkg_url must suppress auto-derived overrides, got:\n{updated}"
1279 );
1280 }
1281
1282 #[test]
1283 fn user_override_suppresses_auto_derivation() {
1284 let tmp = tempfile::tempdir().unwrap();
1285 let cargo_toml = tmp.path().join("Cargo.toml");
1286 std::fs::write(
1287 &cargo_toml,
1288 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1289 )
1290 .unwrap();
1291
1292 let mut user_overrides = BTreeMap::new();
1293 user_overrides.insert(
1294 "x86_64-unknown-linux-gnu".to_string(),
1295 BinstallOverride {
1296 pkg_url: Some("https://example.com/manual-linux.tar.gz".to_string()),
1297 pkg_fmt: Some("tgz".to_string()),
1298 bin_dir: None,
1299 },
1300 );
1301 let cfg = BinstallConfig {
1302 enabled: Some(true),
1303 overrides: Some(user_overrides),
1304 ..Default::default()
1305 };
1306 let mut ctx = make_ctx();
1307 ctx.template_vars_mut().set("ProjectName", "anodizer");
1308 gen_with_crate(
1309 tmp.path().to_str().unwrap(),
1310 anodize_like_crate(),
1311 &cfg,
1312 &six_targets(),
1313 &mut ctx,
1314 )
1315 .unwrap();
1316
1317 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1318 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1319 let overrides = &doc["package"]["metadata"]["binstall"]["overrides"];
1320
1321 assert_eq!(
1324 overrides["x86_64-unknown-linux-gnu"]["pkg-url"]
1325 .as_str()
1326 .unwrap(),
1327 "https://example.com/manual-linux.tar.gz"
1328 );
1329 assert!(
1330 overrides.get("aarch64-apple-darwin").is_none(),
1331 "supplying one override must suppress auto-derivation of the rest, got:\n{updated}"
1332 );
1333 }
1334
1335 #[test]
1336 fn auto_derive_uses_default_template_when_unset() {
1337 let tmp = tempfile::tempdir().unwrap();
1340 let cargo_toml = tmp.path().join("Cargo.toml");
1341 std::fs::write(
1342 &cargo_toml,
1343 "[package]\nname = \"myapp\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1344 )
1345 .unwrap();
1346
1347 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1350 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1351
1352 let mut crate_cfg = anodize_like_crate();
1353 crate_cfg.name = "myapp".to_string();
1354 crate_cfg.archives = ArchivesConfig::Configs(vec![ArchiveConfig {
1356 formats: Some(vec!["tar.gz".to_string()]),
1357 ..Default::default()
1358 }]);
1359
1360 let cfg = BinstallConfig {
1361 enabled: Some(true),
1362 ..Default::default()
1363 };
1364 let mut ctx = make_ctx();
1365 gen_with_crate(
1366 tmp.path().to_str().unwrap(),
1367 crate_cfg,
1368 &cfg,
1369 &["x86_64-unknown-linux-gnu".to_string()],
1370 &mut ctx,
1371 )
1372 .unwrap();
1373
1374 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1375 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1376 let binstall = &doc["package"]["metadata"]["binstall"];
1377 assert_eq!(
1379 override_asset(binstall, "x86_64-unknown-linux-gnu", "1.0.0"),
1380 "myapp_1.0.0_linux_amd64.tar.gz"
1381 );
1382 }
1383
1384 #[test]
1385 fn auto_derive_noops_without_release_repo() {
1386 let tmp = tempfile::tempdir().unwrap();
1388 let cargo_toml = tmp.path().join("Cargo.toml");
1389 std::fs::write(
1390 &cargo_toml,
1391 "[package]\nname = \"myapp\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1392 )
1393 .unwrap();
1394
1395 let mut crate_cfg = anodize_like_crate();
1396 crate_cfg.name = "myapp".to_string();
1397 crate_cfg.release = None;
1398
1399 let cfg = BinstallConfig {
1400 enabled: Some(true),
1401 ..Default::default()
1402 };
1403 let mut ctx = make_ctx();
1404 gen_with_crate(
1405 tmp.path().to_str().unwrap(),
1406 crate_cfg,
1407 &cfg,
1408 &six_targets(),
1409 &mut ctx,
1410 )
1411 .unwrap();
1412
1413 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1414 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1415 let binstall = &doc["package"]["metadata"]["binstall"];
1416 assert!(
1417 binstall.get("overrides").is_none(),
1418 "no release repo should mean no auto-derived overrides, got:\n{updated}"
1419 );
1420 }
1421
1422 #[test]
1425 fn normalize_inserts_empty_table_when_binstall_absent() {
1426 let mut metadata = toml_edit::Table::new();
1427 normalize_binstall_to_table(&mut metadata).unwrap();
1428 let binstall = metadata.get("binstall").expect("binstall inserted");
1429 assert!(binstall.is_table(), "must be a header table");
1430 assert!(
1431 binstall.as_table().unwrap().is_empty(),
1432 "freshly-inserted binstall table must be empty"
1433 );
1434 }
1435
1436 #[test]
1437 fn normalize_leaves_existing_header_table_untouched() {
1438 let mut metadata = toml_edit::Table::new();
1439 let mut existing = toml_edit::Table::new();
1440 existing.insert("pkg-url", toml_edit::value("https://example/x"));
1441 metadata.insert("binstall", toml_edit::Item::Table(existing));
1442 normalize_binstall_to_table(&mut metadata).unwrap();
1443 assert_eq!(
1444 metadata["binstall"]["pkg-url"].as_str(),
1445 Some("https://example/x"),
1446 "an existing header table must be preserved verbatim"
1447 );
1448 }
1449
1450 #[test]
1451 fn normalize_converts_inline_table_preserving_keys() {
1452 let doc: toml_edit::DocumentMut = r#"[package.metadata]
1455binstall = { pkg-url = "https://example/x", custom = "keep" }
1456"#
1457 .parse()
1458 .unwrap();
1459 let mut metadata = doc["package"]["metadata"].as_table().unwrap().clone();
1460 normalize_binstall_to_table(&mut metadata).unwrap();
1461 let binstall = metadata["binstall"].as_table().expect("now a header table");
1462 assert_eq!(binstall["pkg-url"].as_str(), Some("https://example/x"));
1463 assert_eq!(
1464 binstall["custom"].as_str(),
1465 Some("keep"),
1466 "user-authored inline keys must survive the conversion"
1467 );
1468 }
1469
1470 #[test]
1471 fn normalize_errors_on_non_table_binstall() {
1472 let mut metadata = toml_edit::Table::new();
1474 metadata.insert("binstall", toml_edit::value("oops"));
1475 assert!(
1476 normalize_binstall_to_table(&mut metadata).is_err(),
1477 "a malformed scalar binstall must error, not silently pass"
1478 );
1479 }
1480
1481 #[test]
1484 fn set_or_remove_str_sets_and_removes() {
1485 let mut table = toml_edit::Table::new();
1486 set_or_remove_str(&mut table, "k", Some("v"));
1487 assert_eq!(table["k"].as_str(), Some("v"));
1488 table.insert("sibling", toml_edit::value("keep"));
1490 set_or_remove_str(&mut table, "k", None);
1491 assert!(table.get("k").is_none(), "None must remove the key");
1492 assert_eq!(
1493 table["sibling"].as_str(),
1494 Some("keep"),
1495 "removing one key must not disturb siblings"
1496 );
1497 }
1498
1499 #[test]
1502 fn render_overrides_none_when_unset_or_empty() {
1503 let ctx = make_ctx();
1504 let cfg = BinstallConfig {
1505 overrides: None,
1506 ..Default::default()
1507 };
1508 assert!(
1509 render_overrides(&cfg, &ctx).unwrap().is_none(),
1510 "no overrides configured → None"
1511 );
1512 let cfg_empty = BinstallConfig {
1513 overrides: Some(BTreeMap::new()),
1514 ..Default::default()
1515 };
1516 assert!(
1517 render_overrides(&cfg_empty, &ctx).unwrap().is_none(),
1518 "an empty overrides map → None"
1519 );
1520 }
1521
1522 #[test]
1523 fn render_overrides_map_empty_is_none() {
1524 let ctx = make_ctx();
1525 let empty: BTreeMap<String, BinstallOverride> = BTreeMap::new();
1526 assert!(
1527 render_overrides_map(&empty, &ctx).unwrap().is_none(),
1528 "an empty map renders to None, never an empty table"
1529 );
1530 }
1531
1532 fn tuned_crate(env: HashMap<String, HashMap<String, String>>) -> CrateConfig {
1537 CrateConfig {
1538 name: "myapp".to_string(),
1539 builds: Some(vec![BuildConfig {
1540 binary: Some("myapp".to_string()),
1541 targets: Some(vec![
1542 "x86_64-unknown-linux-gnu".to_string(),
1543 "aarch64-unknown-linux-gnu".to_string(),
1544 "x86_64-apple-darwin".to_string(),
1545 ]),
1546 env: Some(env),
1547 ..Default::default()
1548 }]),
1549 archives: ArchivesConfig::Configs(vec![ArchiveConfig {
1550 formats: Some(vec!["tar.gz".to_string()]),
1551 ..Default::default()
1552 }]),
1553 ..Default::default()
1554 }
1555 }
1556
1557 #[test]
1564 fn asset_names_carry_config_declared_amd64_variant() {
1565 let mut env = HashMap::new();
1566 env.insert(
1567 "x86_64-unknown-linux-gnu".to_string(),
1568 HashMap::from([(
1569 "RUSTFLAGS".to_string(),
1570 "-Ctarget-cpu=x86-64-v3".to_string(),
1571 )]),
1572 );
1573 let crate_cfg = tuned_crate(env);
1574 let mut ctx = make_ctx();
1575 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1576 .unwrap()
1577 .expect("binstallable archive with targets derives names");
1578 assert_eq!(
1579 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64v3.tar.gz",
1580 "tuned target must carry the micro-arch suffix"
1581 );
1582 assert_eq!(
1583 assets["aarch64-unknown-linux-gnu"].asset_name,
1584 "myapp_1.0.0_linux_arm64.tar.gz"
1585 );
1586 assert_eq!(
1587 assets["x86_64-apple-darwin"].asset_name, "myapp_1.0.0_darwin_amd64.tar.gz",
1588 "untuned amd64 target stays on the baseline name"
1589 );
1590 }
1591
1592 #[test]
1599 fn os_templated_tuning_env_derives_per_target_levels_across_the_loop() {
1600 let mut env = HashMap::new();
1601 env.insert(
1602 "x86_64-*".to_string(),
1603 HashMap::from([(
1604 "RUSTFLAGS".to_string(),
1605 "{% if Os == \"darwin\" %}-Ctarget-cpu=x86-64-v2\
1606 {% else %}-Ctarget-cpu=x86-64-v3{% endif %}"
1607 .to_string(),
1608 )]),
1609 );
1610 let crate_cfg = tuned_crate(env);
1611 let mut ctx = make_ctx();
1612 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1613 .unwrap()
1614 .expect("binstallable archive with targets derives names");
1615 assert_eq!(
1616 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64v3.tar.gz",
1617 "first target must render the env with ITS OWN Os seeded"
1618 );
1619 assert_eq!(
1620 assets["x86_64-apple-darwin"].asset_name, "myapp_1.0.0_darwin_amd64v2.tar.gz",
1621 "second target must not render with the first target's stale Os"
1622 );
1623 assert_eq!(
1624 assets["aarch64-unknown-linux-gnu"].asset_name,
1625 "myapp_1.0.0_linux_arm64.tar.gz"
1626 );
1627 }
1628
1629 #[test]
1634 fn asset_names_fall_back_to_baseline_when_env_defeats_config_render() {
1635 let mut env = HashMap::new();
1636 env.insert(
1637 "x86_64-unknown-linux-gnu".to_string(),
1638 HashMap::from([(
1639 "RUSTFLAGS".to_string(),
1640 "{{ BuildTimeOnlyVar }}".to_string(),
1641 )]),
1642 );
1643 let crate_cfg = tuned_crate(env);
1644 let mut ctx = make_ctx();
1645 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1646 .unwrap()
1647 .expect("derivation still resolves");
1648 assert_eq!(
1649 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64.tar.gz",
1650 "unrenderable env derives no variant (baseline name)"
1651 );
1652 }
1653
1654 #[test]
1658 fn variant_derivation_does_not_leak_build_env_into_ctx() {
1659 let mut env = HashMap::new();
1660 env.insert(
1661 "x86_64-unknown-linux-gnu".to_string(),
1662 HashMap::from([
1663 (
1664 "RUSTFLAGS".to_string(),
1665 "-Ctarget-cpu=x86-64-v2".to_string(),
1666 ),
1667 ("MY_BUILD_KEY".to_string(), "leaky".to_string()),
1668 ]),
1669 );
1670 let crate_cfg = tuned_crate(env);
1671 let mut ctx = make_ctx();
1672 ctx.template_vars_mut().set_env("RUSTFLAGS", "prior-value");
1673 let before_keys = ctx.template_vars().all_env().len();
1674 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1675 .unwrap()
1676 .unwrap();
1677 assert_eq!(
1678 assets["x86_64-unknown-linux-gnu"].asset_name,
1679 "myapp_1.0.0_linux_amd64v2.tar.gz"
1680 );
1681 assert_eq!(
1682 ctx.template_vars().all_env().get("RUSTFLAGS").unwrap(),
1683 "prior-value",
1684 "the caller's prior env value must be restored after the cascade"
1685 );
1686 assert!(
1687 !ctx.template_vars().all_env().contains_key("MY_BUILD_KEY"),
1688 "cascade-only keys must be removed after derivation"
1689 );
1690 assert_eq!(ctx.template_vars().all_env().len(), before_keys);
1691 }
1692}