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 let dir = crate_cfg
155 .path
156 .trim_start_matches("./")
157 .trim_end_matches('/');
158 let rel = if dir.is_empty() || dir == "." {
159 "Cargo.toml".to_string()
160 } else {
161 format!("{dir}/Cargo.toml")
162 };
163 ctx.record_tree_mutation(rel);
164
165 log.status(&format!(
166 "updated [package.metadata.binstall] in {}",
167 cargo_toml_path.display()
168 ));
169
170 Ok(())
171}
172
173fn normalize_binstall_to_table(metadata: &mut toml_edit::Table) -> Result<()> {
185 match metadata.get("binstall") {
186 None => {
187 metadata.insert("binstall", toml_edit::Item::Table(toml_edit::Table::new()));
188 Ok(())
189 }
190 Some(item) if item.is_table() => Ok(()),
191 Some(item) => {
192 let inline = item.as_inline_table().with_context(|| {
193 "[package.metadata.binstall] is neither a table nor an inline table".to_string()
194 })?;
195 let mut table = toml_edit::Table::new();
198 for (k, v) in inline.iter() {
199 table.insert(k, toml_edit::Item::Value(v.clone()));
200 }
201 metadata.insert("binstall", toml_edit::Item::Table(table));
202 Ok(())
203 }
204 }
205}
206
207fn set_or_remove_str(table: &mut toml_edit::Table, key: &str, value: Option<&str>) {
211 match value {
212 Some(v) => {
213 table.insert(key, toml_edit::value(v));
214 }
215 None => {
216 table.remove(key);
217 }
218 }
219}
220
221fn render_overrides(config: &BinstallConfig, ctx: &Context) -> Result<Option<toml_edit::Table>> {
226 let Some(ref overrides) = config.overrides else {
227 return Ok(None);
228 };
229 if overrides.is_empty() {
230 return Ok(None);
231 }
232 render_overrides_map(overrides, ctx)
233}
234
235fn render_overrides_map(
241 overrides: &BTreeMap<String, BinstallOverride>,
242 ctx: &Context,
243) -> Result<Option<toml_edit::Table>> {
244 if overrides.is_empty() {
245 return Ok(None);
246 }
247 let mut overrides_table = toml_edit::Table::new();
248 overrides_table.set_implicit(true);
251 for (triple, ovr) in overrides {
253 let mut entry = toml_edit::Table::new();
254 if let Some(ref pkg_url) = ovr.pkg_url {
255 let rendered = ctx.render_template(pkg_url).with_context(|| {
256 format!("failed to render binstall overrides.{triple} pkg_url template: {pkg_url}")
257 })?;
258 entry.insert("pkg-url", toml_edit::value(rendered));
259 }
260 if let Some(ref pkg_fmt) = ovr.pkg_fmt {
261 entry.insert("pkg-fmt", toml_edit::value(pkg_fmt.as_str()));
262 }
263 if let Some(ref bin_dir) = ovr.bin_dir {
264 entry.insert("bin-dir", toml_edit::value(bin_dir.as_str()));
265 }
266 overrides_table.insert(triple, toml_edit::Item::Table(entry));
267 }
268 Ok(Some(overrides_table))
269}
270
271fn derive_overrides(
290 crate_cfg: &CrateConfig,
291 default_targets: &[String],
292 ctx: &mut Context,
293) -> Result<Option<BTreeMap<String, BinstallOverride>>> {
294 let Some((owner, repo, download_base)) = release_repo(crate_cfg, ctx) else {
295 return Ok(None);
296 };
297
298 let prior = stamp_sentinel_version(ctx);
304 let assets = crate_archive_asset_names(crate_cfg, default_targets, ctx);
305 restore_version(ctx, prior);
306 let Some(assets) = assets? else {
307 return Ok(None);
308 };
309
310 let tag_with_token = render_tag_with_version_token(crate_cfg, ctx)?;
314
315 let mut map: BTreeMap<String, BinstallOverride> = BTreeMap::new();
316 for (target, asset) in &assets {
317 let Some(pkg_fmt) = binstall_pkg_fmt(&asset.format) else {
318 continue;
322 };
323 let asset_with_token = asset.asset_name.replace(VERSION_SENTINEL, "{ version }");
324 let pkg_url = format!(
325 "{download_base}/{owner}/{repo}/releases/download/{tag_with_token}/{asset_with_token}"
326 );
327 map.insert(
328 target.clone(),
329 BinstallOverride {
330 pkg_url: Some(pkg_url),
331 pkg_fmt: Some(pkg_fmt.to_string()),
332 bin_dir: None,
333 },
334 );
335 }
336
337 if map.is_empty() {
338 return Ok(None);
339 }
340 Ok(Some(map))
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct ArchiveAssetName {
347 pub format: String,
349 pub asset_name: String,
352}
353
354pub fn crate_archive_asset_names(
370 crate_cfg: &CrateConfig,
371 default_targets: &[String],
372 ctx: &mut Context,
373) -> Result<Option<BTreeMap<String, ArchiveAssetName>>> {
374 let Some(archive) = binstallable_archive(crate_cfg) else {
375 return Ok(None);
376 };
377 let targets = derive_target_list(crate_cfg, default_targets);
378 if targets.is_empty() {
379 return Ok(None);
380 }
381
382 let name_template = archive
386 .name_template
387 .clone()
388 .unwrap_or_else(|| default_archive_name_template(ctx));
389 let global_default_format = global_default_archive_format(ctx);
390
391 let mut map: BTreeMap<String, ArchiveAssetName> = BTreeMap::new();
392 for target in &targets {
393 let format = archive_format_for_target(&archive, target, &global_default_format);
394 let amd64_variant =
399 crate::build_env::config_time_amd64_variant(crate_cfg, target, default_targets, ctx)?;
400 let asset_name = render_archive_asset_name_with_variant(
401 ctx,
402 &name_template,
403 target,
404 &format,
405 amd64_variant.as_deref(),
406 )?;
407 map.insert(target.clone(), ArchiveAssetName { format, asset_name });
408 }
409 Ok(Some(map))
410}
411
412fn release_repo(crate_cfg: &CrateConfig, ctx: &Context) -> Option<(String, String, String)> {
417 let release = crate_cfg.release.as_ref()?;
418 let (repo_cfg, base) = if let Some(gh) = release.github.as_ref() {
422 (gh, "https://github.com")
423 } else if let Some(gl) = release.gitlab.as_ref() {
424 (gl, "https://gitlab.com")
425 } else {
426 (release.gitea.as_ref()?, "https://gitea.com")
427 };
428 let owner = ctx.render_template(&repo_cfg.owner).ok()?;
429 let repo = ctx.render_template(&repo_cfg.name).ok()?;
430 if owner.is_empty() || repo.is_empty() {
431 return None;
432 }
433 Some((owner, repo, base.to_string()))
434}
435
436pub(crate) fn binstallable_archive(crate_cfg: &CrateConfig) -> Option<ArchiveConfig> {
441 let ArchivesConfig::Configs(configs) = &crate_cfg.archives else {
442 return None;
443 };
444 configs
445 .iter()
446 .find(|a| {
447 a.formats
448 .as_ref()
449 .and_then(|f| f.first())
450 .map(|fmt| binstall_pkg_fmt(fmt).is_some())
451 .unwrap_or(true)
452 })
453 .cloned()
454 .or_else(|| configs.first().cloned())
455}
456
457fn derive_target_list(crate_cfg: &CrateConfig, default_targets: &[String]) -> Vec<String> {
466 crate::build_plan::crate_target_list(crate_cfg, default_targets)
467}
468
469fn render_tag_with_version_token(crate_cfg: &CrateConfig, ctx: &mut Context) -> Result<String> {
473 let tag_template = crate_cfg
476 .release
477 .as_ref()
478 .and_then(|r| r.tag.clone())
479 .filter(|t| !t.is_empty())
480 .unwrap_or_else(|| crate_cfg.resolved_tag_template().to_string());
481
482 let prior = stamp_sentinel_version(ctx);
483 let rendered = ctx.render_template(&tag_template);
484 restore_version(ctx, prior);
485 let rendered = rendered
486 .with_context(|| format!("failed to render binstall tag template: {tag_template}"))?;
487 Ok(rendered.replace(VERSION_SENTINEL, "{ version }"))
488}
489
490fn stamp_sentinel_version(ctx: &mut Context) -> Vec<(&'static str, Option<String>)> {
495 let prior: Vec<(&'static str, Option<String>)> = ["Version", "RawVersion", "Tag"]
496 .iter()
497 .map(|k| (*k, ctx.template_vars().get(k).cloned()))
498 .collect();
499
500 let vars = ctx.template_vars_mut();
501 vars.set("Version", VERSION_SENTINEL);
502 vars.set("RawVersion", VERSION_SENTINEL);
503 vars.set("Tag", &format!("v{VERSION_SENTINEL}"));
506 prior
507}
508
509fn restore_version(ctx: &mut Context, prior: Vec<(&'static str, Option<String>)>) {
511 let vars = ctx.template_vars_mut();
512 for (key, value) in prior {
513 match value {
514 Some(v) => vars.set(key, &v),
515 None => {
516 vars.unset(key);
517 }
518 }
519 }
520}
521
522fn default_archive_name_template(ctx: &Context) -> String {
529 let producing = crate::archive_selection::archive_producing_crates(
530 &ctx.config,
531 &ctx.artifacts,
532 &ctx.options.selected_crates,
533 );
534 if producing.len() > 1 {
535 crate::archive_name::DEFAULT_NAME_TEMPLATE_MULTI_CRATE.to_string()
536 } else {
537 crate::archive_name::DEFAULT_NAME_TEMPLATE.to_string()
538 }
539}
540
541fn global_default_archive_format(ctx: &Context) -> String {
544 ctx.config
545 .defaults
546 .as_ref()
547 .and_then(|d| d.archives.as_ref())
548 .and_then(|a| a.formats.as_ref())
549 .and_then(|f| f.first())
550 .cloned()
551 .unwrap_or_else(|| "tar.gz".to_string())
552}
553
554fn archive_format_for_target(
558 archive: &ArchiveConfig,
559 target: &str,
560 global_default: &str,
561) -> String {
562 let (os, _arch) = map_target(target);
563 if let Some(overrides) = archive.format_overrides.as_ref() {
564 for ov in overrides {
565 if !ov.os.is_empty()
566 && os.starts_with(&ov.os)
567 && let Some(fmts) = ov.formats.as_ref()
568 && let Some(first) = fmts.first()
569 {
570 return first.clone();
571 }
572 }
573 }
574 archive
575 .formats
576 .as_ref()
577 .and_then(|f| f.first())
578 .cloned()
579 .unwrap_or_else(|| global_default.to_string())
580}
581
582#[cfg(test)]
583mod tests {
584 use std::collections::{BTreeMap, HashMap};
585
586 use super::*;
587 use crate::config::{
588 ArchiveConfig, ArchivesConfig, BinstallConfig, BinstallOverride, BuildConfig, Config,
589 FormatOverride, GitHubConfig, ReleaseConfig,
590 };
591 use crate::context::{Context, ContextOptions};
592
593 fn make_ctx() -> Context {
594 let config = Config {
595 project_name: "myapp".to_string(),
596 ..Default::default()
597 };
598 let mut ctx = Context::new(config, ContextOptions::default());
599 ctx.set_env_source(crate::MapEnvSource::new());
602 ctx.template_vars_mut().set("Version", "1.0.0");
603 ctx.template_vars_mut().set("ProjectName", "myapp");
604 ctx
605 }
606
607 fn gen_meta(path: &str, cfg: &BinstallConfig, ctx: &mut Context, dry_run: bool) -> Result<()> {
613 let crate_cfg = CrateConfig {
614 name: "myapp".to_string(),
615 path: path.to_string(),
616 ..Default::default()
617 };
618 generate_binstall_metadata(&crate_cfg, cfg, &[], ctx, dry_run)
619 }
620
621 fn gen_with_crate(
624 path: &str,
625 mut crate_cfg: CrateConfig,
626 cfg: &BinstallConfig,
627 default_targets: &[String],
628 ctx: &mut Context,
629 ) -> Result<()> {
630 crate_cfg.path = path.to_string();
631 generate_binstall_metadata(&crate_cfg, cfg, default_targets, ctx, false)
632 }
633
634 #[test]
635 fn write_records_tree_mutation_for_clean_tree_guard() {
636 let tmp = tempfile::tempdir().unwrap();
637 std::fs::write(
638 tmp.path().join("Cargo.toml"),
639 "[package]\nname = \"myapp\"\nversion = \"1.0.0\"\n",
640 )
641 .unwrap();
642 let binstall_cfg = BinstallConfig {
643 enabled: Some(true),
644 pkg_url: Some("https://example.com/{ version }.tar.gz".to_string()),
645 bin_dir: None,
646 pkg_fmt: None,
647 overrides: None,
648 };
649 let mut ctx = make_ctx();
650 let path = tmp.path().to_str().unwrap();
651 gen_meta(path, &binstall_cfg, &mut ctx, false).unwrap();
652 assert!(
653 ctx.tree_mutations()
654 .contains(&format!("{}/Cargo.toml", path.trim_end_matches('/'))),
655 "write must be registered as expected residue: {:?}",
656 ctx.tree_mutations()
657 );
658
659 let mut dry_ctx = make_ctx();
661 gen_meta(path, &binstall_cfg, &mut dry_ctx, true).unwrap();
662 assert!(dry_ctx.tree_mutations().is_empty());
663 }
664
665 #[test]
666 fn test_generate_binstall_metadata_inserts_section() {
667 let tmp = tempfile::tempdir().unwrap();
668 let cargo_toml = tmp.path().join("Cargo.toml");
669 std::fs::write(
670 &cargo_toml,
671 r#"[package]
672name = "myapp"
673version = "1.0.0"
674edition = "2024"
675"#,
676 )
677 .unwrap();
678
679 let binstall_cfg = BinstallConfig {
680 enabled: Some(true),
681 pkg_url: Some(
682 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{{ .Version }}-{ target }.tar.gz"
683 .to_string(),
684 ),
685 bin_dir: Some("{ bin }{ binary-ext }".to_string()),
686 pkg_fmt: Some("tgz".to_string()),
687 overrides: None,
688 };
689
690 let mut ctx = make_ctx();
691 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
692
693 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
694 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
695
696 let binstall = &doc["package"]["metadata"]["binstall"];
697 assert!(
698 binstall.as_table().is_some(),
699 "binstall section should exist as a table"
700 );
701 assert_eq!(binstall["pkg-fmt"].as_str().unwrap(), "tgz");
702 assert_eq!(
703 binstall["bin-dir"].as_str().unwrap(),
704 "{ bin }{ binary-ext }"
705 );
706 let pkg_url = binstall["pkg-url"].as_str().unwrap();
708 assert!(
709 pkg_url.contains("1.0.0"),
710 "pkg-url should have Version rendered, got: {pkg_url}"
711 );
712 }
713
714 #[test]
715 fn test_generate_binstall_metadata_dry_run() {
716 let tmp = tempfile::tempdir().unwrap();
717 let cargo_toml = tmp.path().join("Cargo.toml");
718 let original = r#"[package]
719name = "myapp"
720version = "1.0.0"
721edition = "2024"
722"#;
723 std::fs::write(&cargo_toml, original).unwrap();
724
725 let binstall_cfg = BinstallConfig {
726 enabled: Some(true),
727 pkg_url: Some("https://example.com".to_string()),
728 bin_dir: None,
729 pkg_fmt: None,
730 overrides: None,
731 };
732
733 let mut ctx = make_ctx();
734 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, true).unwrap();
735
736 let content = std::fs::read_to_string(&cargo_toml).unwrap();
738 assert_eq!(content, original);
739 }
740
741 #[test]
742 fn test_generate_binstall_metadata_missing_cargo_toml_errors() {
743 let tmp = tempfile::tempdir().unwrap();
744 let binstall_cfg = BinstallConfig {
745 enabled: Some(true),
746 pkg_url: None,
747 bin_dir: None,
748 pkg_fmt: None,
749 overrides: None,
750 };
751
752 let mut ctx = make_ctx();
753 let result = gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false);
754 assert!(result.is_err());
755 }
756
757 #[test]
758 fn test_generate_binstall_metadata_emits_per_target_overrides() {
759 let tmp = tempfile::tempdir().unwrap();
760 let cargo_toml = tmp.path().join("Cargo.toml");
761 std::fs::write(
762 &cargo_toml,
763 r#"[package]
764name = "cfgd"
765version = "1.0.0"
766edition = "2024"
767"#,
768 )
769 .unwrap();
770
771 let mut overrides = BTreeMap::new();
772 overrides.insert(
773 "x86_64-unknown-linux-gnu".to_string(),
774 BinstallOverride {
775 pkg_url: Some(
776 "https://github.com/myorg/cfgd/releases/download/v{{ .Version }}/cfgd-{{ .Version }}-linux-amd64.tar.gz"
777 .to_string(),
778 ),
779 pkg_fmt: Some("tgz".to_string()),
780 bin_dir: Some("{ bin }{ binary-ext }".to_string()),
781 },
782 );
783 overrides.insert(
784 "aarch64-apple-darwin".to_string(),
785 BinstallOverride {
786 pkg_url: Some(
787 "https://github.com/myorg/cfgd/releases/download/v{{ .Version }}/cfgd-{ version }-darwin-arm64.tar.gz"
788 .to_string(),
789 ),
790 pkg_fmt: Some("tgz".to_string()),
791 bin_dir: None,
792 },
793 );
794
795 let binstall_cfg = BinstallConfig {
796 enabled: Some(true),
797 pkg_url: None,
798 bin_dir: None,
799 pkg_fmt: None,
800 overrides: Some(overrides),
801 };
802
803 let mut ctx = make_ctx();
804 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
805
806 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
807 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
808
809 let overrides_item = &doc["package"]["metadata"]["binstall"]["overrides"];
810 assert!(
811 overrides_item.as_table().is_some(),
812 "binstall.overrides should exist as a table"
813 );
814
815 let linux = &overrides_item["x86_64-unknown-linux-gnu"];
817 assert!(
818 linux.as_table().is_some(),
819 "override sub-table should be a real table"
820 );
821 let linux_url = linux["pkg-url"].as_str().unwrap();
822 assert!(
823 linux_url.contains("cfgd-1.0.0-linux-amd64.tar.gz"),
824 "linux pkg-url should be Version-rendered with go-arch asset name, got: {linux_url}"
825 );
826 assert!(
827 !linux_url.contains("{{ .Version }}"),
828 "anodize token should be rendered, got: {linux_url}"
829 );
830 assert_eq!(linux["pkg-fmt"].as_str().unwrap(), "tgz");
831 assert_eq!(linux["bin-dir"].as_str().unwrap(), "{ bin }{ binary-ext }");
832
833 let darwin = &overrides_item["aarch64-apple-darwin"];
835 assert!(darwin.as_table().is_some());
836 let darwin_url = darwin["pkg-url"].as_str().unwrap();
837 assert!(
840 darwin_url.contains("/v1.0.0/cfgd-{ version }-darwin-arm64.tar.gz"),
841 "darwin pkg-url should render the anodize token but leave cargo-binstall's `{{ version }}` intact, got: {darwin_url}"
842 );
843
844 assert!(
846 updated.contains("[package.metadata.binstall.overrides.x86_64-unknown-linux-gnu]"),
847 "override should render as a [...] header, got:\n{updated}"
848 );
849 }
850
851 #[test]
852 fn test_generate_binstall_metadata_preserves_user_authored_keys() {
853 let tmp = tempfile::tempdir().unwrap();
854 let cargo_toml = tmp.path().join("Cargo.toml");
855 std::fs::write(
860 &cargo_toml,
861 r#"[package]
862name = "myapp"
863version = "1.0.0"
864edition = "2024"
865
866[package.metadata.binstall]
867disabled-strategies = ["quick-install", "compile"]
868pkg-url = "https://old.example.com/stale"
869
870[package.metadata.binstall.signing]
871algorithm = "minisign"
872pubkey = "RWQABCDEF1234567890"
873"#,
874 )
875 .unwrap();
876
877 let mut overrides = BTreeMap::new();
878 overrides.insert(
879 "x86_64-unknown-linux-gnu".to_string(),
880 BinstallOverride {
881 pkg_url: Some(
882 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-linux.tar.gz"
883 .to_string(),
884 ),
885 pkg_fmt: Some("tgz".to_string()),
886 bin_dir: None,
887 },
888 );
889
890 let binstall_cfg = BinstallConfig {
891 enabled: Some(true),
892 pkg_url: Some(
893 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{ target }.tar.gz"
894 .to_string(),
895 ),
896 bin_dir: None,
897 pkg_fmt: None,
898 overrides: Some(overrides),
899 };
900
901 let mut ctx = make_ctx();
902 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
903
904 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
905 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
906 let binstall = &doc["package"]["metadata"]["binstall"];
907
908 let strategies = binstall["disabled-strategies"].as_array().unwrap();
910 let strategy_vals: Vec<&str> = strategies.iter().filter_map(|v| v.as_str()).collect();
911 assert_eq!(
912 strategy_vals,
913 vec!["quick-install", "compile"],
914 "disabled-strategies must survive the merge verbatim"
915 );
916
917 let signing = &binstall["signing"];
918 assert!(
919 signing.as_table().is_some(),
920 "signing sub-table must survive the merge"
921 );
922 assert_eq!(signing["algorithm"].as_str().unwrap(), "minisign");
923 assert_eq!(signing["pubkey"].as_str().unwrap(), "RWQABCDEF1234567890");
924
925 let pkg_url = binstall["pkg-url"].as_str().unwrap();
927 assert!(
928 pkg_url.contains("/v1.0.0/myapp-{ target }.tar.gz"),
929 "pkg-url should be rewritten with the rendered Version, got: {pkg_url}"
930 );
931 assert!(
932 !pkg_url.contains("old.example.com"),
933 "stale anodize-owned pkg-url should be replaced, got: {pkg_url}"
934 );
935
936 let linux = &binstall["overrides"]["x86_64-unknown-linux-gnu"];
938 assert_eq!(linux["pkg-fmt"].as_str().unwrap(), "tgz");
939 assert!(
940 linux["pkg-url"]
941 .as_str()
942 .unwrap()
943 .contains("/v1.0.0/myapp-linux.tar.gz")
944 );
945 }
946
947 #[test]
948 fn test_generate_binstall_metadata_clears_unset_owned_key_keeps_unknown() {
949 let tmp = tempfile::tempdir().unwrap();
950 let cargo_toml = tmp.path().join("Cargo.toml");
951 std::fs::write(
954 &cargo_toml,
955 r#"[package]
956name = "myapp"
957version = "1.0.0"
958edition = "2024"
959
960[package.metadata.binstall]
961disabled-strategies = ["compile"]
962pkg-url = "https://old.example.com/stale"
963"#,
964 )
965 .unwrap();
966
967 let binstall_cfg = BinstallConfig {
968 enabled: Some(true),
969 pkg_url: None,
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 assert!(
983 binstall.get("pkg-url").is_none(),
984 "unset owned key should be cleared, got:\n{updated}"
985 );
986 let strategies = binstall["disabled-strategies"].as_array().unwrap();
987 assert_eq!(strategies.len(), 1, "unknown key must survive clearing");
988 assert_eq!(strategies.get(0).unwrap().as_str().unwrap(), "compile");
989 }
990
991 #[test]
992 fn test_generate_binstall_metadata_merges_inline_table_preserving_unknown() {
993 let tmp = tempfile::tempdir().unwrap();
994 let cargo_toml = tmp.path().join("Cargo.toml");
995 std::fs::write(
999 &cargo_toml,
1000 r#"[package]
1001name = "myapp"
1002version = "1.0.0"
1003edition = "2024"
1004metadata.binstall = { pkg-url = "https://old.example.com/stale", disabled-strategies = ["compile"] }
1005"#,
1006 )
1007 .unwrap();
1008
1009 let binstall_cfg = BinstallConfig {
1010 enabled: Some(true),
1011 pkg_url: Some(
1012 "https://github.com/myorg/myapp/releases/download/v{{ .Version }}/myapp-{ target }.tar.gz"
1013 .to_string(),
1014 ),
1015 bin_dir: None,
1016 pkg_fmt: None,
1017 overrides: None,
1018 };
1019
1020 let mut ctx = make_ctx();
1021 gen_meta(tmp.path().to_str().unwrap(), &binstall_cfg, &mut ctx, false).unwrap();
1022
1023 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1024 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1025 let binstall = &doc["package"]["metadata"]["binstall"];
1026
1027 let pkg_url = binstall["pkg-url"].as_str().unwrap();
1029 assert!(
1030 pkg_url.contains("/v1.0.0/myapp-{ target }.tar.gz")
1031 && !pkg_url.contains("old.example.com"),
1032 "inline pkg-url should be rewritten, got: {pkg_url}"
1033 );
1034 let strategies = binstall["disabled-strategies"].as_array().unwrap();
1036 assert_eq!(strategies.len(), 1);
1037 assert_eq!(strategies.get(0).unwrap().as_str().unwrap(), "compile");
1038 }
1039
1040 fn six_targets() -> Vec<String> {
1046 vec![
1047 "x86_64-unknown-linux-gnu".to_string(),
1048 "aarch64-unknown-linux-gnu".to_string(),
1049 "x86_64-apple-darwin".to_string(),
1050 "aarch64-apple-darwin".to_string(),
1051 "x86_64-pc-windows-msvc".to_string(),
1052 "aarch64-pc-windows-msvc".to_string(),
1053 ]
1054 }
1055
1056 fn anodize_like_crate() -> CrateConfig {
1060 let archive = ArchiveConfig {
1061 name_template: Some("{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}".to_string()),
1062 formats: Some(vec!["tar.gz".to_string()]),
1063 format_overrides: Some(vec![FormatOverride {
1064 os: "windows".to_string(),
1065 formats: Some(vec!["zip".to_string()]),
1066 }]),
1067 ..Default::default()
1068 };
1069 CrateConfig {
1070 name: "anodizer".to_string(),
1071 tag_template: Some("v{{ Version }}".to_string()),
1072 archives: ArchivesConfig::Configs(vec![archive]),
1073 release: Some(ReleaseConfig {
1074 github: Some(GitHubConfig {
1075 owner: "tj-smith47".to_string(),
1076 name: "anodizer".to_string(),
1077 token: None,
1078 }),
1079 ..Default::default()
1080 }),
1081 ..Default::default()
1082 }
1083 }
1084
1085 fn override_asset(binstall: &toml_edit::Item, triple: &str, version: &str) -> String {
1088 let url = binstall["overrides"][triple]["pkg-url"].as_str().unwrap();
1089 let leaf = url.rsplit('/').next().unwrap();
1090 leaf.replace("{ version }", version)
1091 }
1092
1093 #[test]
1094 fn auto_derive_covers_all_six_triples_matching_archive_names() {
1095 let tmp = tempfile::tempdir().unwrap();
1096 let cargo_toml = tmp.path().join("Cargo.toml");
1097 std::fs::write(
1098 &cargo_toml,
1099 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1100 )
1101 .unwrap();
1102 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1106 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1107
1108 let cfg = BinstallConfig {
1110 enabled: Some(true),
1111 ..Default::default()
1112 };
1113 let mut ctx = make_ctx();
1114 ctx.template_vars_mut().set("ProjectName", "anodizer");
1115 let targets = six_targets();
1116 gen_with_crate(
1117 tmp.path().to_str().unwrap(),
1118 anodize_like_crate(),
1119 &cfg,
1120 &targets,
1121 &mut ctx,
1122 )
1123 .unwrap();
1124
1125 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1126 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1127 let binstall = &doc["package"]["metadata"]["binstall"];
1128
1129 let mut check_ctx = make_ctx();
1134 check_ctx.template_vars_mut().set("ProjectName", "anodizer");
1135 let name_tmpl = "{{ ProjectName }}-{{ Version }}-{{ Os }}-{{ Arch }}";
1136 for triple in &targets {
1137 let fmt = if triple.contains("windows") {
1138 "zip"
1139 } else {
1140 "tar.gz"
1141 };
1142 let expected = crate::archive_name::render_archive_asset_name(
1143 &mut check_ctx,
1144 name_tmpl,
1145 triple,
1146 fmt,
1147 )
1148 .unwrap();
1149 let actual = override_asset(binstall, triple, "1.0.0");
1150 assert_eq!(
1151 actual, expected,
1152 "override asset for {triple} must equal the archive stage's name"
1153 );
1154
1155 let pkg_fmt = binstall["overrides"][triple]["pkg-fmt"].as_str().unwrap();
1157 assert_eq!(pkg_fmt, if fmt == "zip" { "zip" } else { "tgz" });
1158
1159 let url = binstall["overrides"][triple]["pkg-url"].as_str().unwrap();
1162 assert!(
1163 url.starts_with(
1164 "https://github.com/tj-smith47/anodizer/releases/download/v{ version }/"
1165 ),
1166 "url should target the GitHub release download path with the binstall version token, got: {url}"
1167 );
1168 assert!(
1169 url.contains("{ version }"),
1170 "url must carry cargo-binstall's version token, got: {url}"
1171 );
1172 }
1173
1174 assert_eq!(
1176 override_asset(binstall, "x86_64-unknown-linux-gnu", "9.9.9"),
1177 "anodizer-9.9.9-linux-amd64.tar.gz"
1178 );
1179 assert_eq!(
1180 override_asset(binstall, "aarch64-pc-windows-msvc", "9.9.9"),
1181 "anodizer-9.9.9-windows-arm64.zip"
1182 );
1183
1184 assert!(
1186 binstall.get("pkg-url").is_none(),
1187 "auto-derivation must not write a top-level pkg-url, got:\n{updated}"
1188 );
1189 }
1190
1191 #[test]
1192 fn auto_derive_matches_real_v091_release_assets() {
1193 let tmp = tempfile::tempdir().unwrap();
1201 let cargo_toml = tmp.path().join("Cargo.toml");
1202 std::fs::write(
1203 &cargo_toml,
1204 "[package]\nname = \"anodizer\"\nversion = \"0.9.1\"\nedition = \"2024\"\n",
1205 )
1206 .unwrap();
1207 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1210 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1211
1212 let cfg = BinstallConfig {
1213 enabled: Some(true),
1214 ..Default::default()
1215 };
1216 let mut ctx = make_ctx();
1217 ctx.template_vars_mut().set("ProjectName", "anodizer");
1218 gen_with_crate(
1219 tmp.path().to_str().unwrap(),
1220 anodize_like_crate(),
1221 &cfg,
1222 &six_targets(),
1223 &mut ctx,
1224 )
1225 .unwrap();
1226
1227 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1228 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1229 let binstall = &doc["package"]["metadata"]["binstall"];
1230
1231 let expected: &[(&str, &str, &str)] = &[
1233 (
1234 "x86_64-unknown-linux-gnu",
1235 "anodizer-0.9.1-linux-amd64.tar.gz",
1236 "tgz",
1237 ),
1238 (
1239 "aarch64-unknown-linux-gnu",
1240 "anodizer-0.9.1-linux-arm64.tar.gz",
1241 "tgz",
1242 ),
1243 (
1244 "x86_64-apple-darwin",
1245 "anodizer-0.9.1-darwin-amd64.tar.gz",
1246 "tgz",
1247 ),
1248 (
1249 "aarch64-apple-darwin",
1250 "anodizer-0.9.1-darwin-arm64.tar.gz",
1251 "tgz",
1252 ),
1253 (
1254 "x86_64-pc-windows-msvc",
1255 "anodizer-0.9.1-windows-amd64.zip",
1256 "zip",
1257 ),
1258 (
1259 "aarch64-pc-windows-msvc",
1260 "anodizer-0.9.1-windows-arm64.zip",
1261 "zip",
1262 ),
1263 ];
1264
1265 for (triple, asset, fmt) in expected {
1266 let url = binstall["overrides"][*triple]["pkg-url"].as_str().unwrap();
1267 let resolved = url.replace("{ version }", "0.9.1");
1270 let want =
1271 format!("https://github.com/tj-smith47/anodizer/releases/download/v0.9.1/{asset}");
1272 assert_eq!(
1273 resolved, want,
1274 "override for {triple} must resolve to the real v0.9.1 asset URL"
1275 );
1276 assert_eq!(
1277 binstall["overrides"][*triple]["pkg-fmt"].as_str().unwrap(),
1278 *fmt,
1279 "pkg-fmt for {triple} must match the real v0.9.1 asset format"
1280 );
1281 }
1282 }
1283
1284 #[test]
1285 fn user_pkg_url_suppresses_auto_derivation() {
1286 let tmp = tempfile::tempdir().unwrap();
1287 let cargo_toml = tmp.path().join("Cargo.toml");
1288 std::fs::write(
1289 &cargo_toml,
1290 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1291 )
1292 .unwrap();
1293
1294 let cfg = BinstallConfig {
1295 enabled: Some(true),
1296 pkg_url: Some("https://example.com/custom/anodizer-{ target }.tar.gz".to_string()),
1297 ..Default::default()
1298 };
1299 let mut ctx = make_ctx();
1300 ctx.template_vars_mut().set("ProjectName", "anodizer");
1301 gen_with_crate(
1302 tmp.path().to_str().unwrap(),
1303 anodize_like_crate(),
1304 &cfg,
1305 &six_targets(),
1306 &mut ctx,
1307 )
1308 .unwrap();
1309
1310 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1311 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1312 let binstall = &doc["package"]["metadata"]["binstall"];
1313
1314 assert_eq!(
1316 binstall["pkg-url"].as_str().unwrap(),
1317 "https://example.com/custom/anodizer-{ target }.tar.gz"
1318 );
1319 assert!(
1320 binstall.get("overrides").is_none(),
1321 "a user pkg_url must suppress auto-derived overrides, got:\n{updated}"
1322 );
1323 }
1324
1325 #[test]
1326 fn user_override_suppresses_auto_derivation() {
1327 let tmp = tempfile::tempdir().unwrap();
1328 let cargo_toml = tmp.path().join("Cargo.toml");
1329 std::fs::write(
1330 &cargo_toml,
1331 "[package]\nname = \"anodizer\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1332 )
1333 .unwrap();
1334
1335 let mut user_overrides = BTreeMap::new();
1336 user_overrides.insert(
1337 "x86_64-unknown-linux-gnu".to_string(),
1338 BinstallOverride {
1339 pkg_url: Some("https://example.com/manual-linux.tar.gz".to_string()),
1340 pkg_fmt: Some("tgz".to_string()),
1341 bin_dir: None,
1342 },
1343 );
1344 let cfg = BinstallConfig {
1345 enabled: Some(true),
1346 overrides: Some(user_overrides),
1347 ..Default::default()
1348 };
1349 let mut ctx = make_ctx();
1350 ctx.template_vars_mut().set("ProjectName", "anodizer");
1351 gen_with_crate(
1352 tmp.path().to_str().unwrap(),
1353 anodize_like_crate(),
1354 &cfg,
1355 &six_targets(),
1356 &mut ctx,
1357 )
1358 .unwrap();
1359
1360 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1361 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1362 let overrides = &doc["package"]["metadata"]["binstall"]["overrides"];
1363
1364 assert_eq!(
1367 overrides["x86_64-unknown-linux-gnu"]["pkg-url"]
1368 .as_str()
1369 .unwrap(),
1370 "https://example.com/manual-linux.tar.gz"
1371 );
1372 assert!(
1373 overrides.get("aarch64-apple-darwin").is_none(),
1374 "supplying one override must suppress auto-derivation of the rest, got:\n{updated}"
1375 );
1376 }
1377
1378 #[test]
1379 fn auto_derive_uses_default_template_when_unset() {
1380 let tmp = tempfile::tempdir().unwrap();
1383 let cargo_toml = tmp.path().join("Cargo.toml");
1384 std::fs::write(
1385 &cargo_toml,
1386 "[package]\nname = \"myapp\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1387 )
1388 .unwrap();
1389
1390 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
1393 std::fs::write(tmp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
1394
1395 let mut crate_cfg = anodize_like_crate();
1396 crate_cfg.name = "myapp".to_string();
1397 crate_cfg.archives = ArchivesConfig::Configs(vec![ArchiveConfig {
1399 formats: Some(vec!["tar.gz".to_string()]),
1400 ..Default::default()
1401 }]);
1402
1403 let cfg = BinstallConfig {
1404 enabled: Some(true),
1405 ..Default::default()
1406 };
1407 let mut ctx = make_ctx();
1408 gen_with_crate(
1409 tmp.path().to_str().unwrap(),
1410 crate_cfg,
1411 &cfg,
1412 &["x86_64-unknown-linux-gnu".to_string()],
1413 &mut ctx,
1414 )
1415 .unwrap();
1416
1417 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1418 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1419 let binstall = &doc["package"]["metadata"]["binstall"];
1420 assert_eq!(
1422 override_asset(binstall, "x86_64-unknown-linux-gnu", "1.0.0"),
1423 "myapp_1.0.0_linux_amd64.tar.gz"
1424 );
1425 }
1426
1427 #[test]
1428 fn auto_derive_noops_without_release_repo() {
1429 let tmp = tempfile::tempdir().unwrap();
1431 let cargo_toml = tmp.path().join("Cargo.toml");
1432 std::fs::write(
1433 &cargo_toml,
1434 "[package]\nname = \"myapp\"\nversion = \"1.0.0\"\nedition = \"2024\"\n",
1435 )
1436 .unwrap();
1437
1438 let mut crate_cfg = anodize_like_crate();
1439 crate_cfg.name = "myapp".to_string();
1440 crate_cfg.release = None;
1441
1442 let cfg = BinstallConfig {
1443 enabled: Some(true),
1444 ..Default::default()
1445 };
1446 let mut ctx = make_ctx();
1447 gen_with_crate(
1448 tmp.path().to_str().unwrap(),
1449 crate_cfg,
1450 &cfg,
1451 &six_targets(),
1452 &mut ctx,
1453 )
1454 .unwrap();
1455
1456 let updated = std::fs::read_to_string(&cargo_toml).unwrap();
1457 let doc = updated.parse::<toml_edit::DocumentMut>().unwrap();
1458 let binstall = &doc["package"]["metadata"]["binstall"];
1459 assert!(
1460 binstall.get("overrides").is_none(),
1461 "no release repo should mean no auto-derived overrides, got:\n{updated}"
1462 );
1463 }
1464
1465 #[test]
1468 fn normalize_inserts_empty_table_when_binstall_absent() {
1469 let mut metadata = toml_edit::Table::new();
1470 normalize_binstall_to_table(&mut metadata).unwrap();
1471 let binstall = metadata.get("binstall").expect("binstall inserted");
1472 assert!(binstall.is_table(), "must be a header table");
1473 assert!(
1474 binstall.as_table().unwrap().is_empty(),
1475 "freshly-inserted binstall table must be empty"
1476 );
1477 }
1478
1479 #[test]
1480 fn normalize_leaves_existing_header_table_untouched() {
1481 let mut metadata = toml_edit::Table::new();
1482 let mut existing = toml_edit::Table::new();
1483 existing.insert("pkg-url", toml_edit::value("https://example/x"));
1484 metadata.insert("binstall", toml_edit::Item::Table(existing));
1485 normalize_binstall_to_table(&mut metadata).unwrap();
1486 assert_eq!(
1487 metadata["binstall"]["pkg-url"].as_str(),
1488 Some("https://example/x"),
1489 "an existing header table must be preserved verbatim"
1490 );
1491 }
1492
1493 #[test]
1494 fn normalize_converts_inline_table_preserving_keys() {
1495 let doc: toml_edit::DocumentMut = r#"[package.metadata]
1498binstall = { pkg-url = "https://example/x", custom = "keep" }
1499"#
1500 .parse()
1501 .unwrap();
1502 let mut metadata = doc["package"]["metadata"].as_table().unwrap().clone();
1503 normalize_binstall_to_table(&mut metadata).unwrap();
1504 let binstall = metadata["binstall"].as_table().expect("now a header table");
1505 assert_eq!(binstall["pkg-url"].as_str(), Some("https://example/x"));
1506 assert_eq!(
1507 binstall["custom"].as_str(),
1508 Some("keep"),
1509 "user-authored inline keys must survive the conversion"
1510 );
1511 }
1512
1513 #[test]
1514 fn normalize_errors_on_non_table_binstall() {
1515 let mut metadata = toml_edit::Table::new();
1517 metadata.insert("binstall", toml_edit::value("oops"));
1518 assert!(
1519 normalize_binstall_to_table(&mut metadata).is_err(),
1520 "a malformed scalar binstall must error, not silently pass"
1521 );
1522 }
1523
1524 #[test]
1527 fn set_or_remove_str_sets_and_removes() {
1528 let mut table = toml_edit::Table::new();
1529 set_or_remove_str(&mut table, "k", Some("v"));
1530 assert_eq!(table["k"].as_str(), Some("v"));
1531 table.insert("sibling", toml_edit::value("keep"));
1533 set_or_remove_str(&mut table, "k", None);
1534 assert!(table.get("k").is_none(), "None must remove the key");
1535 assert_eq!(
1536 table["sibling"].as_str(),
1537 Some("keep"),
1538 "removing one key must not disturb siblings"
1539 );
1540 }
1541
1542 #[test]
1545 fn render_overrides_none_when_unset_or_empty() {
1546 let ctx = make_ctx();
1547 let cfg = BinstallConfig {
1548 overrides: None,
1549 ..Default::default()
1550 };
1551 assert!(
1552 render_overrides(&cfg, &ctx).unwrap().is_none(),
1553 "no overrides configured → None"
1554 );
1555 let cfg_empty = BinstallConfig {
1556 overrides: Some(BTreeMap::new()),
1557 ..Default::default()
1558 };
1559 assert!(
1560 render_overrides(&cfg_empty, &ctx).unwrap().is_none(),
1561 "an empty overrides map → None"
1562 );
1563 }
1564
1565 #[test]
1566 fn render_overrides_map_empty_is_none() {
1567 let ctx = make_ctx();
1568 let empty: BTreeMap<String, BinstallOverride> = BTreeMap::new();
1569 assert!(
1570 render_overrides_map(&empty, &ctx).unwrap().is_none(),
1571 "an empty map renders to None, never an empty table"
1572 );
1573 }
1574
1575 fn tuned_crate(env: HashMap<String, HashMap<String, String>>) -> CrateConfig {
1580 CrateConfig {
1581 name: "myapp".to_string(),
1582 builds: Some(vec![BuildConfig {
1583 binary: Some("myapp".to_string()),
1584 targets: Some(vec![
1585 "x86_64-unknown-linux-gnu".to_string(),
1586 "aarch64-unknown-linux-gnu".to_string(),
1587 "x86_64-apple-darwin".to_string(),
1588 ]),
1589 env: Some(env),
1590 ..Default::default()
1591 }]),
1592 archives: ArchivesConfig::Configs(vec![ArchiveConfig {
1593 formats: Some(vec!["tar.gz".to_string()]),
1594 ..Default::default()
1595 }]),
1596 ..Default::default()
1597 }
1598 }
1599
1600 #[test]
1607 fn asset_names_carry_config_declared_amd64_variant() {
1608 let mut env = HashMap::new();
1609 env.insert(
1610 "x86_64-unknown-linux-gnu".to_string(),
1611 HashMap::from([(
1612 "RUSTFLAGS".to_string(),
1613 "-Ctarget-cpu=x86-64-v3".to_string(),
1614 )]),
1615 );
1616 let crate_cfg = tuned_crate(env);
1617 let mut ctx = make_ctx();
1618 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1619 .unwrap()
1620 .expect("binstallable archive with targets derives names");
1621 assert_eq!(
1622 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64v3.tar.gz",
1623 "tuned target must carry the micro-arch suffix"
1624 );
1625 assert_eq!(
1626 assets["aarch64-unknown-linux-gnu"].asset_name,
1627 "myapp_1.0.0_linux_arm64.tar.gz"
1628 );
1629 assert_eq!(
1630 assets["x86_64-apple-darwin"].asset_name, "myapp_1.0.0_darwin_amd64.tar.gz",
1631 "untuned amd64 target stays on the baseline name"
1632 );
1633 }
1634
1635 #[test]
1642 fn os_templated_tuning_env_derives_per_target_levels_across_the_loop() {
1643 let mut env = HashMap::new();
1644 env.insert(
1645 "x86_64-*".to_string(),
1646 HashMap::from([(
1647 "RUSTFLAGS".to_string(),
1648 "{% if Os == \"darwin\" %}-Ctarget-cpu=x86-64-v2\
1649 {% else %}-Ctarget-cpu=x86-64-v3{% endif %}"
1650 .to_string(),
1651 )]),
1652 );
1653 let crate_cfg = tuned_crate(env);
1654 let mut ctx = make_ctx();
1655 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1656 .unwrap()
1657 .expect("binstallable archive with targets derives names");
1658 assert_eq!(
1659 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64v3.tar.gz",
1660 "first target must render the env with ITS OWN Os seeded"
1661 );
1662 assert_eq!(
1663 assets["x86_64-apple-darwin"].asset_name, "myapp_1.0.0_darwin_amd64v2.tar.gz",
1664 "second target must not render with the first target's stale Os"
1665 );
1666 assert_eq!(
1667 assets["aarch64-unknown-linux-gnu"].asset_name,
1668 "myapp_1.0.0_linux_arm64.tar.gz"
1669 );
1670 }
1671
1672 #[test]
1677 fn asset_names_fall_back_to_baseline_when_env_defeats_config_render() {
1678 let mut env = HashMap::new();
1679 env.insert(
1680 "x86_64-unknown-linux-gnu".to_string(),
1681 HashMap::from([(
1682 "RUSTFLAGS".to_string(),
1683 "{{ BuildTimeOnlyVar }}".to_string(),
1684 )]),
1685 );
1686 let crate_cfg = tuned_crate(env);
1687 let mut ctx = make_ctx();
1688 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1689 .unwrap()
1690 .expect("derivation still resolves");
1691 assert_eq!(
1692 assets["x86_64-unknown-linux-gnu"].asset_name, "myapp_1.0.0_linux_amd64.tar.gz",
1693 "unrenderable env derives no variant (baseline name)"
1694 );
1695 }
1696
1697 #[test]
1701 fn variant_derivation_does_not_leak_build_env_into_ctx() {
1702 let mut env = HashMap::new();
1703 env.insert(
1704 "x86_64-unknown-linux-gnu".to_string(),
1705 HashMap::from([
1706 (
1707 "RUSTFLAGS".to_string(),
1708 "-Ctarget-cpu=x86-64-v2".to_string(),
1709 ),
1710 ("MY_BUILD_KEY".to_string(), "leaky".to_string()),
1711 ]),
1712 );
1713 let crate_cfg = tuned_crate(env);
1714 let mut ctx = make_ctx();
1715 ctx.template_vars_mut().set_env("RUSTFLAGS", "prior-value");
1716 let before_keys = ctx.template_vars().all_env().len();
1717 let assets = crate_archive_asset_names(&crate_cfg, &[], &mut ctx)
1718 .unwrap()
1719 .unwrap();
1720 assert_eq!(
1721 assets["x86_64-unknown-linux-gnu"].asset_name,
1722 "myapp_1.0.0_linux_amd64v2.tar.gz"
1723 );
1724 assert_eq!(
1725 ctx.template_vars().all_env().get("RUSTFLAGS").unwrap(),
1726 "prior-value",
1727 "the caller's prior env value must be restored after the cascade"
1728 );
1729 assert!(
1730 !ctx.template_vars().all_env().contains_key("MY_BUILD_KEY"),
1731 "cascade-only keys must be removed after derivation"
1732 );
1733 assert_eq!(ctx.template_vars().all_env().len(), before_keys);
1734 }
1735}