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 {
412 (release.gitea.as_ref()?, "https://gitea.com")
413 };
414 let owner = ctx.render_template(&repo_cfg.owner).ok()?;
415 let repo = ctx.render_template(&repo_cfg.name).ok()?;
416 if owner.is_empty() || repo.is_empty() {
417 return None;
418 }
419 Some((owner, repo, base.to_string()))
420}
421
422pub(crate) fn binstallable_archive(crate_cfg: &CrateConfig) -> Option<ArchiveConfig> {
427 let ArchivesConfig::Configs(configs) = &crate_cfg.archives else {
428 return None;
429 };
430 configs
431 .iter()
432 .find(|a| {
433 a.formats
434 .as_ref()
435 .and_then(|f| f.first())
436 .map(|fmt| binstall_pkg_fmt(fmt).is_some())
437 .unwrap_or(true)
438 })
439 .cloned()
440 .or_else(|| configs.first().cloned())
441}
442
443fn derive_target_list(crate_cfg: &CrateConfig, default_targets: &[String]) -> Vec<String> {
452 crate::build_plan::crate_target_list(crate_cfg, default_targets)
453}
454
455fn render_tag_with_version_token(crate_cfg: &CrateConfig, ctx: &mut Context) -> Result<String> {
459 let tag_template = crate_cfg
462 .release
463 .as_ref()
464 .and_then(|r| r.tag.clone())
465 .filter(|t| !t.is_empty())
466 .unwrap_or_else(|| crate_cfg.resolved_tag_template().to_string());
467
468 let prior = stamp_sentinel_version(ctx);
469 let rendered = ctx.render_template(&tag_template);
470 restore_version(ctx, prior);
471 let rendered = rendered
472 .with_context(|| format!("failed to render binstall tag template: {tag_template}"))?;
473 Ok(rendered.replace(VERSION_SENTINEL, "{ version }"))
474}
475
476fn stamp_sentinel_version(ctx: &mut Context) -> Vec<(&'static str, Option<String>)> {
481 let prior: Vec<(&'static str, Option<String>)> = ["Version", "RawVersion", "Tag"]
482 .iter()
483 .map(|k| (*k, ctx.template_vars().get(k).cloned()))
484 .collect();
485
486 let vars = ctx.template_vars_mut();
487 vars.set("Version", VERSION_SENTINEL);
488 vars.set("RawVersion", VERSION_SENTINEL);
489 vars.set("Tag", &format!("v{VERSION_SENTINEL}"));
492 prior
493}
494
495fn restore_version(ctx: &mut Context, prior: Vec<(&'static str, Option<String>)>) {
497 let vars = ctx.template_vars_mut();
498 for (key, value) in prior {
499 match value {
500 Some(v) => vars.set(key, &v),
501 None => {
502 vars.unset(key);
503 }
504 }
505 }
506}
507
508fn default_archive_name_template(ctx: &Context) -> String {
515 let producing = crate::archive_selection::archive_producing_crates(
516 &ctx.config,
517 &ctx.artifacts,
518 &ctx.options.selected_crates,
519 );
520 if producing.len() > 1 {
521 crate::archive_name::DEFAULT_NAME_TEMPLATE_MULTI_CRATE.to_string()
522 } else {
523 crate::archive_name::DEFAULT_NAME_TEMPLATE.to_string()
524 }
525}
526
527fn global_default_archive_format(ctx: &Context) -> String {
530 ctx.config
531 .defaults
532 .as_ref()
533 .and_then(|d| d.archives.as_ref())
534 .and_then(|a| a.formats.as_ref())
535 .and_then(|f| f.first())
536 .cloned()
537 .unwrap_or_else(|| "tar.gz".to_string())
538}
539
540fn archive_format_for_target(
544 archive: &ArchiveConfig,
545 target: &str,
546 global_default: &str,
547) -> String {
548 let (os, _arch) = map_target(target);
549 if let Some(overrides) = archive.format_overrides.as_ref() {
550 for ov in overrides {
551 if !ov.os.is_empty()
552 && os.starts_with(&ov.os)
553 && let Some(fmts) = ov.formats.as_ref()
554 && let Some(first) = fmts.first()
555 {
556 return first.clone();
557 }
558 }
559 }
560 archive
561 .formats
562 .as_ref()
563 .and_then(|f| f.first())
564 .cloned()
565 .unwrap_or_else(|| global_default.to_string())
566}
567
568#[cfg(test)]
569mod tests {
570 use std::collections::{BTreeMap, HashMap};
571
572 use super::*;
573 use crate::config::{
574 ArchiveConfig, ArchivesConfig, BinstallConfig, BinstallOverride, BuildConfig, Config,
575 FormatOverride, GitHubConfig, ReleaseConfig,
576 };
577 use crate::context::{Context, ContextOptions};
578
579 fn make_ctx() -> Context {
580 let config = Config {
581 project_name: "myapp".to_string(),
582 ..Default::default()
583 };
584 let mut ctx = Context::new(config, ContextOptions::default());
585 ctx.set_env_source(crate::MapEnvSource::new());
588 ctx.template_vars_mut().set("Version", "1.0.0");
589 ctx.template_vars_mut().set("ProjectName", "myapp");
590 ctx
591 }
592
593 fn gen_meta(path: &str, cfg: &BinstallConfig, ctx: &mut Context, dry_run: bool) -> Result<()> {
599 let crate_cfg = CrateConfig {
600 name: "myapp".to_string(),
601 path: path.to_string(),
602 ..Default::default()
603 };
604 generate_binstall_metadata(&crate_cfg, cfg, &[], ctx, dry_run)
605 }
606
607 fn gen_with_crate(
610 path: &str,
611 mut crate_cfg: CrateConfig,
612 cfg: &BinstallConfig,
613 default_targets: &[String],
614 ctx: &mut Context,
615 ) -> Result<()> {
616 crate_cfg.path = path.to_string();
617 generate_binstall_metadata(&crate_cfg, cfg, default_targets, ctx, false)
618 }
619
620 #[test]
621 fn test_generate_binstall_metadata_inserts_section() {
622 let tmp = tempfile::tempdir().unwrap();
623 let cargo_toml = tmp.path().join("Cargo.toml");
624 std::fs::write(
625 &cargo_toml,
626 r#"[package]
627name = "myapp"
628version = "1.0.0"
629edition = "2024"
630"#,
631 )
632 .unwrap();
633
634 let binstall_cfg = BinstallConfig {
635 enabled: Some(true),
636 pkg_url: Some(
637 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{{ .Version }}-{ target }.tar.gz"
638 .to_string(),
639 ),
640 bin_dir: Some("{ bin }{ binary-ext }".to_string()),
641 pkg_fmt: Some("tgz".to_string()),
642 overrides: None,
643 };
644
645 let mut ctx = make_ctx();
646 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
647
648 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
649 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
650
651 let binstall = &doc["package"]["metadata"]["binstall"];
652 assert!(
653 binstall.as_table().is_some(),
654 "binstall section should exist as a table"
655 );
656 assert_eq!(binstall["pkg-fmt"].as_str().unwrap(), "tgz");
657 assert_eq!(
658 binstall["bin-dir"].as_str().unwrap(),
659 "{ bin }{ binary-ext }"
660 );
661 let pkg_url = binstall["pkg-url"].as_str().unwrap();
663 assert!(
664 pkg_url.contains("1.0.0"),
665 "pkg-url should have Version rendered, got: {pkg_url}"
666 );
667 }
668
669 #[test]
670 fn test_generate_binstall_metadata_dry_run() {
671 let tmp = tempfile::tempdir().unwrap();
672 let cargo_toml = tmp.path().join("Cargo.toml");
673 let original = r#"[package]
674name = "myapp"
675version = "1.0.0"
676edition = "2024"
677"#;
678 std::fs::write(&cargo_toml, original).unwrap();
679
680 let binstall_cfg = BinstallConfig {
681 enabled: Some(true),
682 pkg_url: Some("https://example.com".to_string()),
683 bin_dir: None,
684 pkg_fmt: None,
685 overrides: None,
686 };
687
688 let mut ctx = make_ctx();
689 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, true).unwrap();
690
691 let content = std::fs::read_to_string(&cargo_toml).unwrap();
693 assert_eq!(content, original);
694 }
695
696 #[test]
697 fn test_generate_binstall_metadata_missing_cargo_toml_errors() {
698 let tmp = tempfile::tempdir().unwrap();
699 let binstall_cfg = BinstallConfig {
700 enabled: Some(true),
701 pkg_url: None,
702 bin_dir: None,
703 pkg_fmt: None,
704 overrides: None,
705 };
706
707 let mut ctx = make_ctx();
708 let result = gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false);
709 assert!(result.is_err());
710 }
711
712 #[test]
713 fn test_generate_binstall_metadata_emits_per_target_overrides() {
714 let tmp = tempfile::tempdir().unwrap();
715 let cargo_toml = tmp.path().join("Cargo.toml");
716 std::fs::write(
717 &cargo_toml,
718 r#"[package]
719name = "cfgd"
720version = "1.0.0"
721edition = "2024"
722"#,
723 )
724 .unwrap();
725
726 let mut overrides = BTreeMap::new();
727 overrides.insert(
728 "x86_64-unknown-linux-gnu".to_string(),
729 BinstallOverride {
730 pkg_url: Some(
731 "https://github.com/myorg/cfgd/releases/download/v{{ .Version }}/cfgd-{{ .Version }}-linux-amd64.tar.gz"
732 .to_string(),
733 ),
734 pkg_fmt: Some("tgz".to_string()),
735 bin_dir: Some("{ bin }{ binary-ext }".to_string()),
736 },
737 );
738 overrides.insert(
739 "aarch64-apple-darwin".to_string(),
740 BinstallOverride {
741 pkg_url: Some(
742 "https://github.com/myorg/cfgd/releases/download/v{{ .Version }}/cfgd-{ version }-darwin-arm64.tar.gz"
743 .to_string(),
744 ),
745 pkg_fmt: Some("tgz".to_string()),
746 bin_dir: None,
747 },
748 );
749
750 let binstall_cfg = BinstallConfig {
751 enabled: Some(true),
752 pkg_url: None,
753 bin_dir: None,
754 pkg_fmt: None,
755 overrides: Some(overrides),
756 };
757
758 let mut ctx = make_ctx();
759 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
760
761 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
762 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
763
764 let overrides_item = &doc["package"]["metadata"]["binstall"]["overrides"];
765 assert!(
766 overrides_item.as_table().is_some(),
767 "binstall.overrides should exist as a table"
768 );
769
770 let linux = &overrides_item["x86_64-unknown-linux-gnu"];
772 assert!(
773 linux.as_table().is_some(),
774 "override sub-table should be a real table"
775 );
776 let linux_url = linux["pkg-url"].as_str().unwrap();
777 assert!(
778 linux_url.contains("cfgd-1.0.0-linux-amd64.tar.gz"),
779 "linux pkg-url should be Version-rendered with go-arch asset name, got: {linux_url}"
780 );
781 assert!(
782 !linux_url.contains("{{ .Version }}"),
783 "anodize token should be rendered, got: {linux_url}"
784 );
785 assert_eq!(linux["pkg-fmt"].as_str().unwrap(), "tgz");
786 assert_eq!(linux["bin-dir"].as_str().unwrap(), "{ bin }{ binary-ext }");
787
788 let darwin = &overrides_item["aarch64-apple-darwin"];
790 assert!(darwin.as_table().is_some());
791 let darwin_url = darwin["pkg-url"].as_str().unwrap();
792 assert!(
795 darwin_url.contains("/v1.0.0/cfgd-{ version }-darwin-arm64.tar.gz"),
796 "darwin pkg-url should render the anodize token but leave cargo-binstall's `{{ version }}` intact, got: {darwin_url}"
797 );
798
799 assert!(
801 updated.contains("[package.metadata.binstall.overrides.x86_64-unknown-linux-gnu]"),
802 "override should render as a [...] header, got:\n{updated}"
803 );
804 }
805
806 #[test]
807 fn test_generate_binstall_metadata_preserves_user_authored_keys() {
808 let tmp = tempfile::tempdir().unwrap();
809 let cargo_toml = tmp.path().join("Cargo.toml");
810 std::fs::write(
815 &cargo_toml,
816 r#"[package]
817name = "myapp"
818version = "1.0.0"
819edition = "2024"
820
821[package.metadata.binstall]
822disabled-strategies = ["quick-install", "compile"]
823pkg-url = "https://old.example.com/stale"
824
825[package.metadata.binstall.signing]
826algorithm = "minisign"
827pubkey = "RWQABCDEF1234567890"
828"#,
829 )
830 .unwrap();
831
832 let mut overrides = BTreeMap::new();
833 overrides.insert(
834 "x86_64-unknown-linux-gnu".to_string(),
835 BinstallOverride {
836 pkg_url: Some(
837 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-linux.tar.gz"
838 .to_string(),
839 ),
840 pkg_fmt: Some("tgz".to_string()),
841 bin_dir: None,
842 },
843 );
844
845 let binstall_cfg = BinstallConfig {
846 enabled: Some(true),
847 pkg_url: Some(
848 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{ target }.tar.gz"
849 .to_string(),
850 ),
851 bin_dir: None,
852 pkg_fmt: None,
853 overrides: Some(overrides),
854 };
855
856 let mut ctx = make_ctx();
857 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
858
859 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
860 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
861 let binstall = &doc["package"]["metadata"]["binstall"];
862
863 let strategies = binstall["disabled-strategies"].as_array().unwrap();
865 let strategy_vals: Vec<&str> = strategies.iter().filter_map(|v| v.as_str()).collect();
866 assert_eq!(
867 strategy_vals,
868 vec!["quick-install", "compile"],
869 "disabled-strategies must survive the merge verbatim"
870 );
871
872 let signing = &binstall["signing"];
873 assert!(
874 signing.as_table().is_some(),
875 "signing sub-table must survive the merge"
876 );
877 assert_eq!(signing["algorithm"].as_str().unwrap(), "minisign");
878 assert_eq!(signing["pubkey"].as_str().unwrap(), "RWQABCDEF1234567890");
879
880 let pkg_url = binstall["pkg-url"].as_str().unwrap();
882 assert!(
883 pkg_url.contains("/v1.0.0/myapp-{ target }.tar.gz"),
884 "pkg-url should be rewritten with the rendered Version, got: {pkg_url}"
885 );
886 assert!(
887 !pkg_url.contains("old.example.com"),
888 "stale anodize-owned pkg-url should be replaced, got: {pkg_url}"
889 );
890
891 let linux = &binstall["overrides"]["x86_64-unknown-linux-gnu"];
893 assert_eq!(linux["pkg-fmt"].as_str().unwrap(), "tgz");
894 assert!(
895 linux["pkg-url"]
896 .as_str()
897 .unwrap()
898 .contains("/v1.0.0/myapp-linux.tar.gz")
899 );
900 }
901
902 #[test]
903 fn test_generate_binstall_metadata_clears_unset_owned_key_keeps_unknown() {
904 let tmp = tempfile::tempdir().unwrap();
905 let cargo_toml = tmp.path().join("Cargo.toml");
906 std::fs::write(
909 &cargo_toml,
910 r#"[package]
911name = "myapp"
912version = "1.0.0"
913edition = "2024"
914
915[package.metadata.binstall]
916disabled-strategies = ["compile"]
917pkg-url = "https://old.example.com/stale"
918"#,
919 )
920 .unwrap();
921
922 let binstall_cfg = BinstallConfig {
923 enabled: Some(true),
924 pkg_url: None,
925 bin_dir: None,
926 pkg_fmt: None,
927 overrides: None,
928 };
929
930 let mut ctx = make_ctx();
931 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
932
933 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
934 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
935 let binstall = &doc["package"]["metadata"]["binstall"];
936
937 assert!(
938 binstall.get("pkg-url").is_none(),
939 "unset owned key should be cleared, got:\n{updated}"
940 );
941 let strategies = binstall["disabled-strategies"].as_array().unwrap();
942 assert_eq!(strategies.len(), 1, "unknown key must survive clearing");
943 assert_eq!(strategies.get(0).unwrap().as_str().unwrap(), "compile");
944 }
945
946 #[test]
947 fn test_generate_binstall_metadata_merges_inline_table_preserving_unknown() {
948 let tmp = tempfile::tempdir().unwrap();
949 let cargo_toml = tmp.path().join("Cargo.toml");
950 std::fs::write(
954 &cargo_toml,
955 r#"[package]
956name = "myapp"
957version = "1.0.0"
958edition = "2024"
959metadata.binstall = { pkg-url = "https://old.example.com/stale", disabled-strategies = ["compile"] }
960"#,
961 )
962 .unwrap();
963
964 let binstall_cfg = BinstallConfig {
965 enabled: Some(true),
966 pkg_url: Some(
967 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{ target }.tar.gz"
968 .to_string(),
969 ),
970 bin_dir: None,
971 pkg_fmt: None,
972 overrides: None,
973 };
974
975 let mut ctx = make_ctx();
976 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
977
978 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
979 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
980 let binstall = &doc["package"]["metadata"]["binstall"];
981
982 let pkg_url = binstall["pkg-url"].as_str().unwrap();
984 assert!(
985 pkg_url.contains("/v1.0.0/myapp-{ target }.tar.gz")
986 && !pkg_url.contains("old.example.com"),
987 "inline pkg-url should be rewritten, got: {pkg_url}"
988 );
989 let strategies = binstall["disabled-strategies"].as_array().unwrap();
991 assert_eq!(strategies.len(), 1);
992 assert_eq!(strategies.get(0).unwrap().as_str().unwrap(), "compile");
993 }
994
995 fn six_targets() -> Vec<String> {
1001 vec![
1002 "x86_64-unknown-linux-gnu".to_string(),
1003 "aarch64-unknown-linux-gnu".to_string(),
1004 "x86_64-apple-darwin".to_string(),
1005 "aarch64-apple-darwin".to_string(),
1006 "x86_64-pc-windows-msvc".to_string(),
1007 "aarch64-pc-windows-msvc".to_string(),
1008 ]
1009 }
1010
1011 fn anodize_like_crate() -> CrateConfig {
1015 let archive = ArchiveConfig {
1016 name_template: Some("{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}".to_string()),
1017 formats: Some(vec!["tar.gz".to_string()]),
1018 format_overrides: Some(vec![FormatOverride {
1019 os: "windows".to_string(),
1020 formats: Some(vec!["zip".to_string()]),
1021 }]),
1022 ..Default::default()
1023 };
1024 CrateConfig {
1025 name: "anodizer".to_string(),
1026 tag_template: Some("v{{ Version }}".to_string()),
1027 archives: ArchivesConfig::Configs(vec![archive]),
1028 release: Some(ReleaseConfig {
1029 github: Some(GitHubConfig {
1030 owner: "tj-smith47".to_string(),
1031 name: "anodizer".to_string(),
1032 token: None,
1033 }),
1034 ..Default::default()
1035 }),
1036 ..Default::default()
1037 }
1038 }
1039
1040 fn override_asset(binstall: &toml_edit::Item, triple: &str, version: &str) -> String {
1043 let url = binstall["overrides"][triple]["pkg-url"].as_str().unwrap();
1044 let leaf = url.rsplit('/').next().unwrap();
1045 leaf.replace("{ version }", version)
1046 }
1047
1048 #[test]
1049 fn auto_derive_covers_all_six_triples_matching_archive_names() {
1050 let tmp = tempfile::tempdir().unwrap();
1051 let cargo_toml = tmp.path().join("Cargo.toml");
1052 std::fs::write(
1053 &cargo_toml,
1054 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1055 )
1056 .unwrap();
1057 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1061 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1062
1063 let cfg = BinstallConfig {
1065 enabled: Some(true),
1066 ..Default::default()
1067 };
1068 let mut ctx = make_ctx();
1069 ctx.template_vars_mut().set("ProjectName", "anodizer");
1070 let targets = six_targets();
1071 gen_with_crate(
1072 tmp.path().to_str().unwrap(),
1073 anodize_like_crate(),
1074 &cfg,
1075 &targets,
1076 &mut ctx,
1077 )
1078 .unwrap();
1079
1080 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1081 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1082 let binstall = &doc["package"]["metadata"]["binstall"];
1083
1084 let mut check_ctx = make_ctx();
1089 check_ctx.template_vars_mut().set("ProjectName", "anodizer");
1090 let name_tmpl = "{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}";
1091 for triple in &targets {
1092 let fmt = if triple.contains("windows") {
1093 "zip"
1094 } else {
1095 "tar.gz"
1096 };
1097 let expected = crate::archive_name::render_archive_asset_name(
1098 &mut check_ctx,
1099 name_tmpl,
1100 triple,
1101 fmt,
1102 )
1103 .unwrap();
1104 let actual = override_asset(binstall, triple, "1.0.0");
1105 assert_eq!(
1106 actual, expected,
1107 "override asset for {triple} must equal the archive stage's name"
1108 );
1109
1110 let pkg_fmt = binstall["overrides"][triple]["pkg-fmt"].as_str().unwrap();
1112 assert_eq!(pkg_fmt, if fmt == "zip" { "zip" } else { "tgz" });
1113
1114 let url = binstall["overrides"][triple]["pkg-url"].as_str().unwrap();
1117 assert!(
1118 url.starts_with(
1119 "https://github.com/tj-smith47/anodizer/releases/download/v{ version }/"
1120 ),
1121 "url should target the GitHub release download path with the binstall version token, got: {url}"
1122 );
1123 assert!(
1124 url.contains("{ version }"),
1125 "url must carry cargo-binstall's version token, got: {url}"
1126 );
1127 }
1128
1129 assert_eq!(
1131 override_asset(binstall, "x86_64-unknown-linux-gnu", "9.9.9"),
1132 "anodizer-9.9.9-linux-amd64.tar.gz"
1133 );
1134 assert_eq!(
1135 override_asset(binstall, "aarch64-pc-windows-msvc", "9.9.9"),
1136 "anodizer-9.9.9-windows-arm64.zip"
1137 );
1138
1139 assert!(
1141 binstall.get("pkg-url").is_none(),
1142 "auto-derivation must not write a top-level pkg-url, got:\n{updated}"
1143 );
1144 }
1145
1146 #[test]
1147 fn auto_derive_matches_real_v091_release_assets() {
1148 let tmp = tempfile::tempdir().unwrap();
1156 let cargo_toml = tmp.path().join("Cargo.toml");
1157 std::fs::write(
1158 &cargo_toml,
1159 "[package]\nname = \"anodizer\"\nversion = \"0.9.1\"\nedition = \"2024\"\n",
1160 )
1161 .unwrap();
1162 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1165 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1166
1167 let cfg = BinstallConfig {
1168 enabled: Some(true),
1169 ..Default::default()
1170 };
1171 let mut ctx = make_ctx();
1172 ctx.template_vars_mut().set("ProjectName", "anodizer");
1173 gen_with_crate(
1174 tmp.path().to_str().unwrap(),
1175 anodize_like_crate(),
1176 &cfg,
1177 &six_targets(),
1178 &mut ctx,
1179 )
1180 .unwrap();
1181
1182 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1183 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1184 let binstall = &doc["package"]["metadata"]["binstall"];
1185
1186 let expected: &[(&str, &str, &str)] = &[
1188 (
1189 "x86_64-unknown-linux-gnu",
1190 "anodizer-0.9.1-linux-amd64.tar.gz",
1191 "tgz",
1192 ),
1193 (
1194 "aarch64-unknown-linux-gnu",
1195 "anodizer-0.9.1-linux-arm64.tar.gz",
1196 "tgz",
1197 ),
1198 (
1199 "x86_64-apple-darwin",
1200 "anodizer-0.9.1-darwin-amd64.tar.gz",
1201 "tgz",
1202 ),
1203 (
1204 "aarch64-apple-darwin",
1205 "anodizer-0.9.1-darwin-arm64.tar.gz",
1206 "tgz",
1207 ),
1208 (
1209 "x86_64-pc-windows-msvc",
1210 "anodizer-0.9.1-windows-amd64.zip",
1211 "zip",
1212 ),
1213 (
1214 "aarch64-pc-windows-msvc",
1215 "anodizer-0.9.1-windows-arm64.zip",
1216 "zip",
1217 ),
1218 ];
1219
1220 for (triple, asset, fmt) in expected {
1221 let url = binstall["overrides"][*triple]["pkg-url"].as_str().unwrap();
1222 let resolved = url.replace("{ version }", "0.9.1");
1225 let want =
1226 format!("https://github.com/tj-smith47/anodizer/releases/download/v0.9.1/{asset}");
1227 assert_eq!(
1228 resolved, want,
1229 "override for {triple} must resolve to the real v0.9.1 asset URL"
1230 );
1231 assert_eq!(
1232 binstall["overrides"][*triple]["pkg-fmt"].as_str().unwrap(),
1233 *fmt,
1234 "pkg-fmt for {triple} must match the real v0.9.1 asset format"
1235 );
1236 }
1237 }
1238
1239 #[test]
1240 fn user_pkg_url_suppresses_auto_derivation() {
1241 let tmp = tempfile::tempdir().unwrap();
1242 let cargo_toml = tmp.path().join("Cargo.toml");
1243 std::fs::write(
1244 &cargo_toml,
1245 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1246 )
1247 .unwrap();
1248
1249 let cfg = BinstallConfig {
1250 enabled: Some(true),
1251 pkg_url: Some("https://example.com/custom/anodizer-{ target }.tar.gz".to_string()),
1252 ..Default::default()
1253 };
1254 let mut ctx = make_ctx();
1255 ctx.template_vars_mut().set("ProjectName", "anodizer");
1256 gen_with_crate(
1257 tmp.path().to_str().unwrap(),
1258 anodize_like_crate(),
1259 &cfg,
1260 &six_targets(),
1261 &mut ctx,
1262 )
1263 .unwrap();
1264
1265 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1266 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1267 let binstall = &doc["package"]["metadata"]["binstall"];
1268
1269 assert_eq!(
1271 binstall["pkg-url"].as_str().unwrap(),
1272 "https://example.com/custom/anodizer-{ target }.tar.gz"
1273 );
1274 assert!(
1275 binstall.get("overrides").is_none(),
1276 "a user pkg_url must suppress auto-derived overrides, got:\n{updated}"
1277 );
1278 }
1279
1280 #[test]
1281 fn user_override_suppresses_auto_derivation() {
1282 let tmp = tempfile::tempdir().unwrap();
1283 let cargo_toml = tmp.path().join("Cargo.toml");
1284 std::fs::write(
1285 &cargo_toml,
1286 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1287 )
1288 .unwrap();
1289
1290 let mut user_overrides = BTreeMap::new();
1291 user_overrides.insert(
1292 "x86_64-unknown-linux-gnu".to_string(),
1293 BinstallOverride {
1294 pkg_url: Some("https://example.com/manual-linux.tar.gz".to_string()),
1295 pkg_fmt: Some("tgz".to_string()),
1296 bin_dir: None,
1297 },
1298 );
1299 let cfg = BinstallConfig {
1300 enabled: Some(true),
1301 overrides: Some(user_overrides),
1302 ..Default::default()
1303 };
1304 let mut ctx = make_ctx();
1305 ctx.template_vars_mut().set("ProjectName", "anodizer");
1306 gen_with_crate(
1307 tmp.path().to_str().unwrap(),
1308 anodize_like_crate(),
1309 &cfg,
1310 &six_targets(),
1311 &mut ctx,
1312 )
1313 .unwrap();
1314
1315 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1316 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1317 let overrides = &doc["package"]["metadata"]["binstall"]["overrides"];
1318
1319 assert_eq!(
1322 overrides["x86_64-unknown-linux-gnu"]["pkg-url"]
1323 .as_str()
1324 .unwrap(),
1325 "https://example.com/manual-linux.tar.gz"
1326 );
1327 assert!(
1328 overrides.get("aarch64-apple-darwin").is_none(),
1329 "supplying one override must suppress auto-derivation of the rest, got:\n{updated}"
1330 );
1331 }
1332
1333 #[test]
1334 fn auto_derive_uses_default_template_when_unset() {
1335 let tmp = tempfile::tempdir().unwrap();
1338 let cargo_toml = tmp.path().join("Cargo.toml");
1339 std::fs::write(
1340 &cargo_toml,
1341 "[package]\nname = \"myapp\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1342 )
1343 .unwrap();
1344
1345 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1348 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1349
1350 let mut crate_cfg = anodize_like_crate();
1351 crate_cfg.name = "myapp".to_string();
1352 crate_cfg.archives = ArchivesConfig::Configs(vec![ArchiveConfig {
1354 formats: Some(vec!["tar.gz".to_string()]),
1355 ..Default::default()
1356 }]);
1357
1358 let cfg = BinstallConfig {
1359 enabled: Some(true),
1360 ..Default::default()
1361 };
1362 let mut ctx = make_ctx();
1363 gen_with_crate(
1364 tmp.path().to_str().unwrap(),
1365 crate_cfg,
1366 &cfg,
1367 &["x86_64-unknown-linux-gnu".to_string()],
1368 &mut ctx,
1369 )
1370 .unwrap();
1371
1372 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1373 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1374 let binstall = &doc["package"]["metadata"]["binstall"];
1375 assert_eq!(
1377 override_asset(binstall, "x86_64-unknown-linux-gnu", "1.0.0"),
1378 "myapp_1.0.0_linux_amd64.tar.gz"
1379 );
1380 }
1381
1382 #[test]
1383 fn auto_derive_noops_without_release_repo() {
1384 let tmp = tempfile::tempdir().unwrap();
1386 let cargo_toml = tmp.path().join("Cargo.toml");
1387 std::fs::write(
1388 &cargo_toml,
1389 "[package]\nname = \"myapp\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1390 )
1391 .unwrap();
1392
1393 let mut crate_cfg = anodize_like_crate();
1394 crate_cfg.name = "myapp".to_string();
1395 crate_cfg.release = None;
1396
1397 let cfg = BinstallConfig {
1398 enabled: Some(true),
1399 ..Default::default()
1400 };
1401 let mut ctx = make_ctx();
1402 gen_with_crate(
1403 tmp.path().to_str().unwrap(),
1404 crate_cfg,
1405 &cfg,
1406 &six_targets(),
1407 &mut ctx,
1408 )
1409 .unwrap();
1410
1411 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1412 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1413 let binstall = &doc["package"]["metadata"]["binstall"];
1414 assert!(
1415 binstall.get("overrides").is_none(),
1416 "no release repo should mean no auto-derived overrides, got:\n{updated}"
1417 );
1418 }
1419
1420 #[test]
1423 fn normalize_inserts_empty_table_when_binstall_absent() {
1424 let mut metadata = toml_edit::Table::new();
1425 normalize_binstall_to_table(&mut metadata).unwrap();
1426 let binstall = metadata.get("binstall").expect("binstall inserted");
1427 assert!(binstall.is_table(), "must be a header table");
1428 assert!(
1429 binstall.as_table().unwrap().is_empty(),
1430 "freshly-inserted binstall table must be empty"
1431 );
1432 }
1433
1434 #[test]
1435 fn normalize_leaves_existing_header_table_untouched() {
1436 let mut metadata = toml_edit::Table::new();
1437 let mut existing = toml_edit::Table::new();
1438 existing.insert("pkg-url", toml_edit::value("https://example/x"));
1439 metadata.insert("binstall", toml_edit::Item::Table(existing));
1440 normalize_binstall_to_table(&mut metadata).unwrap();
1441 assert_eq!(
1442 metadata["binstall"]["pkg-url"].as_str(),
1443 Some("https://example/x"),
1444 "an existing header table must be preserved verbatim"
1445 );
1446 }
1447
1448 #[test]
1449 fn normalize_converts_inline_table_preserving_keys() {
1450 let doc: toml_edit::DocumentMut = r#"[package.metadata]
1453binstall = { pkg-url = "https://example/x", custom = "keep" }
1454"#
1455 .parse()
1456 .unwrap();
1457 let mut metadata = doc["package"]["metadata"].as_table().unwrap().clone();
1458 normalize_binstall_to_table(&mut metadata).unwrap();
1459 let binstall = metadata["binstall"].as_table().expect("now a header table");
1460 assert_eq!(binstall["pkg-url"].as_str(), Some("https://example/x"));
1461 assert_eq!(
1462 binstall["custom"].as_str(),
1463 Some("keep"),
1464 "user-authored inline keys must survive the conversion"
1465 );
1466 }
1467
1468 #[test]
1469 fn normalize_errors_on_non_table_binstall() {
1470 let mut metadata = toml_edit::Table::new();
1472 metadata.insert("binstall", toml_edit::value("oops"));
1473 assert!(
1474 normalize_binstall_to_table(&mut metadata).is_err(),
1475 "a malformed scalar binstall must error, not silently pass"
1476 );
1477 }
1478
1479 #[test]
1482 fn set_or_remove_str_sets_and_removes() {
1483 let mut table = toml_edit::Table::new();
1484 set_or_remove_str(&mut table, "k", Some("v"));
1485 assert_eq!(table["k"].as_str(), Some("v"));
1486 table.insert("sibling", toml_edit::value("keep"));
1488 set_or_remove_str(&mut table, "k", None);
1489 assert!(table.get("k").is_none(), "None must remove the key");
1490 assert_eq!(
1491 table["sibling"].as_str(),
1492 Some("keep"),
1493 "removing one key must not disturb siblings"
1494 );
1495 }
1496
1497 #[test]
1500 fn render_overrides_none_when_unset_or_empty() {
1501 let ctx = make_ctx();
1502 let cfg = BinstallConfig {
1503 overrides: None,
1504 ..Default::default()
1505 };
1506 assert!(
1507 render_overrides(&cfg, &ctx).unwrap().is_none(),
1508 "no overrides configured → None"
1509 );
1510 let cfg_empty = BinstallConfig {
1511 overrides: Some(BTreeMap::new()),
1512 ..Default::default()
1513 };
1514 assert!(
1515 render_overrides(&cfg_empty, &ctx).unwrap().is_none(),
1516 "an empty overrides map → None"
1517 );
1518 }
1519
1520 #[test]
1521 fn render_overrides_map_empty_is_none() {
1522 let ctx = make_ctx();
1523 let empty: BTreeMap<String, BinstallOverride> = BTreeMap::new();
1524 assert!(
1525 render_overrides_map(&empty, &ctx).unwrap().is_none(),
1526 "an empty map renders to None, never an empty table"
1527 );
1528 }
1529
1530 fn tuned_crate(env: HashMap<String, HashMap<String, String>>) -> CrateConfig {
1535 CrateConfig {
1536 name: "myapp".to_string(),
1537 builds: Some(vec![BuildConfig {
1538 binary: Some("myapp".to_string()),
1539 targets: Some(vec![
1540 "x86_64-unknown-linux-gnu".to_string(),
1541 "aarch64-unknown-linux-gnu".to_string(),
1542 "x86_64-apple-darwin".to_string(),
1543 ]),
1544 env: Some(env),
1545 ..Default::default()
1546 }]),
1547 archives: ArchivesConfig::Configs(vec![ArchiveConfig {
1548 formats: Some(vec!["tar.gz".to_string()]),
1549 ..Default::default()
1550 }]),
1551 ..Default::default()
1552 }
1553 }
1554
1555 #[test]
1562 fn asset_names_carry_config_declared_amd64_variant() {
1563 let mut env = HashMap::new();
1564 env.insert(
1565 "x86_64-unknown-linux-gnu".to_string(),
1566 HashMap::from([(
1567 "RUSTFLAGS".to_string(),
1568 "-Ctarget-cpu=x86-64-v3".to_string(),
1569 )]),
1570 );
1571 let crate_cfg = tuned_crate(env);
1572 let mut ctx = make_ctx();
1573 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1574 .unwrap()
1575 .expect("binstallable archive with targets derives names");
1576 assert_eq!(
1577 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64v3.tar.gz",
1578 "tuned target must carry the micro-arch suffix"
1579 );
1580 assert_eq!(
1581 assets["aarch64-unknown-linux-gnu"].asset_name,
1582 "myapp_1.0.0_linux_arm64.tar.gz"
1583 );
1584 assert_eq!(
1585 assets["x86_64-apple-darwin"].asset_name, "myapp_1.0.0_darwin_amd64.tar.gz",
1586 "untuned amd64 target stays on the baseline name"
1587 );
1588 }
1589
1590 #[test]
1597 fn os_templated_tuning_env_derives_per_target_levels_across_the_loop() {
1598 let mut env = HashMap::new();
1599 env.insert(
1600 "x86_64-*".to_string(),
1601 HashMap::from([(
1602 "RUSTFLAGS".to_string(),
1603 "{% if Os == \"darwin\" %}-Ctarget-cpu=x86-64-v2\
1604 {% else %}-Ctarget-cpu=x86-64-v3{% endif %}"
1605 .to_string(),
1606 )]),
1607 );
1608 let crate_cfg = tuned_crate(env);
1609 let mut ctx = make_ctx();
1610 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1611 .unwrap()
1612 .expect("binstallable archive with targets derives names");
1613 assert_eq!(
1614 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64v3.tar.gz",
1615 "first target must render the env with ITS OWN Os seeded"
1616 );
1617 assert_eq!(
1618 assets["x86_64-apple-darwin"].asset_name, "myapp_1.0.0_darwin_amd64v2.tar.gz",
1619 "second target must not render with the first target's stale Os"
1620 );
1621 assert_eq!(
1622 assets["aarch64-unknown-linux-gnu"].asset_name,
1623 "myapp_1.0.0_linux_arm64.tar.gz"
1624 );
1625 }
1626
1627 #[test]
1632 fn asset_names_fall_back_to_baseline_when_env_defeats_config_render() {
1633 let mut env = HashMap::new();
1634 env.insert(
1635 "x86_64-unknown-linux-gnu".to_string(),
1636 HashMap::from([(
1637 "RUSTFLAGS".to_string(),
1638 "{{ BuildTimeOnlyVar }}".to_string(),
1639 )]),
1640 );
1641 let crate_cfg = tuned_crate(env);
1642 let mut ctx = make_ctx();
1643 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1644 .unwrap()
1645 .expect("derivation still resolves");
1646 assert_eq!(
1647 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64.tar.gz",
1648 "unrenderable env derives no variant (baseline name)"
1649 );
1650 }
1651
1652 #[test]
1656 fn variant_derivation_does_not_leak_build_env_into_ctx() {
1657 let mut env = HashMap::new();
1658 env.insert(
1659 "x86_64-unknown-linux-gnu".to_string(),
1660 HashMap::from([
1661 (
1662 "RUSTFLAGS".to_string(),
1663 "-Ctarget-cpu=x86-64-v2".to_string(),
1664 ),
1665 ("MY_BUILD_KEY".to_string(), "leaky".to_string()),
1666 ]),
1667 );
1668 let crate_cfg = tuned_crate(env);
1669 let mut ctx = make_ctx();
1670 ctx.template_vars_mut().set_env("RUSTFLAGS", "prior-value");
1671 let before_keys = ctx.template_vars().all_env().len();
1672 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1673 .unwrap()
1674 .unwrap();
1675 assert_eq!(
1676 assets["x86_64-unknown-linux-gnu"].asset_name,
1677 "myapp_1.0.0_linux_amd64v2.tar.gz"
1678 );
1679 assert_eq!(
1680 ctx.template_vars().all_env().get("RUSTFLAGS").unwrap(),
1681 "prior-value",
1682 "the caller's prior env value must be restored after the cascade"
1683 );
1684 assert!(
1685 !ctx.template_vars().all_env().contains_key("MY_BUILD_KEY"),
1686 "cascade-only keys must be removed after derivation"
1687 );
1688 assert_eq!(ctx.template_vars().all_env().len(), before_keys);
1689 }
1690}