cargo_dist/config/
mod.rs

1//! Config types (for workspace.metadata.dist)
2
3use std::collections::BTreeMap;
4
5use axoasset::{toml_edit, SourceFile};
6use axoproject::local_repo::LocalRepo;
7use camino::{Utf8Path, Utf8PathBuf};
8use cargo_dist_schema::{
9    AptPackageName, ChecksumExtensionRef, ChocolateyPackageName, HomebrewPackageName,
10    PackageVersion, TripleName, TripleNameRef,
11};
12use serde::{Deserialize, Serialize};
13
14use crate::announce::TagSettings;
15use crate::SortedMap;
16use crate::{
17    errors::{DistError, DistResult},
18    METADATA_DIST,
19};
20
21pub mod v0;
22pub mod v0_to_v1;
23pub mod v1;
24
25pub use v0::{DistMetadata, GenericConfig};
26
27/// values of the form `permission-name: read`
28pub type GithubPermissionMap = SortedMap<String, GithubPermission>;
29
30/// Possible values for a github ci permission
31///
32/// These are assumed to be strictly increasing in power, so admin includes write includes read.
33#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq)]
34#[serde(rename_all = "kebab-case")]
35pub enum GithubPermission {
36    /// Read (min)
37    Read,
38    /// Write
39    Write,
40    /// Admin (max)
41    Admin,
42}
43
44/// Global config for commands
45#[derive(Debug, Clone)]
46pub struct Config {
47    /// Settings for the announcement tag
48    pub tag_settings: TagSettings,
49    /// Whether to actually try to side-effectfully create a hosting directory on a server
50    ///
51    /// this is used for compute_hosting
52    pub create_hosting: bool,
53    /// The subset of artifacts we want to build
54    pub artifact_mode: ArtifactMode,
55    /// Whether local paths to files should be in the final dist json output
56    pub no_local_paths: bool,
57    /// If true, override allow-dirty in the config and ignore all dirtiness
58    pub allow_all_dirty: bool,
59    /// Target triples we want to build for
60    pub targets: Vec<TripleName>,
61    /// CI kinds we want to support
62    pub ci: Vec<CiStyle>,
63    /// Installers we want to generate
64    pub installers: Vec<InstallerStyle>,
65    /// What command was being invoked here, used for SystemIds
66    pub root_cmd: String,
67}
68
69/// How we should select the artifacts to build
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum ArtifactMode {
72    /// Build target-specific artifacts like archives, symbols, msi installers
73    Local,
74    /// Build globally unique artifacts like curl-sh installers, npm packages, metadata...
75    Global,
76    /// Fuzzily build "as much as possible" for the host system
77    Host,
78    /// Build all the artifacts; only really appropriate for `dist manifest`
79    All,
80    /// Fake all the artifacts; useful for testing/mocking/staging
81    Lies,
82}
83
84impl std::fmt::Display for ArtifactMode {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        let string = match self {
87            ArtifactMode::Local => "local",
88            ArtifactMode::Global => "global",
89            ArtifactMode::Host => "host",
90            ArtifactMode::All => "all",
91            ArtifactMode::Lies => "lies",
92        };
93        string.fmt(f)
94    }
95}
96
97/// The style of CI we should generate
98#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
99#[serde(rename_all = "kebab-case")]
100pub enum CiStyle {
101    /// Generate Github CI
102    Github,
103}
104impl CiStyle {
105    /// If the CI provider provides a native release hosting system, get it
106    pub(crate) fn native_hosting(&self) -> Option<HostingStyle> {
107        match self {
108            CiStyle::Github => Some(HostingStyle::Github),
109        }
110    }
111}
112
113impl std::fmt::Display for CiStyle {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        let string = match self {
116            CiStyle::Github => "github",
117        };
118        string.fmt(f)
119    }
120}
121
122impl std::str::FromStr for CiStyle {
123    type Err = DistError;
124    fn from_str(val: &str) -> DistResult<Self> {
125        let res = match val {
126            "github" => CiStyle::Github,
127            s => {
128                return Err(DistError::UnrecognizedCiStyle {
129                    style: s.to_string(),
130                })
131            }
132        };
133        Ok(res)
134    }
135}
136
137/// Type of library to install
138#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
139pub enum LibraryStyle {
140    /// cdylib
141    #[serde(rename = "cdylib")]
142    CDynamic,
143    /// cstaticlib
144    #[serde(rename = "cstaticlib")]
145    CStatic,
146}
147
148impl std::fmt::Display for LibraryStyle {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        let string = match self {
151            Self::CDynamic => "cdylib",
152            Self::CStatic => "cstaticlib",
153        };
154        string.fmt(f)
155    }
156}
157
158impl std::str::FromStr for LibraryStyle {
159    type Err = DistError;
160    fn from_str(val: &str) -> DistResult<Self> {
161        let res = match val {
162            "cdylib" => Self::CDynamic,
163            "cstaticlib" => Self::CStatic,
164            s => {
165                return Err(DistError::UnrecognizedLibraryStyle {
166                    style: s.to_string(),
167                })
168            }
169        };
170        Ok(res)
171    }
172}
173
174/// The style of Installer we should generate
175#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
176#[serde(rename_all = "kebab-case")]
177pub enum InstallerStyle {
178    /// Generate a shell script that fetches from [`cargo_dist_schema::Release::artifact_download_url`][]
179    Shell,
180    /// Generate a powershell script that fetches from [`cargo_dist_schema::Release::artifact_download_url`][]
181    Powershell,
182    /// Generate an npm project that fetches from [`cargo_dist_schema::Release::artifact_download_url`][]
183    Npm,
184    /// Generate a Homebrew formula that fetches from [`cargo_dist_schema::Release::artifact_download_url`][]
185    Homebrew,
186    /// Generate an msi installer that embeds the binary
187    Msi,
188    /// Generate an Apple pkg installer that embeds the binary
189    Pkg,
190}
191
192impl std::fmt::Display for InstallerStyle {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        let string = match self {
195            InstallerStyle::Shell => "shell",
196            InstallerStyle::Powershell => "powershell",
197            InstallerStyle::Npm => "npm",
198            InstallerStyle::Homebrew => "homebrew",
199            InstallerStyle::Msi => "msi",
200            InstallerStyle::Pkg => "pkg",
201        };
202        string.fmt(f)
203    }
204}
205
206/// When to create GitHub releases
207#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
208#[serde(rename_all = "kebab-case")]
209pub enum GithubReleasePhase {
210    /// Release position depends on whether axo releases is enabled
211    #[default]
212    Auto,
213    /// Create release during the "host" stage, before npm and Homebrew
214    Host,
215    /// Create release during the "announce" stage, after all publish jobs
216    Announce,
217}
218
219impl std::fmt::Display for GithubReleasePhase {
220    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221        let string = match self {
222            GithubReleasePhase::Auto => "auto",
223            GithubReleasePhase::Host => "host",
224            GithubReleasePhase::Announce => "announce",
225        };
226        string.fmt(f)
227    }
228}
229
230/// The style of hosting we should use for artifacts
231#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
232#[serde(rename_all = "kebab-case")]
233pub enum HostingStyle {
234    /// Host on Github Releases
235    Github,
236    /// Host on Axo Releases ("Abyss")
237    Axodotdev,
238}
239
240impl std::fmt::Display for HostingStyle {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        let string = match self {
243            HostingStyle::Github => "github",
244            HostingStyle::Axodotdev => "axodotdev",
245        };
246        string.fmt(f)
247    }
248}
249
250impl std::str::FromStr for HostingStyle {
251    type Err = DistError;
252    fn from_str(val: &str) -> DistResult<Self> {
253        let res = match val {
254            "github" => HostingStyle::Github,
255            "axodotdev" => HostingStyle::Axodotdev,
256            s => {
257                return Err(DistError::UnrecognizedHostingStyle {
258                    style: s.to_string(),
259                })
260            }
261        };
262        Ok(res)
263    }
264}
265
266/// The publish jobs we should run
267#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
268#[serde(rename_all = "kebab-case")]
269pub enum PublishStyle {
270    /// Publish a Homebrew formula to a tap repository
271    Homebrew,
272    /// Publish an npm pkg to the global npm registry
273    Npm,
274    /// User-supplied value
275    User(String),
276}
277
278impl std::str::FromStr for PublishStyle {
279    type Err = DistError;
280    fn from_str(s: &str) -> DistResult<Self> {
281        if let Some(slug) = s.strip_prefix("./") {
282            Ok(Self::User(slug.to_owned()))
283        } else if s == "homebrew" {
284            Ok(Self::Homebrew)
285        } else if s == "npm" {
286            Ok(Self::Npm)
287        } else {
288            Err(DistError::UnrecognizedJobStyle {
289                style: s.to_owned(),
290            })
291        }
292    }
293}
294
295impl<'de> serde::Deserialize<'de> for PublishStyle {
296    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
297    where
298        D: serde::Deserializer<'de>,
299    {
300        use serde::de::Error;
301
302        let path = String::deserialize(deserializer)?;
303        path.parse().map_err(|e| D::Error::custom(format!("{e}")))
304    }
305}
306
307impl std::fmt::Display for PublishStyle {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        match self {
310            PublishStyle::Homebrew => write!(f, "homebrew"),
311            PublishStyle::Npm => write!(f, "npm"),
312            PublishStyle::User(s) => write!(f, "./{s}"),
313        }
314    }
315}
316
317/// Extra CI jobs we should run
318#[derive(Clone, Debug, PartialEq, Eq)]
319pub enum JobStyle {
320    /// User-supplied value
321    User(String),
322}
323
324impl std::str::FromStr for JobStyle {
325    type Err = DistError;
326    fn from_str(s: &str) -> DistResult<Self> {
327        if let Some(slug) = s.strip_prefix("./") {
328            Ok(Self::User(slug.to_owned()))
329        } else {
330            Err(DistError::UnrecognizedJobStyle {
331                style: s.to_owned(),
332            })
333        }
334    }
335}
336
337impl serde::Serialize for JobStyle {
338    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
339    where
340        S: serde::Serializer,
341    {
342        let s = self.to_string();
343        s.serialize(serializer)
344    }
345}
346
347impl<'de> serde::Deserialize<'de> for JobStyle {
348    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
349    where
350        D: serde::Deserializer<'de>,
351    {
352        use serde::de::Error;
353
354        let path = String::deserialize(deserializer)?;
355        path.parse().map_err(|e| D::Error::custom(format!("{e}")))
356    }
357}
358
359impl std::fmt::Display for JobStyle {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        match self {
362            JobStyle::User(s) => write!(f, "./{s}"),
363        }
364    }
365}
366
367/// The style of zip/tarball to make
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum ZipStyle {
370    /// `.zip`
371    Zip,
372    /// `.tar.<compression>`
373    Tar(CompressionImpl),
374    /// Don't bundle/compress this, it's just a temp dir
375    TempDir,
376}
377
378/// Compression impls (used by [`ZipStyle::Tar`][])
379#[derive(Debug, Copy, Clone, PartialEq, Eq)]
380pub enum CompressionImpl {
381    /// `.gz`
382    Gzip,
383    /// `.xz`
384    Xzip,
385    /// `.zst`
386    Zstd,
387}
388impl ZipStyle {
389    /// Get the extension used for this kind of zip
390    pub fn ext(&self) -> &'static str {
391        match self {
392            ZipStyle::TempDir => "",
393            ZipStyle::Zip => ".zip",
394            ZipStyle::Tar(compression) => match compression {
395                CompressionImpl::Gzip => ".tar.gz",
396                CompressionImpl::Xzip => ".tar.xz",
397                CompressionImpl::Zstd => ".tar.zst",
398            },
399        }
400    }
401}
402
403impl Serialize for ZipStyle {
404    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
405    where
406        S: serde::Serializer,
407    {
408        serializer.serialize_str(self.ext())
409    }
410}
411
412impl<'de> Deserialize<'de> for ZipStyle {
413    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
414    where
415        D: serde::Deserializer<'de>,
416    {
417        use serde::de::Error;
418
419        let ext = String::deserialize(deserializer)?;
420        match &*ext {
421            ".zip" => Ok(ZipStyle::Zip),
422            ".tar.gz" => Ok(ZipStyle::Tar(CompressionImpl::Gzip)),
423            ".tar.xz" => Ok(ZipStyle::Tar(CompressionImpl::Xzip)),
424            ".tar.zstd" | ".tar.zst" => Ok(ZipStyle::Tar(CompressionImpl::Zstd)),
425            _ => Err(D::Error::custom(format!(
426                "unknown archive format {ext}, expected one of: .zip, .tar.gz, .tar.xz, .tar.zstd, .tar.zst"
427            ))),
428        }
429    }
430}
431
432/// key for the install-path config that selects [`InstallPathStrategyCargoHome`][]
433const CARGO_HOME_INSTALL_PATH: &str = "CARGO_HOME";
434
435/// Strategy for install binaries
436#[derive(Debug, Clone, PartialEq)]
437pub enum InstallPathStrategy {
438    /// install to $CARGO_HOME, falling back to ~/.cargo/
439    CargoHome,
440    /// install to this subdir of the user's home
441    ///
442    /// syntax: `~/subdir`
443    HomeSubdir {
444        /// The subdir of home to install to
445        subdir: String,
446    },
447    /// install to this subdir of this env var
448    ///
449    /// syntax: `$ENV_VAR/subdir`
450    EnvSubdir {
451        /// The env var to get the base of the path from
452        env_key: String,
453        /// The subdir to install to
454        subdir: String,
455    },
456}
457
458impl InstallPathStrategy {
459    /// Returns the default set of install paths
460    pub fn default_list() -> Vec<Self> {
461        vec![InstallPathStrategy::CargoHome]
462    }
463}
464
465impl std::str::FromStr for InstallPathStrategy {
466    type Err = DistError;
467    fn from_str(path: &str) -> DistResult<Self> {
468        if path == CARGO_HOME_INSTALL_PATH {
469            Ok(InstallPathStrategy::CargoHome)
470        } else if let Some(subdir) = path.strip_prefix("~/") {
471            if subdir.is_empty() {
472                Err(DistError::InstallPathHomeSubdir {
473                    path: path.to_owned(),
474                })
475            } else {
476                Ok(InstallPathStrategy::HomeSubdir {
477                    // If there's a trailing slash, strip it to be uniform
478                    subdir: subdir.strip_suffix('/').unwrap_or(subdir).to_owned(),
479                })
480            }
481        } else if let Some(val) = path.strip_prefix('$') {
482            if let Some((env_key, subdir)) = val.split_once('/') {
483                Ok(InstallPathStrategy::EnvSubdir {
484                    env_key: env_key.to_owned(),
485                    // If there's a trailing slash, strip it to be uniform
486                    subdir: subdir.strip_suffix('/').unwrap_or(subdir).to_owned(),
487                })
488            } else {
489                Err(DistError::InstallPathEnvSlash {
490                    path: path.to_owned(),
491                })
492            }
493        } else {
494            Err(DistError::InstallPathInvalid {
495                path: path.to_owned(),
496            })
497        }
498    }
499}
500
501impl std::fmt::Display for InstallPathStrategy {
502    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503        match self {
504            InstallPathStrategy::CargoHome => write!(f, "{}", CARGO_HOME_INSTALL_PATH),
505            InstallPathStrategy::HomeSubdir { subdir } => write!(f, "~/{subdir}"),
506            InstallPathStrategy::EnvSubdir { env_key, subdir } => write!(f, "${env_key}/{subdir}"),
507        }
508    }
509}
510
511impl serde::Serialize for InstallPathStrategy {
512    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
513    where
514        S: serde::Serializer,
515    {
516        serializer.serialize_str(&self.to_string())
517    }
518}
519
520impl<'de> serde::Deserialize<'de> for InstallPathStrategy {
521    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
522    where
523        D: serde::Deserializer<'de>,
524    {
525        use serde::de::Error;
526
527        let path = String::deserialize(deserializer)?;
528        path.parse().map_err(|e| D::Error::custom(format!("{e}")))
529    }
530}
531
532/// A GitHub repo like 'axodotdev/axolotlsay'
533#[derive(Debug, Clone, PartialEq)]
534pub struct GithubRepoPair {
535    /// owner (axodotdev)
536    pub owner: String,
537    /// repo (axolotlsay)
538    pub repo: String,
539}
540
541impl std::str::FromStr for GithubRepoPair {
542    type Err = DistError;
543    fn from_str(pair: &str) -> DistResult<Self> {
544        let Some((owner, repo)) = pair.split_once('/') else {
545            return Err(DistError::GithubRepoPairParse {
546                pair: pair.to_owned(),
547            });
548        };
549        Ok(GithubRepoPair {
550            owner: owner.to_owned(),
551            repo: repo.to_owned(),
552        })
553    }
554}
555
556impl std::fmt::Display for GithubRepoPair {
557    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558        write!(f, "{}/{}", self.owner, self.repo)
559    }
560}
561
562impl serde::Serialize for GithubRepoPair {
563    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
564    where
565        S: serde::Serializer,
566    {
567        serializer.serialize_str(&self.to_string())
568    }
569}
570
571impl<'de> serde::Deserialize<'de> for GithubRepoPair {
572    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
573    where
574        D: serde::Deserializer<'de>,
575    {
576        use serde::de::Error;
577
578        let path = String::deserialize(deserializer)?;
579        path.parse().map_err(|e| D::Error::custom(format!("{e}")))
580    }
581}
582
583impl GithubRepoPair {
584    /// Convert this into a jinja-friendly form
585    pub fn into_jinja(self) -> JinjaGithubRepoPair {
586        JinjaGithubRepoPair {
587            owner: self.owner,
588            repo: self.repo,
589        }
590    }
591}
592
593/// Jinja-friendly version of [`GithubRepoPair`][]
594#[derive(Debug, Clone, Serialize)]
595pub struct JinjaGithubRepoPair {
596    /// owner
597    pub owner: String,
598    /// repo
599    pub repo: String,
600}
601
602/// Strategy for install binaries (replica to have different Serialize for jinja)
603///
604/// The serialize/deserialize impls are already required for loading/saving the config
605/// from toml/json, and that serialize impl just creates a plain string again. To allow
606/// jinja templates to have richer context we have use duplicate type with a more
607/// conventional derived serialize.
608#[derive(Debug, Clone, Serialize)]
609#[serde(tag = "kind")]
610pub enum JinjaInstallPathStrategy {
611    /// install to $CARGO_HOME, falling back to ~/.cargo/
612    CargoHome,
613    /// install to this subdir of the user's home
614    ///
615    /// syntax: `~/subdir`
616    HomeSubdir {
617        /// The subdir of home to install to
618        subdir: String,
619    },
620    /// install to this subdir of this env var
621    ///
622    /// syntax: `$ENV_VAR/subdir`
623    EnvSubdir {
624        /// The env var to get the base of the path from
625        env_key: String,
626        /// The subdir to install to
627        subdir: String,
628    },
629}
630
631impl InstallPathStrategy {
632    /// Convert this into a jinja-friendly form
633    pub fn into_jinja(self) -> JinjaInstallPathStrategy {
634        match self {
635            InstallPathStrategy::CargoHome => JinjaInstallPathStrategy::CargoHome,
636            InstallPathStrategy::HomeSubdir { subdir } => {
637                JinjaInstallPathStrategy::HomeSubdir { subdir }
638            }
639            InstallPathStrategy::EnvSubdir { env_key, subdir } => {
640                JinjaInstallPathStrategy::EnvSubdir { env_key, subdir }
641            }
642        }
643    }
644}
645
646/// A checksumming algorithm
647#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
648#[serde(rename_all = "kebab-case")]
649pub enum ChecksumStyle {
650    /// sha256sum (using the sha2 crate)
651    Sha256,
652    /// sha512sum (using the sha2 crate)
653    Sha512,
654    /// sha3-256sum (using the sha3 crate)
655    Sha3_256,
656    /// sha3-512sum (using the sha3 crate)
657    Sha3_512,
658    /// b2sum (using the blake2 crate)
659    Blake2s,
660    /// b2sum (using the blake2 crate)
661    Blake2b,
662    /// Do not checksum
663    False,
664}
665
666impl ChecksumStyle {
667    /// Get the extension of a checksum
668    pub fn ext(self) -> &'static ChecksumExtensionRef {
669        ChecksumExtensionRef::from_str(match self {
670            ChecksumStyle::Sha256 => "sha256",
671            ChecksumStyle::Sha512 => "sha512",
672            ChecksumStyle::Sha3_256 => "sha3-256",
673            ChecksumStyle::Sha3_512 => "sha3-512",
674            ChecksumStyle::Blake2s => "blake2s",
675            ChecksumStyle::Blake2b => "blake2b",
676            ChecksumStyle::False => "false",
677        })
678    }
679}
680
681/// Which style(s) of configuration to generate
682#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
683pub enum GenerateMode {
684    /// Generate CI scripts for orchestrating dist
685    #[serde(rename = "ci")]
686    Ci,
687    /// Generate wsx (WiX) templates for msi installers
688    #[serde(rename = "msi")]
689    Msi,
690}
691
692impl std::fmt::Display for GenerateMode {
693    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
694        match self {
695            GenerateMode::Ci => "ci".fmt(f),
696            GenerateMode::Msi => "msi".fmt(f),
697        }
698    }
699}
700
701/// Arguments to `dist host`
702#[derive(Clone, Debug)]
703pub struct HostArgs {
704    /// Which hosting steps to run
705    pub steps: Vec<HostStyle>,
706}
707
708/// What parts of hosting to perform
709#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
710pub enum HostStyle {
711    /// Check that hosting API keys are working
712    Check,
713    /// Create a location to host artifacts
714    Create,
715    /// Upload artifacts
716    Upload,
717    /// Release artifacts
718    Release,
719    /// Announce artifacts
720    Announce,
721}
722
723impl std::fmt::Display for HostStyle {
724    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
725        let string = match self {
726            HostStyle::Check => "check",
727            HostStyle::Create => "create",
728            HostStyle::Upload => "upload",
729            HostStyle::Release => "release",
730            HostStyle::Announce => "announce",
731        };
732        string.fmt(f)
733    }
734}
735
736/// Configuration for Mac .pkg installers
737#[derive(Debug, Clone, Deserialize, Serialize)]
738#[serde(rename_all = "kebab-case")]
739pub struct MacPkgConfig {
740    /// A unique identifier, in tld.domain.package format
741    pub identifier: Option<String>,
742    /// The location to which the software should be installed.
743    /// If not specified, /usr/local will be used.
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub install_location: Option<String>,
746}
747
748/// Packages to install before build from the system package manager
749#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
750pub struct SystemDependencies {
751    /// Packages to install in Homebrew
752    #[serde(default)]
753    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
754    pub homebrew: BTreeMap<HomebrewPackageName, SystemDependency>,
755
756    /// Packages to install in apt
757    #[serde(default)]
758    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
759    pub apt: BTreeMap<AptPackageName, SystemDependency>,
760
761    /// Package to install in Chocolatey
762    #[serde(default)]
763    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
764    pub chocolatey: BTreeMap<ChocolateyPackageName, SystemDependency>,
765}
766
767impl SystemDependencies {
768    /// Extends `self` with the elements of `other`.
769    pub fn append(&mut self, other: &mut Self) {
770        self.homebrew.append(&mut other.homebrew);
771        self.apt.append(&mut other.apt);
772        self.chocolatey.append(&mut other.chocolatey);
773    }
774}
775
776/// Represents a package from a system package manager
777// newtype wrapper to hang a manual derive impl off of
778#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
779pub struct SystemDependency(pub SystemDependencyComplex);
780
781/// Backing type for SystemDependency
782#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
783pub struct SystemDependencyComplex {
784    /// The version to install, as expected by the underlying package manager
785    pub version: Option<PackageVersion>,
786    /// Stages at which the dependency is required
787    #[serde(default)]
788    pub stage: Vec<DependencyKind>,
789    /// One or more targets this package should be installed on; defaults to all targets if not specified
790    #[serde(default)]
791    pub targets: Vec<TripleName>,
792}
793
794impl SystemDependencyComplex {
795    /// Checks if this dependency should be installed on the specified target.
796    pub fn wanted_for_target(&self, target: &TripleNameRef) -> bool {
797        if self.targets.is_empty() {
798            true
799        } else {
800            self.targets.iter().any(|t| t == target)
801        }
802    }
803
804    /// Checks if this dependency should used in the specified stage.
805    pub fn stage_wanted(&self, stage: &DependencyKind) -> bool {
806        if self.stage.is_empty() {
807            match stage {
808                DependencyKind::Build => true,
809                DependencyKind::Run => false,
810            }
811        } else {
812            self.stage.contains(stage)
813        }
814    }
815}
816
817/// Definition for a single package
818#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
819#[serde(untagged)]
820pub enum SystemDependencyKind {
821    /// Simple specification format, parsed as cmake = 'version'
822    /// The special string "*" is parsed as a None version
823    Untagged(String),
824    /// Complex specification format
825    Tagged(SystemDependencyComplex),
826}
827
828/// Provides detail on when a specific dependency is required
829#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
830#[serde(rename_all = "kebab-case")]
831pub enum DependencyKind {
832    /// A dependency that must be present when the software is being built
833    Build,
834    /// A dependency that must be present when the software is being used
835    Run,
836}
837
838impl std::fmt::Display for DependencyKind {
839    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
840        match self {
841            DependencyKind::Build => "build".fmt(f),
842            DependencyKind::Run => "run".fmt(f),
843        }
844    }
845}
846
847impl<'de> Deserialize<'de> for SystemDependency {
848    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
849    where
850        D: serde::Deserializer<'de>,
851    {
852        let kind: SystemDependencyKind = SystemDependencyKind::deserialize(deserializer)?;
853
854        let res = match kind {
855            SystemDependencyKind::Untagged(version) => {
856                let v = if version == "*" { None } else { Some(version) };
857                SystemDependencyComplex {
858                    version: v.map(PackageVersion::new),
859                    stage: vec![],
860                    targets: vec![],
861                }
862            }
863            SystemDependencyKind::Tagged(dep) => dep,
864        };
865
866        Ok(SystemDependency(res))
867    }
868}
869
870/// Settings for which Generate targets can be dirty
871#[derive(Debug, Clone)]
872pub enum DirtyMode {
873    /// Allow only these targets
874    AllowList(Vec<GenerateMode>),
875    /// Allow all targets
876    AllowAll,
877}
878
879impl DirtyMode {
880    /// Do we need to run this Generate Mode
881    pub fn should_run(&self, mode: GenerateMode) -> bool {
882        match self {
883            DirtyMode::AllowAll => false,
884            DirtyMode::AllowList(list) => !list.contains(&mode),
885        }
886    }
887}
888
889/// For features that can be generated in "test" or "production" mode
890#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
891#[serde(rename_all = "kebab-case")]
892pub enum ProductionMode {
893    /// test mode
894    Test,
895    /// production mode
896    Prod,
897}
898
899/// An extra artifact to upload alongside the release tarballs,
900/// and the build command which produces it.
901#[derive(Debug, Clone, Deserialize, Serialize)]
902#[serde(rename_all = "kebab-case")]
903pub struct ExtraArtifact {
904    /// The working dir to run the command in
905    ///
906    /// If blank, the directory of the manifest that defines this is used.
907    #[serde(default)]
908    #[serde(skip_serializing_if = "path_is_empty")]
909    pub working_dir: Utf8PathBuf,
910    /// The build command to invoke in the working_dir
911    #[serde(rename = "build")]
912    pub command: Vec<String>,
913    /// Relative paths (from the working_dir) to artifacts that should be included
914    #[serde(rename = "artifacts")]
915    pub artifact_relpaths: Vec<Utf8PathBuf>,
916}
917
918/// Why doesn't this exist omg
919fn path_is_empty(p: &Utf8PathBuf) -> bool {
920    p.as_str().is_empty()
921}
922
923impl std::fmt::Display for ProductionMode {
924    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
925        match self {
926            ProductionMode::Test => "test".fmt(f),
927            ProductionMode::Prod => "prod".fmt(f),
928        }
929    }
930}
931
932pub(crate) fn parse_metadata_table_or_manifest(
933    manifest_path: &Utf8Path,
934    dist_manifest_path: Option<&Utf8Path>,
935    metadata_table: Option<&serde_json::Value>,
936) -> DistResult<DistMetadata> {
937    if let Some(dist_manifest_path) = dist_manifest_path {
938        reject_metadata_table(manifest_path, dist_manifest_path, metadata_table)?;
939        // Generic dist.toml
940        let src = SourceFile::load_local(dist_manifest_path)?;
941        parse_generic_config(src)
942    } else {
943        // Pre-parsed Rust metadata table
944        parse_metadata_table(manifest_path, metadata_table)
945    }
946}
947
948pub(crate) fn parse_generic_config(src: SourceFile) -> DistResult<DistMetadata> {
949    let config: GenericConfig = src.deserialize_toml()?;
950    Ok(config.dist.unwrap_or_default())
951}
952
953pub(crate) fn reject_metadata_table(
954    manifest_path: &Utf8Path,
955    dist_manifest_path: &Utf8Path,
956    metadata_table: Option<&serde_json::Value>,
957) -> DistResult<()> {
958    let has_dist_metadata = metadata_table.and_then(|t| t.get(METADATA_DIST)).is_some();
959    if has_dist_metadata {
960        Err(DistError::UnusedMetadata {
961            manifest_path: manifest_path.to_owned(),
962            dist_manifest_path: dist_manifest_path.to_owned(),
963        })
964    } else {
965        Ok(())
966    }
967}
968
969pub(crate) fn parse_metadata_table(
970    manifest_path: &Utf8Path,
971    metadata_table: Option<&serde_json::Value>,
972) -> DistResult<DistMetadata> {
973    Ok(metadata_table
974        .and_then(|t| t.get(METADATA_DIST))
975        .map(DistMetadata::deserialize)
976        .transpose()
977        .map_err(|cause| DistError::CargoTomlParse {
978            manifest_path: manifest_path.to_owned(),
979            cause,
980        })?
981        .unwrap_or_default())
982}
983
984/// Find the dist workspaces relative to the current directory
985pub fn get_project() -> Result<axoproject::WorkspaceGraph, axoproject::errors::ProjectError> {
986    let start_dir = std::env::current_dir().expect("couldn't get current working dir!?");
987    let start_dir = Utf8PathBuf::from_path_buf(start_dir).expect("project path isn't utf8!?");
988    let repo = LocalRepo::new("git", &start_dir).ok();
989    let workspaces = axoproject::WorkspaceGraph::find_from_git(&start_dir, repo)?;
990    Ok(workspaces)
991}
992
993/// Load a TOML file to a toml-edit document.
994pub fn load_toml(manifest_path: &Utf8Path) -> DistResult<toml_edit::DocumentMut> {
995    let src = axoasset::SourceFile::load_local(manifest_path)?;
996    let toml = src.deserialize_toml_edit()?;
997    Ok(toml)
998}
999
1000/// Save a toml-edit document to a TOML file.
1001pub fn write_toml(manifest_path: &Utf8Path, toml: toml_edit::DocumentMut) -> DistResult<()> {
1002    let toml_text = toml.to_string();
1003    axoasset::LocalAsset::write_new(&toml_text, manifest_path)?;
1004    Ok(())
1005}
1006
1007/// Get the `[workspace.metadata]` or `[package.metadata]` (based on `is_workspace`)
1008pub fn get_toml_metadata(
1009    toml: &mut toml_edit::DocumentMut,
1010    is_workspace: bool,
1011) -> &mut toml_edit::Item {
1012    // Walk down/prepare the components...
1013    let root_key = if is_workspace { "workspace" } else { "package" };
1014    let workspace = toml[root_key].or_insert(toml_edit::table());
1015    if let Some(t) = workspace.as_table_mut() {
1016        t.set_implicit(true)
1017    }
1018    let metadata = workspace["metadata"].or_insert(toml_edit::table());
1019    if let Some(t) = metadata.as_table_mut() {
1020        t.set_implicit(true)
1021    }
1022
1023    metadata
1024}
1025
1026/// This module implements support for serializing and deserializing
1027/// `Option<Vec<T>>> where T: Display + FromStr`
1028/// when we want both of these syntaxes to be valid:
1029///
1030/// * install-path = "~/.mycompany"
1031/// * install-path = ["$MY_COMPANY", "~/.mycompany"]
1032///
1033/// Notable corners of roundtripping:
1034///
1035/// * `["one_elem"]`` will be force-rewritten as `"one_elem"` (totally equivalent and prettier)
1036/// * `[]` will be preserved as `[]` (it's semantically distinct from None when cascading config)
1037///
1038/// This is a variation on a documented serde idiom for "string or struct":
1039/// <https://serde.rs/string-or-struct.html>
1040mod opt_string_or_vec {
1041    use super::*;
1042    use serde::de::Error;
1043
1044    pub fn serialize<S, T>(v: &Option<Vec<T>>, s: S) -> Result<S::Ok, S::Error>
1045    where
1046        S: serde::Serializer,
1047        T: std::fmt::Display,
1048    {
1049        // If none, do none
1050        let Some(vec) = v else {
1051            return s.serialize_none();
1052        };
1053        // If one item, make it a string
1054        if vec.len() == 1 {
1055            s.serialize_str(&vec[0].to_string())
1056        // If many items (or zero), make it a list
1057        } else {
1058            let string_vec = Vec::from_iter(vec.iter().map(ToString::to_string));
1059            string_vec.serialize(s)
1060        }
1061    }
1062
1063    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<Vec<T>>, D::Error>
1064    where
1065        D: serde::Deserializer<'de>,
1066        T: std::str::FromStr,
1067        T::Err: std::fmt::Display,
1068    {
1069        struct StringOrVec<T>(std::marker::PhantomData<T>);
1070
1071        impl<'de, T> serde::de::Visitor<'de> for StringOrVec<T>
1072        where
1073            T: std::str::FromStr,
1074            T::Err: std::fmt::Display,
1075        {
1076            type Value = Option<Vec<T>>;
1077
1078            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1079                formatter.write_str("string or list of strings")
1080            }
1081
1082            // if none, return none
1083            fn visit_none<E>(self) -> Result<Self::Value, E>
1084            where
1085                E: Error,
1086            {
1087                Ok(None)
1088            }
1089
1090            // if string, parse it and make a single-element list
1091            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
1092            where
1093                E: Error,
1094            {
1095                Ok(Some(vec![s
1096                    .parse()
1097                    .map_err(|e| E::custom(format!("{e}")))?]))
1098            }
1099
1100            // if a sequence, parse the whole thing
1101            fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
1102            where
1103                S: serde::de::SeqAccess<'de>,
1104            {
1105                let vec: Vec<String> =
1106                    Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?;
1107                let parsed: Result<Vec<T>, S::Error> = vec
1108                    .iter()
1109                    .map(|s| s.parse::<T>().map_err(|e| S::Error::custom(format!("{e}"))))
1110                    .collect();
1111                Ok(Some(parsed?))
1112            }
1113        }
1114
1115        deserializer.deserialize_any(StringOrVec::<T>(std::marker::PhantomData))
1116    }
1117}