Skip to main content

cargo/context/
schema.rs

1//! Cargo configuration schemas.
2//!
3//! This module contains types that define the schema for various configuration
4//! sections found in Cargo configuration.
5//!
6//! These types are mostly used by [`GlobalContext::get`](super::GlobalContext::get)
7//! to deserialize configuration values from TOML files, environment variables,
8//! and CLI arguments.
9//!
10//! Schema types here should only contain data and simple accessor methods.
11//! Avoid depending on [`GlobalContext`](super::GlobalContext) directly.
12
13use crate::sources::CRATES_IO_REGISTRY;
14use crate::util::data_structures::HashMap;
15use std::borrow::Cow;
16use std::ffi::OsStr;
17use std::str::FromStr;
18use std::{fmt, hash};
19
20use cargo_credential::Secret;
21use cargo_util_schemas::manifest::ProfileName;
22use serde::Deserialize;
23use serde::Serialize;
24use serde::de;
25use serde_untagged::UntaggedEnumVisitor;
26
27use std::path::Path;
28
29use crate::CargoResult;
30
31use super::OptValue;
32use super::PathAndArgs;
33use super::StringList;
34use super::Value;
35use super::path::ConfigRelativePath;
36
37#[derive(Debug, Default, Deserialize, PartialEq)]
38#[serde(rename_all = "kebab-case")]
39pub struct CargoCacheConfig {
40    /// How often to automatically clean unused cache data.
41    pub auto_clean_frequency: Option<String>,
42    /// Settings for cleaning the global cache.
43    pub global_clean: Option<GlobalCleanConfig>,
44}
45
46/// Cache cleaning settings from the `cache.global-clean` config table.
47///
48/// NOTE: Not all of these options may get stabilized. Some of them are very
49/// low-level details, and may not be something typical users need.
50///
51/// If any of these options are `None`, the built-in default is used.
52#[derive(Debug, Default, Deserialize, PartialEq)]
53#[serde(rename_all = "kebab-case")]
54pub struct GlobalCleanConfig {
55    /// Anything older than this duration will be deleted in the source cache.
56    pub max_src_age: Option<String>,
57    /// Anything older than this duration will be deleted in the compressed crate cache.
58    pub max_crate_age: Option<String>,
59    /// Any index older than this duration will be deleted from the index cache.
60    pub max_index_age: Option<String>,
61    /// Any git checkout older than this duration will be deleted from the checkout cache.
62    pub max_git_co_age: Option<String>,
63    /// Any git clone older than this duration will be deleted from the git cache.
64    pub max_git_db_age: Option<String>,
65}
66
67#[derive(Deserialize)]
68#[serde(rename_all = "kebab-case")]
69pub struct CargoNewConfig {
70    #[deprecated = "cargo-new no longer supports adding the authors field"]
71    #[expect(dead_code, reason = "deprecated")]
72    name: Option<String>,
73
74    #[deprecated = "cargo-new no longer supports adding the authors field"]
75    #[expect(dead_code, reason = "deprecated")]
76    email: Option<String>,
77
78    #[serde(rename = "vcs")]
79    pub version_control: Option<VersionControl>,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq)]
83pub enum VersionControl {
84    Git,
85    Hg,
86    Pijul,
87    Fossil,
88    NoVcs,
89}
90
91impl VersionControl {
92    pub const VALUES: &[Self] = &[Self::Git, Self::Hg, Self::Pijul, Self::Fossil, Self::NoVcs];
93
94    pub fn as_str(&self) -> &'static str {
95        match self {
96            VersionControl::Git => "git",
97            VersionControl::Hg => "hg",
98            VersionControl::Pijul => "pijul",
99            VersionControl::Fossil => "fossil",
100            VersionControl::NoVcs => "none",
101        }
102    }
103}
104
105impl FromStr for VersionControl {
106    type Err = anyhow::Error;
107
108    fn from_str(s: &str) -> Result<Self, anyhow::Error> {
109        match s {
110            "git" => Ok(VersionControl::Git),
111            "hg" => Ok(VersionControl::Hg),
112            "pijul" => Ok(VersionControl::Pijul),
113            "fossil" => Ok(VersionControl::Fossil),
114            "none" => Ok(VersionControl::NoVcs),
115            other => anyhow::bail!("unknown vcs specification: `{}`", other),
116        }
117    }
118}
119
120impl<'de> de::Deserialize<'de> for VersionControl {
121    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
122    where
123        D: de::Deserializer<'de>,
124    {
125        let s = String::deserialize(deserializer)?;
126        FromStr::from_str(&s).map_err(de::Error::custom)
127    }
128}
129
130/// Definition of a source in a config file.
131#[derive(Debug, Deserialize)]
132#[serde(rename_all = "kebab-case")]
133pub struct SourceConfigDef {
134    /// Indicates this source should be replaced with another of the given name.
135    pub replace_with: OptValue<String>,
136    /// A directory source.
137    pub directory: Option<ConfigRelativePath>,
138    /// A registry source. Value is a URL.
139    pub registry: OptValue<String>,
140    /// A local registry source.
141    pub local_registry: Option<ConfigRelativePath>,
142    /// A git source. Value is a URL.
143    pub git: OptValue<String>,
144    /// The git branch.
145    pub branch: OptValue<String>,
146    /// The git tag.
147    pub tag: OptValue<String>,
148    /// The git revision.
149    pub rev: OptValue<String>,
150}
151
152/// A map of registry names to URLs where documentations are hosted.
153/// This is for unstable feature [`-Zrustdoc-map`][1].
154///
155/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
156#[derive(serde::Deserialize, Debug)]
157#[serde(default)]
158pub struct RustdocExternMap {
159    #[serde(deserialize_with = "default_crates_io_to_docs_rs")]
160    /// * Key is the registry name in the configuration `[registries.<name>]`.
161    /// * Value is the URL where the documentation is hosted.
162    pub registries: HashMap<String, String>,
163    pub std: Option<RustdocExternMode>,
164}
165
166impl Default for RustdocExternMap {
167    fn default() -> Self {
168        Self {
169            registries: HashMap::from_iter([(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into())]),
170            std: None,
171        }
172    }
173}
174
175const DOCS_RS_URL: &'static str = "https://docs.rs/";
176
177fn default_crates_io_to_docs_rs<'de, D: serde::Deserializer<'de>>(
178    de: D,
179) -> Result<HashMap<String, String>, D::Error> {
180    let mut registries = HashMap::deserialize(de)?;
181    if !registries.contains_key(CRATES_IO_REGISTRY) {
182        registries.insert(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into());
183    }
184    Ok(registries)
185}
186
187impl hash::Hash for RustdocExternMap {
188    fn hash<H: hash::Hasher>(&self, into: &mut H) {
189        self.std.hash(into);
190        for (key, value) in &self.registries {
191            key.hash(into);
192            value.hash(into);
193        }
194    }
195}
196
197/// Mode used for `std`. This is for unstable feature [`-Zrustdoc-map`][1].
198///
199/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
200#[derive(Debug, Hash)]
201pub enum RustdocExternMode {
202    /// Use a local `file://` URL.
203    Local,
204    /// Use a remote URL to <https://doc.rust-lang.org/> (default).
205    Remote,
206    /// An arbitrary URL.
207    Url(String),
208}
209
210impl From<String> for RustdocExternMode {
211    fn from(s: String) -> RustdocExternMode {
212        match s.as_ref() {
213            "local" => RustdocExternMode::Local,
214            "remote" => RustdocExternMode::Remote,
215            _ => RustdocExternMode::Url(s),
216        }
217    }
218}
219
220impl fmt::Display for RustdocExternMode {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        match self {
223            RustdocExternMode::Local => "local".fmt(f),
224            RustdocExternMode::Remote => "remote".fmt(f),
225            RustdocExternMode::Url(s) => s.fmt(f),
226        }
227    }
228}
229
230impl<'de> serde::de::Deserialize<'de> for RustdocExternMode {
231    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
232    where
233        D: serde::de::Deserializer<'de>,
234    {
235        let s = String::deserialize(deserializer)?;
236        Ok(s.into())
237    }
238}
239
240/// The `[http]` table.
241///
242/// Example configuration:
243///
244/// ```toml
245/// [http]
246/// proxy = "host:port"
247/// timeout = 30
248/// cainfo = "/path/to/ca-bundle.crt"
249/// check-revoke = true
250/// multiplexing = true
251/// ssl-version = "tlsv1.3"
252/// ```
253#[derive(Debug, Default, Deserialize, PartialEq)]
254#[serde(rename_all = "kebab-case")]
255pub struct CargoHttpConfig {
256    pub proxy: Option<String>,
257    pub low_speed_limit: Option<u32>,
258    pub timeout: Option<u64>,
259    pub cainfo: Option<ConfigRelativePath>,
260    pub proxy_cainfo: Option<ConfigRelativePath>,
261    pub check_revoke: Option<bool>,
262    pub user_agent: Option<String>,
263    pub debug: Option<bool>,
264    pub multiplexing: Option<bool>,
265    pub ssl_version: Option<SslVersionConfig>,
266}
267
268/// The `[future-incompat-report]` stable
269///
270/// Example configuration:
271///
272/// ```toml
273/// [future-incompat-report]
274/// frequency = "always"
275/// ```
276#[derive(Debug, Default, Deserialize, PartialEq)]
277#[serde(rename_all = "kebab-case")]
278pub struct CargoFutureIncompatConfig {
279    frequency: Option<CargoFutureIncompatFrequencyConfig>,
280}
281
282#[derive(Debug, Default, Deserialize, PartialEq)]
283#[serde(rename_all = "kebab-case")]
284pub enum CargoFutureIncompatFrequencyConfig {
285    #[default]
286    Always,
287    Never,
288}
289
290impl CargoFutureIncompatConfig {
291    pub fn should_display_message(&self) -> bool {
292        use CargoFutureIncompatFrequencyConfig::*;
293
294        let frequency = self.frequency.as_ref().unwrap_or(&Always);
295        match frequency {
296            Always => true,
297            Never => false,
298        }
299    }
300}
301
302/// Configuration for `ssl-version` in `http` section
303/// There are two ways to configure:
304///
305/// ```text
306/// [http]
307/// ssl-version = "tlsv1.3"
308/// ```
309///
310/// ```text
311/// [http]
312/// ssl-version.min = "tlsv1.2"
313/// ssl-version.max = "tlsv1.3"
314/// ```
315#[derive(Clone, Debug, PartialEq)]
316pub enum SslVersionConfig {
317    Single(String),
318    Range(SslVersionConfigRange),
319}
320
321impl<'de> Deserialize<'de> for SslVersionConfig {
322    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
323    where
324        D: serde::Deserializer<'de>,
325    {
326        UntaggedEnumVisitor::new()
327            .string(|single| Ok(SslVersionConfig::Single(single.to_owned())))
328            .map(|map| map.deserialize().map(SslVersionConfig::Range))
329            .deserialize(deserializer)
330    }
331}
332
333#[derive(Clone, Debug, Deserialize, PartialEq)]
334#[serde(rename_all = "kebab-case")]
335pub struct SslVersionConfigRange {
336    pub min: Option<String>,
337    pub max: Option<String>,
338}
339
340/// The `[net]` table.
341///
342/// Example configuration:
343///
344/// ```toml
345/// [net]
346/// retry = 2
347/// offline = false
348/// git-fetch-with-cli = true
349/// ```
350#[derive(Debug, Deserialize)]
351#[serde(rename_all = "kebab-case")]
352pub struct CargoNetConfig {
353    pub retry: Option<u32>,
354    pub offline: Option<bool>,
355    pub git_fetch_with_cli: Option<bool>,
356    pub ssh: Option<CargoSshConfig>,
357}
358
359#[derive(Debug, Deserialize)]
360#[serde(rename_all = "kebab-case")]
361pub struct CargoSshConfig {
362    pub known_hosts: Option<Vec<Value<String>>>,
363}
364
365/// Configuration for `jobs` in `build` section. There are two
366/// ways to configure: An integer or a simple string expression.
367///
368/// ```toml
369/// [build]
370/// jobs = 1
371/// ```
372///
373/// ```toml
374/// [build]
375/// jobs = "default" # Currently only support "default".
376/// ```
377#[derive(Debug, Clone)]
378pub enum JobsConfig {
379    Integer(i32),
380    String(String),
381}
382
383impl<'de> Deserialize<'de> for JobsConfig {
384    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
385    where
386        D: serde::Deserializer<'de>,
387    {
388        UntaggedEnumVisitor::new()
389            .i32(|int| Ok(JobsConfig::Integer(int)))
390            .string(|string| Ok(JobsConfig::String(string.to_owned())))
391            .deserialize(deserializer)
392    }
393}
394
395/// The `[build]` table.
396///
397/// Example configuration:
398///
399/// ```toml
400/// [build]
401/// jobs = 4
402/// target = "x86_64-unknown-linux-gnu"
403/// target-dir = "target"
404/// rustflags = ["-C", "link-arg=-fuse-ld=lld"]
405/// incremental = true
406/// ```
407#[derive(Debug, Deserialize)]
408#[serde(rename_all = "kebab-case")]
409pub struct CargoBuildConfig {
410    // deprecated, but preserved for compatibility
411    pub pipelining: Option<bool>,
412    pub dep_info_basedir: Option<ConfigRelativePath>,
413    pub target_dir: Option<ConfigRelativePath>,
414    pub build_dir: Option<ConfigRelativePath>,
415    pub incremental: Option<bool>,
416    pub target: Option<BuildTargetConfig>,
417    pub profile: Option<ProfileName>,
418    pub jobs: Option<JobsConfig>,
419    pub rustflags: Option<StringList>,
420    pub rustdocflags: Option<StringList>,
421    pub rustc_wrapper: Option<ConfigRelativePath>,
422    pub rustc_workspace_wrapper: Option<ConfigRelativePath>,
423    pub rustc: Option<ConfigRelativePath>,
424    pub rustdoc: Option<ConfigRelativePath>,
425    pub artifact_dir: Option<ConfigRelativePath>,
426    pub warnings: Option<WarningHandling>,
427    /// Unstable feature `-Zsbom`.
428    pub sbom: Option<bool>,
429    /// Unstable feature `-Zbuild-analysis`.
430    pub analysis: Option<CargoBuildAnalysis>,
431    /// Unstable feature `-Zchecksum-freshness`.
432    pub fingerprint: Option<FingerprintMethod>,
433    pub build_std: Option<BuildStdConfig>,
434}
435
436/// Metrics collection for build analysis.
437#[derive(Debug, Deserialize, Default)]
438#[serde(rename_all = "kebab-case")]
439pub struct CargoBuildAnalysis {
440    pub enabled: bool,
441}
442
443/// Whether warnings should warn, be allowed, or cause an error.
444#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Default)]
445#[serde(rename_all = "kebab-case")]
446pub enum WarningHandling {
447    #[default]
448    /// Output warnings.
449    Warn,
450    /// Allow warnings (do not output them).
451    Allow,
452    /// Error if  warnings are emitted.
453    Deny,
454}
455
456#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Default)]
457#[serde(rename_all = "kebab-case")]
458pub enum FingerprintMethod {
459    #[default]
460    Mtime,
461    Content,
462}
463
464impl FingerprintMethod {
465    pub fn as_str(&self) -> &'static str {
466        match self {
467            Self::Mtime => "mtime",
468            Self::Content => "content",
469        }
470    }
471}
472
473impl std::fmt::Display for FingerprintMethod {
474    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475        self.as_str().fmt(f)
476    }
477}
478
479/// Configuration for `build.build-std`.
480#[derive(Debug, Deserialize)]
481#[serde(rename_all = "kebab-case")]
482pub struct BuildStdConfig {
483    pub when: Option<BuildStdWhen>,
484    pub crates: Option<BuildStdCrates>,
485}
486
487/// When to build the standard library.
488#[derive(Debug, Deserialize, PartialEq, Eq)]
489#[serde(rename_all = "kebab-case")]
490pub enum BuildStdWhen {
491    Never,
492    Always,
493}
494
495/// Which standard library crates to build.
496/// Valid values are "core", "alloc", "std", or an array of crate names
497#[derive(Debug, PartialEq, Eq)]
498pub enum BuildStdCrates {
499    Core,
500    Alloc,
501    Std,
502    // Custom list of crates - permanently unstable
503    List(Vec<String>),
504}
505
506impl<'de> Deserialize<'de> for BuildStdCrates {
507    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
508    where
509        D: de::Deserializer<'de>,
510    {
511        UntaggedEnumVisitor::new()
512            .string(|s| match s {
513                "core" => Ok(Self::Core),
514                "alloc" => Ok(Self::Alloc),
515                "std" => Ok(Self::Std),
516                _ => Err(serde::de::Error::unknown_variant(
517                    s,
518                    &["core", "alloc", "std"],
519                )),
520            })
521            .seq(|seq| seq.deserialize().map(Self::List))
522            .deserialize(deserializer)
523    }
524}
525
526/// Configuration for `build.target`.
527///
528/// Accepts in the following forms:
529///
530/// ```toml
531/// target = "a"
532/// target = ["a"]
533/// target = ["a", "b"]
534/// ```
535#[derive(Debug, Deserialize)]
536#[serde(transparent)]
537pub struct BuildTargetConfig {
538    inner: Value<BuildTargetConfigInner>,
539}
540
541#[derive(Debug)]
542enum BuildTargetConfigInner {
543    One(String),
544    Many(Vec<String>),
545}
546
547impl<'de> Deserialize<'de> for BuildTargetConfigInner {
548    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
549    where
550        D: serde::Deserializer<'de>,
551    {
552        UntaggedEnumVisitor::new()
553            .string(|one| Ok(BuildTargetConfigInner::One(one.to_owned())))
554            .seq(|many| many.deserialize().map(BuildTargetConfigInner::Many))
555            .deserialize(deserializer)
556    }
557}
558
559impl BuildTargetConfig {
560    /// Gets values of `build.target` as a list of strings.
561    pub fn values(&self, cwd: &Path) -> CargoResult<Vec<String>> {
562        let map = |s: &String| {
563            if s.ends_with(".json") {
564                // Path to a target specification file (in JSON).
565                // <https://doc.rust-lang.org/rustc/targets/custom.html>
566                self.inner
567                    .definition
568                    .root(cwd)
569                    .join(s)
570                    .to_str()
571                    .expect("must be utf-8 in toml")
572                    .to_string()
573            } else {
574                // A string. Probably a target tuple.
575                s.to_string()
576            }
577        };
578        let values = match &self.inner.val {
579            BuildTargetConfigInner::One(s) => vec![map(s)],
580            BuildTargetConfigInner::Many(v) => v.iter().map(map).collect(),
581        };
582        Ok(values)
583    }
584}
585
586/// The `[resolver]` table.
587///
588/// Example configuration:
589///
590/// ```toml
591/// [resolver]
592/// incompatible-rust-versions = "fallback"
593/// incompatible-publish-age = "deny"
594/// feature-unification = "workspace"
595/// lockfile-path = "my/Cargo.lock"
596/// ```
597#[derive(Debug, Deserialize)]
598#[serde(rename_all = "kebab-case")]
599pub struct CargoResolverConfig {
600    pub incompatible_rust_versions: Option<IncompatibleRustVersions>,
601    pub incompatible_publish_age: Option<IncompatiblePublishAge>,
602    pub feature_unification: Option<FeatureUnification>,
603    pub lockfile_path: Option<ConfigRelativePath>,
604}
605
606#[derive(Debug, Deserialize, PartialEq, Eq)]
607#[serde(rename_all = "kebab-case")]
608pub enum IncompatibleRustVersions {
609    Allow,
610    Fallback,
611}
612
613#[derive(Debug, Deserialize, PartialEq, Eq)]
614#[serde(rename_all = "kebab-case")]
615pub enum IncompatiblePublishAge {
616    Allow,
617    Deny,
618}
619
620#[derive(Copy, Clone, Debug, Deserialize)]
621#[serde(rename_all = "kebab-case")]
622pub enum FeatureUnification {
623    Package,
624    Selected,
625    Workspace,
626}
627
628/// The `[term]` table.
629///
630/// Example configuration:
631///
632/// ```toml
633/// [term]
634/// verbose = false
635/// quiet = false
636/// color = "auto"
637/// progress.when = "auto"
638/// ```
639#[derive(Debug, Deserialize, Default)]
640#[serde(rename_all = "kebab-case")]
641pub struct TermConfig {
642    pub verbose: Option<bool>,
643    pub quiet: Option<bool>,
644    pub color: Option<String>,
645    pub hyperlinks: Option<bool>,
646    pub unicode: Option<bool>,
647    pub progress: Option<ProgressConfig>,
648}
649
650/// The `term.progress` configuration.
651///
652/// Example configuration:
653///
654/// ```toml
655/// [term]
656/// progress.when = "never" # or "auto"
657/// ```
658///
659/// ```toml
660/// # `when = "always"` requires a `width` field
661/// [term]
662/// progress = { when = "always", width = 80 }
663/// ```
664#[derive(Debug, Default)]
665pub struct ProgressConfig {
666    pub when: ProgressWhen,
667    pub width: Option<usize>,
668    /// Communicate progress status with a terminal
669    pub term_integration: Option<bool>,
670}
671
672#[derive(Debug, Default, Deserialize)]
673#[serde(rename_all = "kebab-case")]
674pub enum ProgressWhen {
675    #[default]
676    Auto,
677    Never,
678    Always,
679}
680
681// We need this custom deserialization for validadting the rule of
682// `when = "always"` requiring a `width` field.
683impl<'de> Deserialize<'de> for ProgressConfig {
684    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
685    where
686        D: serde::Deserializer<'de>,
687    {
688        #[derive(Deserialize)]
689        #[serde(rename_all = "kebab-case")]
690        struct ProgressConfigInner {
691            #[serde(default)]
692            when: ProgressWhen,
693            width: Option<usize>,
694            term_integration: Option<bool>,
695        }
696
697        let pc = ProgressConfigInner::deserialize(deserializer)?;
698        if let ProgressConfigInner {
699            when: ProgressWhen::Always,
700            width: None,
701            ..
702        } = pc
703        {
704            return Err(serde::de::Error::custom(
705                "\"always\" progress requires a `width` key",
706            ));
707        }
708        Ok(ProgressConfig {
709            when: pc.when,
710            width: pc.width,
711            term_integration: pc.term_integration,
712        })
713    }
714}
715
716#[derive(Debug)]
717enum EnvConfigValueInner {
718    Simple(String),
719    WithOptions {
720        value: ConfigRelativePath,
721        force: bool,
722        relative: bool,
723    },
724}
725
726impl<'de> Deserialize<'de> for EnvConfigValueInner {
727    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
728    where
729        D: serde::Deserializer<'de>,
730    {
731        #[derive(Deserialize)]
732        struct WithOptions {
733            value: ConfigRelativePath,
734            #[serde(default)]
735            force: bool,
736            #[serde(default)]
737            relative: bool,
738        }
739
740        UntaggedEnumVisitor::new()
741            .string(|simple| Ok(EnvConfigValueInner::Simple(simple.to_owned())))
742            .map(|map| {
743                let with_options: WithOptions = map.deserialize()?;
744                Ok(EnvConfigValueInner::WithOptions {
745                    value: with_options.value,
746                    force: with_options.force,
747                    relative: with_options.relative,
748                })
749            })
750            .deserialize(deserializer)
751    }
752}
753
754/// Configuration value for environment variables in `[env]` section.
755///
756/// Supports two formats: simple string and with options.
757///
758/// ```toml
759/// [env]
760/// FOO = "value"
761/// ```
762///
763/// ```toml
764/// [env]
765/// BAR = { value = "relative/path", relative = true }
766/// BAZ = { value = "override", force = true }
767/// ```
768#[derive(Debug, Deserialize)]
769#[serde(transparent)]
770pub struct EnvConfigValue {
771    inner: EnvConfigValueInner,
772}
773
774impl EnvConfigValue {
775    /// Whether this value should override existing environment variables.
776    pub fn is_force(&self) -> bool {
777        match self.inner {
778            EnvConfigValueInner::Simple(_) => false,
779            EnvConfigValueInner::WithOptions { force, .. } => force,
780        }
781    }
782
783    /// Resolves the environment variable value.
784    ///
785    /// If `relative = true`,
786    /// the value is interpreted as a [`ConfigRelativePath`]-like path.
787    pub fn resolve<'a>(&'a self, cwd: &Path) -> Cow<'a, OsStr> {
788        match self.inner {
789            EnvConfigValueInner::Simple(ref s) => Cow::Borrowed(OsStr::new(s.as_str())),
790            EnvConfigValueInner::WithOptions {
791                ref value,
792                relative,
793                ..
794            } => {
795                if relative {
796                    let p = value.value().definition.root(cwd).join(value.raw_value());
797                    Cow::Owned(p.into_os_string())
798                } else {
799                    Cow::Borrowed(OsStr::new(value.raw_value()))
800                }
801            }
802        }
803    }
804}
805
806pub type EnvConfig = HashMap<String, EnvConfigValue>;
807
808/// Credentials for a registry.
809#[derive(Clone, Debug, Default, Deserialize)]
810#[serde(rename_all = "kebab-case")]
811pub struct RegistryCredential {
812    pub token: OptValue<Secret<String>>,
813}
814
815/// `[registries.NAME]` tables.
816///
817/// The values here should be kept in sync with `GlobalRegistryConfig`
818#[derive(Deserialize, Clone, Debug)]
819#[serde(rename_all = "kebab-case")]
820pub struct RegistryConfig {
821    pub index: Option<String>,
822    #[deprecated = "see `RegistryCredential::token` instead"]
823    pub token: OptValue<Secret<String>>,
824    pub credential_provider: Option<PathAndArgs>,
825    /// Minimum publish age threshold for RFC 3923
826    pub min_publish_age: Option<String>,
827    #[serde(rename = "protocol")]
828    _protocol: Option<String>,
829}
830
831/// The `[registry]` table, which has more keys than the `[registries.NAME]` tables.
832///
833/// Note: nesting `RegistryConfig` inside this struct and using `serde(flatten)` *should* work
834/// but fails with "invalid type: sequence, expected a value" when attempting to deserialize.
835#[derive(Deserialize)]
836#[serde(rename_all = "kebab-case")]
837pub struct GlobalRegistryConfig {
838    pub index: Option<String>,
839    #[deprecated = "see `RegistryCredential::token` instead"]
840    pub token: OptValue<Secret<String>>,
841    pub credential_provider: Option<PathAndArgs>,
842    /// Global default Minimum publish age threshold for RFC 3923
843    pub global_min_publish_age: Option<String>,
844    #[serde(rename = "default")]
845    _default: Option<String>,
846    #[serde(rename = "global-credential-providers")]
847    _global_credential_providers: Option<Vec<String>>,
848}
849
850impl GlobalRegistryConfig {
851    pub fn to_registry_config(self) -> RegistryConfig {
852        RegistryConfig {
853            index: self.index,
854            #[expect(deprecated)]
855            token: self.token,
856            credential_provider: self.credential_provider,
857            // `min-publish-age` is per-registry config only.
858            min_publish_age: None,
859            _protocol: None,
860        }
861    }
862}