cargo_dist/
init.rs

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/// Arguments for `dist init` ([`do_init`][])
20#[derive(Debug)]
21pub struct InitArgs {
22    /// Whether to auto-accept the default values for interactive prompts
23    pub yes: bool,
24    /// Don't automatically generate ci
25    pub no_generate: bool,
26    /// A path to a json file containing values to set in workspace.metadata.dist
27    pub with_json_config: Option<Utf8PathBuf>,
28    /// Hosts to enable
29    pub host: Vec<HostingStyle>,
30}
31
32/// Input for --with-json-config
33///
34/// Contains a DistMetadata for the workspace.metadata.dist and
35/// then optionally ones for each package.
36#[derive(Debug, Deserialize)]
37#[serde(deny_unknown_fields)]
38struct MultiDistMetadata {
39    /// `[workspace.metadata.dist]`
40    workspace: Option<DistMetadata>,
41    /// package_name => `[package.metadata.dist]`
42    #[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
55/// Copy [workspace.metadata.dist] from one workspace to [dist] in another.
56fn 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
69/// Remove [workspace.metadata.dist], if it exists.
70fn 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
78/// Create a toml-edit document set up for a cargo workspace.
79fn new_cargo_workspace() -> toml_edit::DocumentMut {
80    let mut new_workspace = toml_edit::DocumentMut::new();
81
82    // Write generic workspace config
83    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
94/// Create a toml-edit document set up for a cargo workspace.
95fn new_generic_workspace() -> toml_edit::DocumentMut {
96    let mut new_workspace = toml_edit::DocumentMut::new();
97
98    // Write generic workspace config
99    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        // we're not using a Rust workspace, so no migration needed.
117        return Ok(());
118    }
119
120    if !initted {
121        // the workspace hasn't been initialized, so no migration needed.
122        return Ok(());
123    }
124
125    // Load in the root workspace toml to edit and write back
126    let workspace_toml = config::load_toml(&root_workspace.manifest_path)?;
127    let mut original_workspace_toml = workspace_toml.clone();
128
129    // Generate a new workspace, then populate it using config from Cargo.toml.
130    let mut new_workspace_toml = new_cargo_workspace();
131    copy_cargo_workspace_metadata_dist(&mut new_workspace_toml, workspace_toml);
132
133    // Determine config file location.
134    let filename = "dist-workspace.toml";
135    let destination = root_workspace.workspace_dir.join(filename);
136
137    // Write new config file.
138    config::write_toml(&destination, new_workspace_toml)?;
139
140    // We've been asked to migrate away from Cargo.toml; delete what
141    // we've added after writing the new config
142    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    // OK, now we know we have a root-level dist.toml. Time to fix that.
164    let workspace_toml = config::load_toml(&root_workspace.manifest_path)?;
165
166    eprintln!("Migrating tables");
167    // Init a generic workspace with the appropriate members
168    let mut new_workspace_toml = new_generic_workspace();
169    // First copy the [package] section
170    if let Some(package) = workspace_toml.get("package") {
171        let mut package = package.clone();
172        // Ensures we have whitespace between the end of [workspace] and
173        // the start of [package]
174        if let Some(table) = package.as_table_mut() {
175            let decor = table.decor_mut();
176            // Try to keep existing comments if we can
177            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    // ...then copy the [dist] section
188    if let Some(dist) = workspace_toml.get("dist") {
189        new_workspace_toml.insert("dist", dist.to_owned());
190    }
191
192    // Finally, write out the new config...
193    let filename = "dist-workspace.toml";
194    let destination = root_workspace.workspace_dir.join(filename);
195    config::write_toml(&destination, new_workspace_toml)?;
196    // ...and delete the old config
197    LocalAsset::remove_file(&root_workspace.manifest_path)?;
198
199    Ok(())
200}
201
202/// Run `dist migrate`
203pub fn do_migrate() -> DistResult<()> {
204    do_migrate_from_rust_workspace()?;
205    do_migrate_from_dist_toml()?;
206    //do_migrate_from_v0()?;
207    Ok(())
208}
209
210/// Run 'dist init'
211pub fn do_init(cfg: &Config, args: &InitArgs) -> DistResult<()> {
212    // on ctrl-c,  dialoguer/console will clean up the rest of its
213    // formatting, but the cursor will remain hidden unless we
214    // explicitly go in and show it again
215    // See: https://github.com/console-rs/dialoguer/issues/294
216    let ctrlc_handler = tokio::spawn(async move {
217        tokio::signal::ctrl_c().await.unwrap();
218
219        let term = console::Term::stdout();
220        // Ignore the error here if there is any, this is best effort
221        let _ = term.show_cursor();
222
223        // Immediately re-exit the process with the same
224        // exit code the unhandled ctrl-c would have used
225        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    // For each [workspace] Cargo.toml in the workspaces, initialize [profile]
241    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    // Load in the root workspace toml to edit and write back
256    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    // Already-initted users should be asked whether to migrate.
268    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    // If this is a Cargo.toml, offer to either write their config to
284    // a dist-workspace.toml, or migrate existing config there
285    let mut newly_initted_generic = false;
286    // Users who haven't initted yet should be opted into the
287    // new config format by default.
288    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        // json update path, read from a file and apply all requested updates verbatim
297        let src = axoasset::SourceFile::load_local(json_path)?;
298        let multi_meta: MultiDistMetadata = src.deserialize_json()?;
299        multi_meta
300    } else {
301        // run (potentially interactive) init logic
302        let meta = get_new_dist_metadata(cfg, args, &workspaces)?;
303        MultiDistMetadata {
304            workspace: Some(meta),
305            packages: SortedMap::new(),
306        }
307    };
308
309    // We're past the final dialoguer call; we can remove the
310    // ctrl-c handler.
311    ctrlc_handler.abort();
312
313    // If we're migrating, the configuration will be missing the
314    // generic workspace specification, and will have some
315    // extraneous cargo-specific stuff that we don't want.
316    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        // Migrations and newly-initted setups always use dist-workspace.toml.
332        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    // Save the workspace toml (potentially an effective no-op if we made no edits)
343    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    // Now that we've done the stuff that's definitely part of the root Cargo.toml,
352    // Optionally apply updates to packages
353    for (_idx, package) in workspaces.all_packages() {
354        // Gather up all the things we'd like to be written to this file
355        let meta = multi_meta.packages.get(&package.name);
356        let needs_edit = meta.is_some();
357
358        if needs_edit {
359            // Ok we have changes to make, let's load the toml
360            let mut package_toml = config::load_toml(&package.manifest_path)?;
361            let metadata = config::get_toml_metadata(&mut package_toml, false);
362
363            // Apply [package.metadata.dist]
364            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            // Save the result
371            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    // regenerate anything that needs to be
385    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        // For some detailed discussion, see: https://github.com/axodotdev/cargo-dist/issues/118
413        let new_profile = new_profile.as_table_mut().unwrap();
414        // We're building for release, so this is a good base!
415        new_profile.insert("inherits", toml_edit::value("release"));
416        // We're building for SUPER DUPER release, so lto is a good idea to enable!
417        //
418        // There's a decent argument for lto=true (aka "fat") here but the cost-benefit
419        // is a bit complex. Fat LTO can be way more expensive to compute (to the extent
420        // that enormous applications like chromium can become unbuildable), but definitely
421        // eeks out a bit more from your binaries.
422        //
423        // In principle dist is targeting True Shippable Binaries and so it's
424        // worth it to go nuts getting every last drop out of your binaries... but a lot
425        // of people are going to build binaries that might never even be used, so really
426        // we're just burning a bunch of CI time for nothing.
427        //
428        // The user has the freedom to crank this up higher (and/or set codegen-units=1)
429        // if they think it's worth it, but we otherwise probably shouldn't set the planet
430        // on fire just because Number Theoretically Go Up.
431        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        // Setup [workspace.metadata.dist]
444        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
460/// Initialize [workspace.metadata.dist] with default values based on what was passed on the CLI
461///
462/// Returns whether the initialization was actually done
463/// and whether ci was set
464fn 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            // If they init with this version we're gonna try to stick to it!
482            cargo_dist_version: Some(std::env!("CARGO_PKG_VERSION").parse().unwrap()),
483            cargo_dist_url_override: None,
484            // deprecated, default to not emitting it
485            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    // Clone this to simplify checking for settings changes
553    let orig_meta = meta.clone();
554
555    // Now prompt the user interactively to initialize these...
556
557    // Tune the theming a bit
558    let theme = theme();
559    // Some indicators we'll use in a few places
560    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    // Set cargo-dist-version
568    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 != &current_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        // Really not allowed, so just force them onto the current version
599        meta.cargo_dist_version = Some(current_version);
600    }
601
602    {
603        // Start with builtin targets
604        let default_platforms = crate::default_desktop_targets();
605        let mut known = crate::known_desktop_targets();
606        // If the config doesn't have targets at all, generate them
607        let config_vals = meta.targets.as_deref().unwrap_or(&default_platforms);
608        let cli_vals = cfg.targets.as_slice();
609        // Add anything custom they did to the list (this will do some reordering if they hand-edited)
610        for val in config_vals.iter().chain(cli_vals) {
611            if !known.contains(val) {
612                known.push(val.clone());
613            }
614        }
615
616        // Prettify/sort things
617        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            // If this target is in their config, keep it
627            // If they passed it on the CLI, flip it on
628            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        // Prompt the user
638        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        // Apply the results
657        meta.targets = Some(selected.into_iter().map(|i| known[i].clone()).collect());
658    }
659
660    // Enable CI backends
661    // FIXME: when there is more than one option we maybe shouldn't hide this
662    // once the user has any one enabled, right now it's just annoying to always
663    // prompt for Github CI support.
664    if meta.ci.as_deref().unwrap_or_default().is_empty() {
665        // FIXME: when there is more than one option this should be a proper
666        // multiselect like the installer selector is! For now we do
667        // most of the multi-select logic and then just give a prompt.
668        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            // If this CI style is in their config, keep it
674            // If they passed it on the CLI, flip it on
675            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            // Currently default to enabling github CI because we don't
683            // support anything else and we can give a good error later
684            #[allow(irrefutable_let_patterns)]
685            if let CiStyle::Github = item {
686                github_key = 0;
687                default = true;
688            }
689            defaults.push(default);
690            // This match is here to remind you to add new CiStyles
691            // to `known` above!
692            keys.push(match item {
693                CiStyle::Github => "github",
694            });
695        }
696
697        // Prompt the user
698        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        // Apply the results
719        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    // Enable installer backends (if they have a CI backend that can provide URLs)
724    // FIXME: "vendored" installers like msi could be enabled without any CI...
725    let has_ci = meta.ci.as_ref().map(|ci| !ci.is_empty()).unwrap_or(false);
726    {
727        // If they have CI, then they can use fetching installers,
728        // otherwise they can only do vendored installers.
729        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            // If this CI style is in their config, keep it
745            // If they passed it on the CLI, flip it on
746            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            // This match is here to remind you to add new InstallerStyles
757            // to `known` above!
758            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        // Prompt the user
769        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        // Apply the results
788        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    // Special handling of the Homebrew installer
794    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    // Special handling of the npm installer
853    if meta
854        .installers
855        .as_deref()
856        .unwrap_or_default()
857        .contains(&InstallerStyle::Npm)
858    {
859        // If npm is being newly enabled here, prompt for a @scope
860        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
955/// Update a workspace toml-edit document with the current DistMetadata value
956pub(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        // Write to metadata table
963        config::get_toml_metadata(workspace_toml, true)
964    } else {
965        // Write to document root
966        workspace_toml.as_item_mut()
967    };
968    apply_dist_to_metadata(metadata, meta);
969}
970
971/// Ensure [*.metadata.dist] has the given values
972fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &DistMetadata) {
973    let dist_metadata = &mut metadata[METADATA_DIST];
974
975    // If there's no table, make one
976    if !dist_metadata.is_table() {
977        *dist_metadata = toml_edit::table();
978    }
979
980    // Apply formatted/commented values
981    let table = dist_metadata.as_table_mut().unwrap();
982
983    // This is intentionally written awkwardly to make you update this
984    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        // These settings are complex enough that we don't support editing them in init
1046        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    // Forcibly inline the default install_path if not specified,
1055    // and if we've specified a shell or powershell installer
1056    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    // Finalize the table
1488    table.decor_mut().set_prefix("\n# Config for 'dist'\n");
1489}
1490
1491/// Update the toml table to add/remove this value
1492///
1493/// If the value is Some we will set the value and hang a description comment off of it.
1494/// If the given key already existed in the table, this will update it in place and overwrite
1495/// whatever comment was above it. If the given key is new, it will appear at the end of the
1496/// table.
1497///
1498/// If the value is None, we delete it (and any comment above it).
1499fn 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
1513/// Same as [`apply_optional_value`][] but with a list of items to `.to_string()`
1514fn 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        // FIXME: Break the array up into multiple lines with pretty formatting
1523        // if the list is "too long". Alternatively, more precisely toml-edit
1524        // the existing value so that we can preserve the user's formatting and comments.
1525        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
1534/// Same as [`apply_string_list`][] but when the list can be shorthanded as a string
1535fn 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
1552/// Similar to [`apply_optional_value`][] but specialized to `MacPkgConfig`, since we're not able to work with structs dynamically
1553fn 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
1588/// Similar to [`apply_optional_value`][] but specialized to `MinGlibcVersion`, since we're not able to work with structs dynamically
1589fn 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}