1use axoasset::{toml_edit, LocalAsset};
2use axoproject::{WorkspaceGraph, WorkspaceInfo, WorkspaceKind};
3use camino::Utf8PathBuf;
4use cargo_dist_schema::TripleNameRef;
5use semver::Version;
6use serde::Deserialize;
7
8use crate::{
9 config::{
10 self, CiStyle, Config, DistMetadata, HostingStyle, InstallPathStrategy, InstallerStyle,
11 MacPkgConfig, PublishStyle,
12 },
13 do_generate,
14 errors::{DistError, DistResult},
15 platform::{triple_to_display_name, MinGlibcVersion},
16 GenerateArgs, SortedMap, METADATA_DIST, PROFILE_DIST,
17};
18
19#[derive(Debug)]
21pub struct InitArgs {
22 pub yes: bool,
24 pub no_generate: bool,
26 pub with_json_config: Option<Utf8PathBuf>,
28 pub host: Vec<HostingStyle>,
30}
31
32#[derive(Debug, Deserialize)]
37#[serde(deny_unknown_fields)]
38struct MultiDistMetadata {
39 workspace: Option<DistMetadata>,
41 #[serde(default)]
43 packages: SortedMap<String, DistMetadata>,
44}
45
46fn theme() -> dialoguer::theme::ColorfulTheme {
47 dialoguer::theme::ColorfulTheme {
48 checked_item_prefix: console::style(" [x]".to_string()).for_stderr().green(),
49 unchecked_item_prefix: console::style(" [ ]".to_string()).for_stderr().dim(),
50 active_item_style: console::Style::new().for_stderr().cyan().bold(),
51 ..dialoguer::theme::ColorfulTheme::default()
52 }
53}
54
55fn copy_cargo_workspace_metadata_dist(
57 new_workspace: &mut toml_edit::DocumentMut,
58 workspace_toml: toml_edit::DocumentMut,
59) {
60 if let Some(dist) = workspace_toml
61 .get("workspace")
62 .and_then(|t| t.get("metadata"))
63 .and_then(|t| t.get("dist"))
64 {
65 new_workspace.insert("dist", dist.to_owned());
66 }
67}
68
69fn prune_cargo_workspace_metadata_dist(workspace: &mut toml_edit::DocumentMut) {
71 workspace
72 .get_mut("workspace")
73 .and_then(|ws| ws.get_mut("metadata"))
74 .and_then(|metadata_item| metadata_item.as_table_mut())
75 .and_then(|table| table.remove("dist"));
76}
77
78fn new_cargo_workspace() -> toml_edit::DocumentMut {
80 let mut new_workspace = toml_edit::DocumentMut::new();
81
82 let mut table = toml_edit::table();
84 if let Some(t) = table.as_table_mut() {
85 let mut array = toml_edit::Array::new();
86 array.push("cargo:.");
87 t["members"] = toml_edit::value(array);
88 }
89 new_workspace.insert("workspace", table);
90
91 new_workspace
92}
93
94fn new_generic_workspace() -> toml_edit::DocumentMut {
96 let mut new_workspace = toml_edit::DocumentMut::new();
97
98 let mut table = toml_edit::table();
100 if let Some(t) = table.as_table_mut() {
101 let mut array = toml_edit::Array::new();
102 array.push("dist:.");
103 t["members"] = toml_edit::value(array);
104 }
105 new_workspace.insert("workspace", table);
106
107 new_workspace
108}
109
110fn do_migrate_from_rust_workspace() -> DistResult<()> {
111 let workspaces = config::get_project()?;
112 let root_workspace = workspaces.root_workspace();
113 let initted = has_metadata_table(root_workspace);
114
115 if root_workspace.kind != WorkspaceKind::Rust {
116 return Ok(());
118 }
119
120 if !initted {
121 return Ok(());
123 }
124
125 let workspace_toml = config::load_toml(&root_workspace.manifest_path)?;
127 let mut original_workspace_toml = workspace_toml.clone();
128
129 let mut new_workspace_toml = new_cargo_workspace();
131 copy_cargo_workspace_metadata_dist(&mut new_workspace_toml, workspace_toml);
132
133 let filename = "dist-workspace.toml";
135 let destination = root_workspace.workspace_dir.join(filename);
136
137 config::write_toml(&destination, new_workspace_toml)?;
139
140 prune_cargo_workspace_metadata_dist(&mut original_workspace_toml);
143 config::write_toml(&root_workspace.manifest_path, original_workspace_toml)?;
144
145 Ok(())
146}
147
148fn do_migrate_from_dist_toml() -> DistResult<()> {
149 let workspaces = config::get_project()?;
150 let root_workspace = workspaces.root_workspace();
151 let initted = has_metadata_table(root_workspace);
152
153 if !initted {
154 return Ok(());
155 }
156
157 if root_workspace.kind != WorkspaceKind::Generic
158 || root_workspace.manifest_path.file_name() != Some("dist.toml")
159 {
160 return Ok(());
161 }
162
163 let workspace_toml = config::load_toml(&root_workspace.manifest_path)?;
165
166 eprintln!("Migrating tables");
167 let mut new_workspace_toml = new_generic_workspace();
169 if let Some(package) = workspace_toml.get("package") {
171 let mut package = package.clone();
172 if let Some(table) = package.as_table_mut() {
175 let decor = table.decor_mut();
176 if let Some(desc) = decor.prefix().and_then(|p| p.as_str()) {
178 if !desc.starts_with('\n') {
179 decor.set_prefix(format!("\n{desc}"));
180 }
181 } else {
182 decor.set_prefix("\n");
183 }
184 }
185 new_workspace_toml.insert("package", package.to_owned());
186 }
187 if let Some(dist) = workspace_toml.get("dist") {
189 new_workspace_toml.insert("dist", dist.to_owned());
190 }
191
192 let filename = "dist-workspace.toml";
194 let destination = root_workspace.workspace_dir.join(filename);
195 config::write_toml(&destination, new_workspace_toml)?;
196 LocalAsset::remove_file(&root_workspace.manifest_path)?;
198
199 Ok(())
200}
201
202pub fn do_migrate() -> DistResult<()> {
204 do_migrate_from_rust_workspace()?;
205 do_migrate_from_dist_toml()?;
206 Ok(())
208}
209
210pub fn do_init(cfg: &Config, args: &InitArgs) -> DistResult<()> {
212 let ctrlc_handler = tokio::spawn(async move {
217 tokio::signal::ctrl_c().await.unwrap();
218
219 let term = console::Term::stdout();
220 let _ = term.show_cursor();
222
223 let exitstatus = if cfg!(windows) {
226 0xc000013a_u32 as i32
227 } else {
228 130
229 };
230 std::process::exit(exitstatus);
231 });
232
233 let workspaces = config::get_project()?;
234 let root_workspace = workspaces.root_workspace();
235 let check = console::style("✔".to_string()).for_stderr().green();
236
237 eprintln!("let's setup your dist config...");
238 eprintln!();
239
240 let mut did_add_profile = false;
242 for workspace_idx in workspaces.all_workspace_indices() {
243 let workspace = workspaces.workspace(workspace_idx);
244 if workspace.kind == WorkspaceKind::Rust {
245 let mut workspace_toml = config::load_toml(&workspace.manifest_path)?;
246 did_add_profile |= init_dist_profile(cfg, &mut workspace_toml)?;
247 config::write_toml(&workspace.manifest_path, workspace_toml)?;
248 }
249 }
250
251 if did_add_profile {
252 eprintln!("{check} added [profile.dist] to your workspace Cargo.toml");
253 }
254
255 let workspace_toml = config::load_toml(&root_workspace.manifest_path)?;
257 let initted = has_metadata_table(root_workspace);
258
259 if root_workspace.kind == WorkspaceKind::Generic
260 && initted
261 && root_workspace.manifest_path.file_name() == Some("dist.toml")
262 {
263 do_migrate()?;
264 return do_init(cfg, args);
265 }
266
267 if root_workspace.kind == WorkspaceKind::Rust && initted && !args.yes {
269 let prompt = r#"Would you like to opt in to the new configuration format?
270 Future versions of dist will feature major changes to the
271 configuration format, including a new dist-specific configuration file."#;
272 let is_migrating = dialoguer::Confirm::with_theme(&theme())
273 .with_prompt(prompt)
274 .default(false)
275 .interact()?;
276
277 if is_migrating {
278 do_migrate()?;
279 return do_init(cfg, args);
280 }
281 }
282
283 let mut newly_initted_generic = false;
286 let desired_workspace_kind = if root_workspace.kind == WorkspaceKind::Rust && !initted {
289 newly_initted_generic = true;
290 WorkspaceKind::Generic
291 } else {
292 root_workspace.kind
293 };
294
295 let multi_meta = if let Some(json_path) = &args.with_json_config {
296 let src = axoasset::SourceFile::load_local(json_path)?;
298 let multi_meta: MultiDistMetadata = src.deserialize_json()?;
299 multi_meta
300 } else {
301 let meta = get_new_dist_metadata(cfg, args, &workspaces)?;
303 MultiDistMetadata {
304 workspace: Some(meta),
305 packages: SortedMap::new(),
306 }
307 };
308
309 ctrlc_handler.abort();
312
313 let mut workspace_toml = if newly_initted_generic {
317 new_cargo_workspace()
318 } else {
319 workspace_toml
320 };
321
322 if let Some(meta) = &multi_meta.workspace {
323 apply_dist_to_workspace_toml(&mut workspace_toml, desired_workspace_kind, meta);
324 }
325
326 eprintln!();
327
328 let filename;
329 let destination;
330 if newly_initted_generic {
331 filename = "dist-workspace.toml";
333 destination = root_workspace.workspace_dir.join(filename);
334 } else {
335 filename = root_workspace
336 .manifest_path
337 .file_name()
338 .expect("no filename!?");
339 destination = root_workspace.manifest_path.to_owned();
340 };
341
342 config::write_toml(&destination, workspace_toml)?;
344 let key = if desired_workspace_kind == WorkspaceKind::Rust {
345 "[workspace.metadata.dist]"
346 } else {
347 "[dist]"
348 };
349 eprintln!("{check} added {key} to your root {filename}");
350
351 for (_idx, package) in workspaces.all_packages() {
354 let meta = multi_meta.packages.get(&package.name);
356 let needs_edit = meta.is_some();
357
358 if needs_edit {
359 let mut package_toml = config::load_toml(&package.manifest_path)?;
361 let metadata = config::get_toml_metadata(&mut package_toml, false);
362
363 let mut writing_metadata = false;
365 if let Some(meta) = meta {
366 apply_dist_to_metadata(metadata, meta);
367 writing_metadata = true;
368 }
369
370 config::write_toml(&package.manifest_path, package_toml)?;
372 if writing_metadata {
373 eprintln!(
374 "{check} added [package.metadata.dist] to {}'s Cargo.toml",
375 package.name
376 );
377 }
378 }
379 }
380
381 eprintln!("{check} dist is setup!");
382 eprintln!();
383
384 if !args.no_generate {
386 eprintln!("running 'dist generate' to apply any changes");
387 eprintln!();
388
389 let ci_args = GenerateArgs {
390 check: false,
391 modes: vec![],
392 };
393 do_generate(cfg, &ci_args)?;
394 }
395 Ok(())
396}
397
398fn init_dist_profile(
399 _cfg: &Config,
400 workspace_toml: &mut toml_edit::DocumentMut,
401) -> DistResult<bool> {
402 let profiles = workspace_toml["profile"].or_insert(toml_edit::table());
403 if let Some(t) = profiles.as_table_mut() {
404 t.set_implicit(true)
405 }
406 let dist_profile = &mut profiles[PROFILE_DIST];
407 if !dist_profile.is_none() {
408 return Ok(false);
409 }
410 let mut new_profile = toml_edit::table();
411 {
412 let new_profile = new_profile.as_table_mut().unwrap();
414 new_profile.insert("inherits", toml_edit::value("release"));
416 new_profile.insert("lto", toml_edit::value("thin"));
432 new_profile
433 .decor_mut()
434 .set_prefix("\n# The profile that 'dist' will build with\n")
435 }
436 dist_profile.or_insert(new_profile);
437
438 Ok(true)
439}
440
441fn has_metadata_table(workspace_info: &WorkspaceInfo) -> bool {
442 if workspace_info.kind == WorkspaceKind::Rust {
443 workspace_info
445 .cargo_metadata_table
446 .as_ref()
447 .and_then(|t| t.as_object())
448 .map(|t| t.contains_key(METADATA_DIST))
449 .unwrap_or(false)
450 } else {
451 config::parse_metadata_table_or_manifest(
452 &workspace_info.manifest_path,
453 workspace_info.dist_manifest_path.as_deref(),
454 workspace_info.cargo_metadata_table.as_ref(),
455 )
456 .is_ok()
457 }
458}
459
460fn get_new_dist_metadata(
465 cfg: &Config,
466 args: &InitArgs,
467 workspaces: &WorkspaceGraph,
468) -> DistResult<DistMetadata> {
469 use dialoguer::{Confirm, Input, MultiSelect};
470 let root_workspace = workspaces.root_workspace();
471 let has_config = has_metadata_table(root_workspace);
472
473 let mut meta = if has_config {
474 config::parse_metadata_table_or_manifest(
475 &root_workspace.manifest_path,
476 root_workspace.dist_manifest_path.as_deref(),
477 root_workspace.cargo_metadata_table.as_ref(),
478 )?
479 } else {
480 DistMetadata {
481 cargo_dist_version: Some(std::env!("CARGO_PKG_VERSION").parse().unwrap()),
483 cargo_dist_url_override: None,
484 rust_toolchain_version: None,
486 ci: None,
487 installers: None,
488 install_success_msg: None,
489 tap: None,
490 formula: None,
491 system_dependencies: None,
492 targets: None,
493 dist: None,
494 include: None,
495 auto_includes: None,
496 windows_archive: None,
497 unix_archive: None,
498 npm_scope: None,
499 npm_package: None,
500 checksum: None,
501 precise_builds: None,
502 merge_tasks: None,
503 fail_fast: None,
504 cache_builds: None,
505 build_local_artifacts: None,
506 dispatch_releases: None,
507 release_branch: None,
508 install_path: None,
509 features: None,
510 default_features: None,
511 all_features: None,
512 plan_jobs: None,
513 local_artifacts_jobs: None,
514 global_artifacts_jobs: None,
515 source_tarball: None,
516 host_jobs: None,
517 publish_jobs: None,
518 post_announce_jobs: None,
519 publish_prereleases: None,
520 force_latest: None,
521 create_release: None,
522 github_releases_repo: None,
523 github_releases_submodule_path: None,
524 github_release: None,
525 pr_run_mode: None,
526 allow_dirty: None,
527 ssldotcom_windows_sign: None,
528 macos_sign: None,
529 github_attestations: None,
530 msvc_crt_static: None,
531 hosting: None,
532 extra_artifacts: None,
533 github_custom_runners: None,
534 github_custom_job_permissions: None,
535 bin_aliases: None,
536 tag_namespace: None,
537 install_updater: None,
538 always_use_latest_updater: None,
539 display: None,
540 display_name: None,
541 package_libraries: None,
542 install_libraries: None,
543 github_build_setup: None,
544 mac_pkg_config: None,
545 min_glibc_version: None,
546 cargo_auditable: None,
547 cargo_cyclonedx: None,
548 omnibor: None,
549 }
550 };
551
552 let orig_meta = meta.clone();
554
555 let theme = theme();
559 let check = console::style("✔".to_string()).for_stderr().green();
561 let notice = console::style("⚠️".to_string()).for_stderr().yellow();
562
563 if !args.host.is_empty() {
564 meta.hosting = Some(args.host.clone());
565 }
566
567 let current_version: Version = std::env!("CARGO_PKG_VERSION").parse().unwrap();
569 if let Some(desired_version) = &meta.cargo_dist_version {
570 if desired_version != ¤t_version && !desired_version.pre.starts_with("github-") {
571 let default = true;
572 let prompt = format!(
573 r#"update your project to this version of dist?
574 {} => {}"#,
575 desired_version, current_version
576 );
577 let response = if args.yes {
578 default
579 } else {
580 let res = Confirm::with_theme(&theme)
581 .with_prompt(prompt)
582 .default(default)
583 .interact()?;
584 eprintln!();
585 res
586 };
587
588 if response {
589 meta.cargo_dist_version = Some(current_version);
590 } else {
591 Err(DistError::NoUpdateVersion {
592 project_version: desired_version.clone(),
593 running_version: current_version,
594 })?;
595 }
596 }
597 } else {
598 meta.cargo_dist_version = Some(current_version);
600 }
601
602 {
603 let default_platforms = crate::default_desktop_targets();
605 let mut known = crate::known_desktop_targets();
606 let config_vals = meta.targets.as_deref().unwrap_or(&default_platforms);
608 let cli_vals = cfg.targets.as_slice();
609 for val in config_vals.iter().chain(cli_vals) {
611 if !known.contains(val) {
612 known.push(val.clone());
613 }
614 }
615
616 let desc = move |triple: &TripleNameRef| -> String {
618 let pretty = triple_to_display_name(triple).unwrap_or("[unknown]");
619 format!("{pretty} ({triple})")
620 };
621 known.sort_by_cached_key(|k| desc(k).to_uppercase());
622
623 let mut defaults = vec![];
624 let mut keys = vec![];
625 for item in &known {
626 let config_had_it = config_vals.contains(item);
629 let cli_had_it = cli_vals.contains(item);
630
631 let default = config_had_it || cli_had_it;
632 defaults.push(default);
633
634 keys.push(desc(item));
635 }
636
637 let prompt = r#"what platforms do you want to build for?
639 (select with arrow keys and space, submit with enter)"#;
640 let selected = if args.yes {
641 defaults
642 .iter()
643 .enumerate()
644 .filter_map(|(idx, enabled)| enabled.then_some(idx))
645 .collect()
646 } else {
647 let res = MultiSelect::with_theme(&theme)
648 .items(&keys)
649 .defaults(&defaults)
650 .with_prompt(prompt)
651 .interact()?;
652 eprintln!();
653 res
654 };
655
656 meta.targets = Some(selected.into_iter().map(|i| known[i].clone()).collect());
658 }
659
660 if meta.ci.as_deref().unwrap_or_default().is_empty() {
665 let known = &[CiStyle::Github];
669 let mut defaults = vec![];
670 let mut keys = vec![];
671 let mut github_key = 0;
672 for item in known {
673 let mut default = meta
676 .ci
677 .as_ref()
678 .map(|ci| ci.contains(item))
679 .unwrap_or(false)
680 || cfg.ci.contains(item);
681
682 #[allow(irrefutable_let_patterns)]
685 if let CiStyle::Github = item {
686 github_key = 0;
687 default = true;
688 }
689 defaults.push(default);
690 keys.push(match item {
693 CiStyle::Github => "github",
694 });
695 }
696
697 let prompt = r#"enable Github CI and Releases?"#;
699 let default = defaults[github_key];
700
701 let github_selected = if args.yes {
702 default
703 } else {
704 let res = Confirm::with_theme(&theme)
705 .with_prompt(prompt)
706 .default(default)
707 .interact()?;
708 eprintln!();
709 res
710 };
711
712 let selected = if github_selected {
713 vec![github_key]
714 } else {
715 vec![]
716 };
717
718 let ci: Vec<_> = selected.into_iter().map(|i| known[i]).collect();
720 meta.ci = if ci.is_empty() { None } else { Some(ci) };
721 }
722
723 let has_ci = meta.ci.as_ref().map(|ci| !ci.is_empty()).unwrap_or(false);
726 {
727 let known: &[InstallerStyle] = if has_ci {
730 &[
731 InstallerStyle::Shell,
732 InstallerStyle::Powershell,
733 InstallerStyle::Npm,
734 InstallerStyle::Homebrew,
735 InstallerStyle::Msi,
736 ]
737 } else {
738 eprintln!("{notice} no CI backends enabled, most installers have been hidden");
739 &[InstallerStyle::Msi]
740 };
741 let mut defaults = vec![];
742 let mut keys = vec![];
743 for item in known {
744 let config_had_it = meta
747 .installers
748 .as_deref()
749 .unwrap_or_default()
750 .contains(item);
751 let cli_had_it = cfg.installers.contains(item);
752
753 let default = config_had_it || cli_had_it;
754 defaults.push(default);
755
756 keys.push(match item {
759 InstallerStyle::Shell => "shell",
760 InstallerStyle::Powershell => "powershell",
761 InstallerStyle::Npm => "npm",
762 InstallerStyle::Homebrew => "homebrew",
763 InstallerStyle::Msi => "msi",
764 InstallerStyle::Pkg => "pkg",
765 });
766 }
767
768 let prompt = r#"what installers do you want to build?
770 (select with arrow keys and space, submit with enter)"#;
771 let selected = if args.yes {
772 defaults
773 .iter()
774 .enumerate()
775 .filter_map(|(idx, enabled)| enabled.then_some(idx))
776 .collect()
777 } else {
778 let res = MultiSelect::with_theme(&theme)
779 .items(&keys)
780 .defaults(&defaults)
781 .with_prompt(prompt)
782 .interact()?;
783 eprintln!();
784 res
785 };
786
787 meta.installers = Some(selected.into_iter().map(|i| known[i]).collect());
789 }
790
791 let mut publish_jobs = orig_meta.publish_jobs.clone().unwrap_or(vec![]);
792
793 if meta
795 .installers
796 .as_deref()
797 .unwrap_or_default()
798 .contains(&InstallerStyle::Homebrew)
799 {
800 let homebrew_is_new = !orig_meta
801 .installers
802 .as_deref()
803 .unwrap_or_default()
804 .contains(&InstallerStyle::Homebrew);
805
806 if homebrew_is_new {
807 let prompt = r#"you've enabled Homebrew support; if you want dist
808 to automatically push package updates to a tap (repository) for you,
809 please enter the tap name (in GitHub owner/name format)"#;
810 let default = "".to_string();
811
812 let tap: String = if args.yes {
813 default
814 } else {
815 let res = Input::with_theme(&theme)
816 .with_prompt(prompt)
817 .allow_empty(true)
818 .interact_text()?;
819 eprintln!();
820 res
821 };
822 let tap = tap.trim();
823 if tap.is_empty() {
824 eprintln!("Homebrew packages will not be automatically published");
825 meta.tap = None;
826 } else {
827 meta.tap = Some(tap.to_owned());
828 publish_jobs.push(PublishStyle::Homebrew);
829
830 eprintln!("{check} Homebrew package will be published to {tap}");
831
832 eprintln!(
833 r#"{check} You must provision a GitHub token and expose it as a secret named
834 HOMEBREW_TAP_TOKEN in GitHub Actions. For more information,
835 see the documentation:
836 https://axodotdev.github.io/cargo-dist/book/installers/homebrew.html"#
837 );
838 }
839 }
840 } else {
841 let homebrew_toggled_off = orig_meta
842 .installers
843 .as_deref()
844 .unwrap_or_default()
845 .contains(&InstallerStyle::Homebrew);
846 if homebrew_toggled_off {
847 meta.tap = None;
848 publish_jobs.retain(|job| job != &PublishStyle::Homebrew);
849 }
850 }
851
852 if meta
854 .installers
855 .as_deref()
856 .unwrap_or_default()
857 .contains(&InstallerStyle::Npm)
858 {
859 let npm_is_new = !orig_meta
861 .installers
862 .as_deref()
863 .unwrap_or_default()
864 .contains(&InstallerStyle::Npm);
865 if npm_is_new {
866 let prompt = r#"you've enabled npm support, please enter the @scope you want to use
867 this is the "namespace" the package will be published under
868 (leave blank to publish globally)"#;
869 let default = "".to_string();
870
871 let scope: String = if args.yes {
872 default
873 } else {
874 let res = Input::with_theme(&theme)
875 .with_prompt(prompt)
876 .allow_empty(true)
877 .validate_with(|v: &String| {
878 let v = v.trim();
879 if v.is_empty() {
880 Ok(())
881 } else if v != v.to_ascii_lowercase() {
882 Err("npm scopes must be lowercase")
883 } else if let Some(v) = v.strip_prefix('@') {
884 if v.is_empty() {
885 Err("@ must be followed by something")
886 } else {
887 Ok(())
888 }
889 } else {
890 Err("npm scopes must start with @")
891 }
892 })
893 .interact_text()?;
894 eprintln!();
895 res
896 };
897 let scope = scope.trim();
898 if scope.is_empty() {
899 eprintln!("{check} npm packages will be published globally");
900 meta.npm_scope = None;
901 } else {
902 meta.npm_scope = Some(scope.to_owned());
903 eprintln!("{check} npm packages will be published under {scope}");
904 }
905 eprintln!();
906 }
907 } else {
908 let npm_toggled_off = orig_meta
909 .installers
910 .as_deref()
911 .unwrap_or_default()
912 .contains(&InstallerStyle::Npm);
913 if npm_toggled_off {
914 meta.npm_scope = None;
915 publish_jobs.retain(|job| job != &PublishStyle::Npm);
916 }
917 }
918
919 meta.publish_jobs = if publish_jobs.is_empty() {
920 None
921 } else {
922 Some(publish_jobs)
923 };
924
925 if orig_meta.install_updater.is_none()
926 && meta
927 .installers
928 .as_deref()
929 .unwrap_or_default()
930 .iter()
931 .any(|installer| {
932 installer == &InstallerStyle::Shell || installer == &InstallerStyle::Powershell
933 })
934 {
935 let default = false;
936 let install_updater = if args.yes {
937 default
938 } else {
939 let prompt = r#"Would you like to include an updater program with your binaries?"#;
940 let res = Confirm::with_theme(&theme)
941 .with_prompt(prompt)
942 .default(default)
943 .interact()?;
944 eprintln!();
945
946 res
947 };
948
949 meta.install_updater = Some(install_updater);
950 }
951
952 Ok(meta)
953}
954
955pub(crate) fn apply_dist_to_workspace_toml(
957 workspace_toml: &mut toml_edit::DocumentMut,
958 workspace_kind: WorkspaceKind,
959 meta: &DistMetadata,
960) {
961 let metadata = if workspace_kind == WorkspaceKind::Rust {
962 config::get_toml_metadata(workspace_toml, true)
964 } else {
965 workspace_toml.as_item_mut()
967 };
968 apply_dist_to_metadata(metadata, meta);
969}
970
971fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &DistMetadata) {
973 let dist_metadata = &mut metadata[METADATA_DIST];
974
975 if !dist_metadata.is_table() {
977 *dist_metadata = toml_edit::table();
978 }
979
980 let table = dist_metadata.as_table_mut().unwrap();
982
983 let DistMetadata {
985 cargo_dist_version,
986 cargo_dist_url_override,
987 rust_toolchain_version,
988 dist,
989 ci,
990 installers,
991 install_success_msg,
992 tap,
993 formula,
994 targets,
995 include,
996 auto_includes,
997 windows_archive,
998 unix_archive,
999 npm_scope,
1000 npm_package,
1001 checksum,
1002 precise_builds,
1003 merge_tasks,
1004 fail_fast,
1005 cache_builds,
1006 build_local_artifacts,
1007 dispatch_releases,
1008 release_branch,
1009 install_path,
1010 features,
1011 all_features,
1012 default_features,
1013 plan_jobs,
1014 local_artifacts_jobs,
1015 global_artifacts_jobs,
1016 source_tarball,
1017 host_jobs,
1018 publish_jobs,
1019 post_announce_jobs,
1020 publish_prereleases,
1021 force_latest,
1022 create_release,
1023 github_releases_repo,
1024 github_releases_submodule_path,
1025 pr_run_mode,
1026 allow_dirty,
1027 ssldotcom_windows_sign,
1028 macos_sign,
1029 github_attestations,
1030 msvc_crt_static,
1031 hosting,
1032 tag_namespace,
1033 install_updater,
1034 always_use_latest_updater,
1035 display,
1036 display_name,
1037 github_release,
1038 package_libraries,
1039 install_libraries,
1040 mac_pkg_config,
1041 min_glibc_version,
1042 cargo_auditable,
1043 cargo_cyclonedx,
1044 omnibor,
1045 extra_artifacts: _,
1047 github_custom_runners: _,
1048 github_custom_job_permissions: _,
1049 bin_aliases: _,
1050 system_dependencies: _,
1051 github_build_setup: _,
1052 } = &meta;
1053
1054 let install_path = if install_path.is_none()
1057 && installers
1058 .as_ref()
1059 .map(|i| {
1060 i.iter()
1061 .any(|el| matches!(el, InstallerStyle::Shell | InstallerStyle::Powershell))
1062 })
1063 .unwrap_or(false)
1064 {
1065 Some(InstallPathStrategy::default_list())
1066 } else {
1067 install_path.clone()
1068 };
1069
1070 apply_optional_value(
1071 table,
1072 "cargo-dist-version",
1073 "# The preferred dist version to use in CI (Cargo.toml SemVer syntax)\n",
1074 cargo_dist_version.as_ref().map(|v| v.to_string()),
1075 );
1076
1077 apply_optional_value(
1078 table,
1079 "cargo-dist-url-override",
1080 "# A URL to use to install `cargo-dist` (with the installer script)\n",
1081 cargo_dist_url_override.as_ref().map(|v| v.to_string()),
1082 );
1083
1084 apply_optional_value(
1085 table,
1086 "rust-toolchain-version",
1087 "# The preferred Rust toolchain to use in CI (rustup toolchain syntax)\n",
1088 rust_toolchain_version.as_deref(),
1089 );
1090
1091 apply_string_or_list(table, "ci", "# CI backends to support\n", ci.as_ref());
1092
1093 apply_string_list(
1094 table,
1095 "installers",
1096 "# The installers to generate for each app\n",
1097 installers.as_ref(),
1098 );
1099
1100 apply_optional_mac_pkg(
1101 table,
1102 "mac-pkg-config",
1103 "\n# Configuration for the Mac .pkg installer\n",
1104 mac_pkg_config.as_ref(),
1105 );
1106
1107 apply_optional_value(
1108 table,
1109 "tap",
1110 "# A GitHub repo to push Homebrew formulas to\n",
1111 tap.clone(),
1112 );
1113
1114 apply_optional_value(
1115 table,
1116 "formula",
1117 "# Customize the Homebrew formula name\n",
1118 formula.clone(),
1119 );
1120
1121 apply_string_list(
1122 table,
1123 "targets",
1124 "# Target platforms to build apps for (Rust target-triple syntax)\n",
1125 targets.as_ref(),
1126 );
1127
1128 apply_optional_value(
1129 table,
1130 "dist",
1131 "# Whether to consider the binaries in a package for distribution (defaults true)\n",
1132 *dist,
1133 );
1134
1135 apply_string_list(
1136 table,
1137 "include",
1138 "# Extra static files to include in each App (path relative to this Cargo.toml's dir)\n",
1139 include.as_ref(),
1140 );
1141
1142 apply_optional_value(
1143 table,
1144 "auto-includes",
1145 "# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)\n",
1146 *auto_includes,
1147 );
1148
1149 apply_optional_value(
1150 table,
1151 "windows-archive",
1152 "# The archive format to use for windows builds (defaults .zip)\n",
1153 windows_archive.map(|a| a.ext()),
1154 );
1155
1156 apply_optional_value(
1157 table,
1158 "unix-archive",
1159 "# The archive format to use for non-windows builds (defaults .tar.xz)\n",
1160 unix_archive.map(|a| a.ext()),
1161 );
1162
1163 apply_optional_value(
1164 table,
1165 "npm-package",
1166 "# The npm package should have this name\n",
1167 npm_package.as_deref(),
1168 );
1169
1170 apply_optional_value(
1171 table,
1172 "install-success-msg",
1173 "# Custom message to display on successful install\n",
1174 install_success_msg.as_deref(),
1175 );
1176
1177 apply_optional_value(
1178 table,
1179 "npm-scope",
1180 "# A namespace to use when publishing this package to the npm registry\n",
1181 npm_scope.as_deref(),
1182 );
1183
1184 apply_optional_value(
1185 table,
1186 "checksum",
1187 "# Checksums to generate for each App\n",
1188 checksum.map(|c| c.ext().as_str()),
1189 );
1190
1191 apply_optional_value(
1192 table,
1193 "precise-builds",
1194 "# Build only the required packages, and individually\n",
1195 *precise_builds,
1196 );
1197
1198 apply_optional_value(
1199 table,
1200 "merge-tasks",
1201 "# Whether to run otherwise-parallelizable tasks on the same machine\n",
1202 *merge_tasks,
1203 );
1204
1205 apply_optional_value(
1206 table,
1207 "fail-fast",
1208 "# Whether failing tasks should make us give up on all other tasks\n",
1209 *fail_fast,
1210 );
1211
1212 apply_optional_value(
1213 table,
1214 "cache-builds",
1215 "# Whether builds should try to be cached in CI\n",
1216 *cache_builds,
1217 );
1218
1219 apply_optional_value(
1220 table,
1221 "build-local-artifacts",
1222 "# Whether CI should include auto-generated code to build local artifacts\n",
1223 *build_local_artifacts,
1224 );
1225
1226 apply_optional_value(
1227 table,
1228 "dispatch-releases",
1229 "# Whether CI should trigger releases with dispatches instead of tag pushes\n",
1230 *dispatch_releases,
1231 );
1232
1233 apply_optional_value(
1234 table,
1235 "release-branch",
1236 "# Trigger releases on pushes to this branch instead of tag pushes\n",
1237 release_branch.as_ref(),
1238 );
1239
1240 apply_optional_value(
1241 table,
1242 "create-release",
1243 "# Whether dist should create a Github Release or use an existing draft\n",
1244 *create_release,
1245 );
1246
1247 apply_optional_value(
1248 table,
1249 "github-release",
1250 "# Which phase dist should use to create the GitHub release\n",
1251 github_release.as_ref().map(|a| a.to_string()),
1252 );
1253
1254 apply_optional_value(
1255 table,
1256 "github-releases-repo",
1257 "# Publish GitHub Releases to this repo instead\n",
1258 github_releases_repo.as_ref().map(|a| a.to_string()),
1259 );
1260
1261 apply_optional_value(
1262 table,
1263 "github-releases-submodule-path",
1264 "# Read the commit to be tagged from the submodule at this path\n",
1265 github_releases_submodule_path
1266 .as_ref()
1267 .map(|a| a.to_string()),
1268 );
1269
1270 apply_string_or_list(
1271 table,
1272 "install-path",
1273 "# Path that installers should place binaries in\n",
1274 install_path.as_ref(),
1275 );
1276
1277 apply_string_list(
1278 table,
1279 "features",
1280 "# Features to pass to cargo build\n",
1281 features.as_ref(),
1282 );
1283
1284 apply_optional_value(
1285 table,
1286 "default-features",
1287 "# Whether default-features should be enabled with cargo build\n",
1288 *default_features,
1289 );
1290
1291 apply_optional_value(
1292 table,
1293 "all-features",
1294 "# Whether to pass --all-features to cargo build\n",
1295 *all_features,
1296 );
1297
1298 apply_string_list(
1299 table,
1300 "plan-jobs",
1301 "# Plan jobs to run in CI\n",
1302 plan_jobs.as_ref(),
1303 );
1304
1305 apply_string_list(
1306 table,
1307 "local-artifacts-jobs",
1308 "# Local artifacts jobs to run in CI\n",
1309 local_artifacts_jobs.as_ref(),
1310 );
1311
1312 apply_string_list(
1313 table,
1314 "global-artifacts-jobs",
1315 "# Global artifacts jobs to run in CI\n",
1316 global_artifacts_jobs.as_ref(),
1317 );
1318
1319 apply_optional_value(
1320 table,
1321 "source-tarball",
1322 "# Generate and dist a source tarball\n",
1323 *source_tarball,
1324 );
1325
1326 apply_string_list(
1327 table,
1328 "host-jobs",
1329 "# Host jobs to run in CI\n",
1330 host_jobs.as_ref(),
1331 );
1332
1333 apply_string_list(
1334 table,
1335 "publish-jobs",
1336 "# Publish jobs to run in CI\n",
1337 publish_jobs.as_ref(),
1338 );
1339
1340 apply_string_list(
1341 table,
1342 "post-announce-jobs",
1343 "# Post-announce jobs to run in CI\n",
1344 post_announce_jobs.as_ref(),
1345 );
1346
1347 apply_optional_value(
1348 table,
1349 "publish-prereleases",
1350 "# Whether to publish prereleases to package managers\n",
1351 *publish_prereleases,
1352 );
1353
1354 apply_optional_value(
1355 table,
1356 "force-latest",
1357 "# Always mark releases as latest, ignoring semver semantics\n",
1358 *force_latest,
1359 );
1360
1361 apply_optional_value(
1362 table,
1363 "pr-run-mode",
1364 "# Which actions to run on pull requests\n",
1365 pr_run_mode.as_ref().map(|m| m.to_string()),
1366 );
1367
1368 apply_string_list(
1369 table,
1370 "allow-dirty",
1371 "# Skip checking whether the specified configuration files are up to date\n",
1372 allow_dirty.as_ref(),
1373 );
1374
1375 apply_optional_value(
1376 table,
1377 "msvc-crt-static",
1378 "# Whether +crt-static should be used on msvc\n",
1379 *msvc_crt_static,
1380 );
1381
1382 apply_optional_value(
1383 table,
1384 "ssldotcom-windows-sign",
1385 "",
1386 ssldotcom_windows_sign.as_ref().map(|p| p.to_string()),
1387 );
1388
1389 apply_optional_value(
1390 table,
1391 "macos-sign",
1392 "# Whether to sign macOS executables\n",
1393 *macos_sign,
1394 );
1395
1396 apply_optional_value(
1397 table,
1398 "github-attestations",
1399 "# Whether to enable GitHub Attestations\n",
1400 *github_attestations,
1401 );
1402
1403 apply_string_or_list(
1404 table,
1405 "hosting",
1406 "# Where to host releases\n",
1407 hosting.as_ref(),
1408 );
1409
1410 apply_optional_value(
1411 table,
1412 "tag-namespace",
1413 "# A prefix git tags must include for dist to care about them\n",
1414 tag_namespace.as_ref(),
1415 );
1416
1417 apply_optional_value(
1418 table,
1419 "install-updater",
1420 "# Whether to install an updater program\n",
1421 *install_updater,
1422 );
1423
1424 apply_optional_value(
1425 table,
1426 "always-use-latest-updater",
1427 "# Whether to always use the latest updater instead of a specific known-good version\n",
1428 *always_use_latest_updater,
1429 );
1430
1431 apply_optional_value(
1432 table,
1433 "display",
1434 "# Whether to display this app's installers/artifacts in release bodies\n",
1435 *display,
1436 );
1437
1438 apply_optional_value(
1439 table,
1440 "display-name",
1441 "# Custom display name to use for this app in release bodies\n",
1442 display_name.as_ref(),
1443 );
1444
1445 apply_string_or_list(
1446 table,
1447 "package-libraries",
1448 "# Which kinds of built libraries to include in the final archives\n",
1449 package_libraries.as_ref(),
1450 );
1451
1452 apply_string_or_list(
1453 table,
1454 "install-libraries",
1455 "# Which kinds of packaged libraries to install\n",
1456 install_libraries.as_ref(),
1457 );
1458
1459 apply_optional_min_glibc_version(
1460 table,
1461 "min-glibc-version",
1462 "# The minimum glibc version supported by the package (overrides auto-detection)\n",
1463 min_glibc_version.as_ref(),
1464 );
1465
1466 apply_optional_value(
1467 table,
1468 "cargo-auditable",
1469 "# Whether to embed dependency information using cargo-auditable\n",
1470 *cargo_auditable,
1471 );
1472
1473 apply_optional_value(
1474 table,
1475 "cargo-cyclonedx",
1476 "# Whether to use cargo-cyclonedx to generate an SBOM\n",
1477 *cargo_cyclonedx,
1478 );
1479
1480 apply_optional_value(
1481 table,
1482 "omnibor",
1483 "# Whether to use omnibor-cli to generate OmniBOR Artifact IDs\n",
1484 *omnibor,
1485 );
1486
1487 table.decor_mut().set_prefix("\n# Config for 'dist'\n");
1489}
1490
1491fn apply_optional_value<I>(table: &mut toml_edit::Table, key: &str, desc: &str, val: Option<I>)
1500where
1501 I: Into<toml_edit::Value>,
1502{
1503 if let Some(val) = val {
1504 table.insert(key, toml_edit::value(val));
1505 if let Some(mut key) = table.key_mut(key) {
1506 key.leaf_decor_mut().set_prefix(desc)
1507 }
1508 } else {
1509 table.remove(key);
1510 }
1511}
1512
1513fn apply_string_list<I>(table: &mut toml_edit::Table, key: &str, desc: &str, list: Option<I>)
1515where
1516 I: IntoIterator,
1517 I::Item: std::fmt::Display,
1518{
1519 if let Some(list) = list {
1520 let items = list.into_iter().map(|i| i.to_string()).collect::<Vec<_>>();
1521 let array: toml_edit::Array = items.into_iter().collect();
1522 table.insert(key, toml_edit::Item::Value(toml_edit::Value::Array(array)));
1526 if let Some(mut key) = table.key_mut(key) {
1527 key.leaf_decor_mut().set_prefix(desc)
1528 }
1529 } else {
1530 table.remove(key);
1531 }
1532}
1533
1534fn apply_string_or_list<I>(table: &mut toml_edit::Table, key: &str, desc: &str, list: Option<I>)
1536where
1537 I: IntoIterator,
1538 I::Item: std::fmt::Display,
1539{
1540 if let Some(list) = list {
1541 let items = list.into_iter().map(|i| i.to_string()).collect::<Vec<_>>();
1542 if items.len() == 1 {
1543 apply_optional_value(table, key, desc, items.into_iter().next())
1544 } else {
1545 apply_string_list(table, key, desc, Some(items))
1546 }
1547 } else {
1548 table.remove(key);
1549 }
1550}
1551
1552fn apply_optional_mac_pkg(
1554 table: &mut toml_edit::Table,
1555 key: &str,
1556 desc: &str,
1557 val: Option<&MacPkgConfig>,
1558) {
1559 if let Some(mac_pkg_config) = val {
1560 let MacPkgConfig {
1561 identifier,
1562 install_location,
1563 } = mac_pkg_config;
1564
1565 let new_item = &mut table[key];
1566 let mut new_table = toml_edit::table();
1567 if let Some(new_table) = new_table.as_table_mut() {
1568 apply_optional_value(
1569 new_table,
1570 "identifier",
1571 "# A unique identifier, in tld.domain.package format\n",
1572 identifier.as_ref(),
1573 );
1574 apply_optional_value(
1575 new_table,
1576 "install-location",
1577 "# The location to which the software should be installed\n",
1578 install_location.as_ref(),
1579 );
1580 new_table.decor_mut().set_prefix(desc);
1581 }
1582 new_item.or_insert(new_table);
1583 } else {
1584 table.remove(key);
1585 }
1586}
1587
1588fn apply_optional_min_glibc_version(
1590 table: &mut toml_edit::Table,
1591 key: &str,
1592 desc: &str,
1593 val: Option<&MinGlibcVersion>,
1594) {
1595 if let Some(min_glibc_version) = val {
1596 let new_item = &mut table[key];
1597 let mut new_table = toml_edit::table();
1598 if let Some(new_table) = new_table.as_table_mut() {
1599 for (target, version) in min_glibc_version {
1600 new_table.insert(target, toml_edit::Item::Value(version.to_string().into()));
1601 }
1602 new_table.decor_mut().set_prefix(desc);
1603 }
1604 new_item.or_insert(new_table);
1605 } else {
1606 table.remove(key);
1607 }
1608}