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::util::data_structures::HashMap;
14use std::borrow::Cow;
15use std::ffi::OsStr;
16
17use cargo_credential::Secret;
18use serde::Deserialize;
19use serde_untagged::UntaggedEnumVisitor;
20
21use std::path::Path;
22
23use crate::CargoResult;
24
25use super::OptValue;
26use super::PathAndArgs;
27use super::StringList;
28use super::Value;
29use super::path::ConfigRelativePath;
30
31/// The `[http]` table.
32///
33/// Example configuration:
34///
35/// ```toml
36/// [http]
37/// proxy = "host:port"
38/// timeout = 30
39/// cainfo = "/path/to/ca-bundle.crt"
40/// check-revoke = true
41/// multiplexing = true
42/// ssl-version = "tlsv1.3"
43/// ```
44#[derive(Debug, Default, Deserialize, PartialEq)]
45#[serde(rename_all = "kebab-case")]
46pub struct CargoHttpConfig {
47    pub proxy: Option<String>,
48    pub low_speed_limit: Option<u32>,
49    pub timeout: Option<u64>,
50    pub cainfo: Option<ConfigRelativePath>,
51    pub proxy_cainfo: Option<ConfigRelativePath>,
52    pub check_revoke: Option<bool>,
53    pub user_agent: Option<String>,
54    pub debug: Option<bool>,
55    pub multiplexing: Option<bool>,
56    pub ssl_version: Option<SslVersionConfig>,
57}
58
59/// The `[future-incompat-report]` stable
60///
61/// Example configuration:
62///
63/// ```toml
64/// [future-incompat-report]
65/// frequency = "always"
66/// ```
67#[derive(Debug, Default, Deserialize, PartialEq)]
68#[serde(rename_all = "kebab-case")]
69pub struct CargoFutureIncompatConfig {
70    frequency: Option<CargoFutureIncompatFrequencyConfig>,
71}
72
73#[derive(Debug, Default, Deserialize, PartialEq)]
74#[serde(rename_all = "kebab-case")]
75pub enum CargoFutureIncompatFrequencyConfig {
76    #[default]
77    Always,
78    Never,
79}
80
81impl CargoFutureIncompatConfig {
82    pub fn should_display_message(&self) -> bool {
83        use CargoFutureIncompatFrequencyConfig::*;
84
85        let frequency = self.frequency.as_ref().unwrap_or(&Always);
86        match frequency {
87            Always => true,
88            Never => false,
89        }
90    }
91}
92
93/// Configuration for `ssl-version` in `http` section
94/// There are two ways to configure:
95///
96/// ```text
97/// [http]
98/// ssl-version = "tlsv1.3"
99/// ```
100///
101/// ```text
102/// [http]
103/// ssl-version.min = "tlsv1.2"
104/// ssl-version.max = "tlsv1.3"
105/// ```
106#[derive(Clone, Debug, PartialEq)]
107pub enum SslVersionConfig {
108    Single(String),
109    Range(SslVersionConfigRange),
110}
111
112impl<'de> Deserialize<'de> for SslVersionConfig {
113    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
114    where
115        D: serde::Deserializer<'de>,
116    {
117        UntaggedEnumVisitor::new()
118            .string(|single| Ok(SslVersionConfig::Single(single.to_owned())))
119            .map(|map| map.deserialize().map(SslVersionConfig::Range))
120            .deserialize(deserializer)
121    }
122}
123
124#[derive(Clone, Debug, Deserialize, PartialEq)]
125#[serde(rename_all = "kebab-case")]
126pub struct SslVersionConfigRange {
127    pub min: Option<String>,
128    pub max: Option<String>,
129}
130
131/// The `[net]` table.
132///
133/// Example configuration:
134///
135/// ```toml
136/// [net]
137/// retry = 2
138/// offline = false
139/// git-fetch-with-cli = true
140/// ```
141#[derive(Debug, Deserialize)]
142#[serde(rename_all = "kebab-case")]
143pub struct CargoNetConfig {
144    pub retry: Option<u32>,
145    pub offline: Option<bool>,
146    pub git_fetch_with_cli: Option<bool>,
147    pub ssh: Option<CargoSshConfig>,
148}
149
150#[derive(Debug, Deserialize)]
151#[serde(rename_all = "kebab-case")]
152pub struct CargoSshConfig {
153    pub known_hosts: Option<Vec<Value<String>>>,
154}
155
156/// Configuration for `jobs` in `build` section. There are two
157/// ways to configure: An integer or a simple string expression.
158///
159/// ```toml
160/// [build]
161/// jobs = 1
162/// ```
163///
164/// ```toml
165/// [build]
166/// jobs = "default" # Currently only support "default".
167/// ```
168#[derive(Debug, Clone)]
169pub enum JobsConfig {
170    Integer(i32),
171    String(String),
172}
173
174impl<'de> Deserialize<'de> for JobsConfig {
175    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
176    where
177        D: serde::Deserializer<'de>,
178    {
179        UntaggedEnumVisitor::new()
180            .i32(|int| Ok(JobsConfig::Integer(int)))
181            .string(|string| Ok(JobsConfig::String(string.to_owned())))
182            .deserialize(deserializer)
183    }
184}
185
186/// The `[build]` table.
187///
188/// Example configuration:
189///
190/// ```toml
191/// [build]
192/// jobs = 4
193/// target = "x86_64-unknown-linux-gnu"
194/// target-dir = "target"
195/// rustflags = ["-C", "link-arg=-fuse-ld=lld"]
196/// incremental = true
197/// ```
198#[derive(Debug, Deserialize)]
199#[serde(rename_all = "kebab-case")]
200pub struct CargoBuildConfig {
201    // deprecated, but preserved for compatibility
202    pub pipelining: Option<bool>,
203    pub dep_info_basedir: Option<ConfigRelativePath>,
204    pub target_dir: Option<ConfigRelativePath>,
205    pub build_dir: Option<ConfigRelativePath>,
206    pub incremental: Option<bool>,
207    pub target: Option<BuildTargetConfig>,
208    pub jobs: Option<JobsConfig>,
209    pub rustflags: Option<StringList>,
210    pub rustdocflags: Option<StringList>,
211    pub rustc_wrapper: Option<ConfigRelativePath>,
212    pub rustc_workspace_wrapper: Option<ConfigRelativePath>,
213    pub rustc: Option<ConfigRelativePath>,
214    pub rustdoc: Option<ConfigRelativePath>,
215    pub artifact_dir: Option<ConfigRelativePath>,
216    pub warnings: Option<WarningHandling>,
217    /// Unstable feature `-Zsbom`.
218    pub sbom: Option<bool>,
219    /// Unstable feature `-Zbuild-analysis`.
220    pub analysis: Option<CargoBuildAnalysis>,
221}
222
223/// Metrics collection for build analysis.
224#[derive(Debug, Deserialize, Default)]
225#[serde(rename_all = "kebab-case")]
226pub struct CargoBuildAnalysis {
227    pub enabled: bool,
228}
229
230/// Whether warnings should warn, be allowed, or cause an error.
231#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Default)]
232#[serde(rename_all = "kebab-case")]
233pub enum WarningHandling {
234    #[default]
235    /// Output warnings.
236    Warn,
237    /// Allow warnings (do not output them).
238    Allow,
239    /// Error if  warnings are emitted.
240    Deny,
241}
242
243/// Configuration for `build.target`.
244///
245/// Accepts in the following forms:
246///
247/// ```toml
248/// target = "a"
249/// target = ["a"]
250/// target = ["a", "b"]
251/// ```
252#[derive(Debug, Deserialize)]
253#[serde(transparent)]
254pub struct BuildTargetConfig {
255    inner: Value<BuildTargetConfigInner>,
256}
257
258#[derive(Debug)]
259enum BuildTargetConfigInner {
260    One(String),
261    Many(Vec<String>),
262}
263
264impl<'de> Deserialize<'de> for BuildTargetConfigInner {
265    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
266    where
267        D: serde::Deserializer<'de>,
268    {
269        UntaggedEnumVisitor::new()
270            .string(|one| Ok(BuildTargetConfigInner::One(one.to_owned())))
271            .seq(|many| many.deserialize().map(BuildTargetConfigInner::Many))
272            .deserialize(deserializer)
273    }
274}
275
276impl BuildTargetConfig {
277    /// Gets values of `build.target` as a list of strings.
278    pub fn values(&self, cwd: &Path) -> CargoResult<Vec<String>> {
279        let map = |s: &String| {
280            if s.ends_with(".json") {
281                // Path to a target specification file (in JSON).
282                // <https://doc.rust-lang.org/rustc/targets/custom.html>
283                self.inner
284                    .definition
285                    .root(cwd)
286                    .join(s)
287                    .to_str()
288                    .expect("must be utf-8 in toml")
289                    .to_string()
290            } else {
291                // A string. Probably a target triple.
292                s.to_string()
293            }
294        };
295        let values = match &self.inner.val {
296            BuildTargetConfigInner::One(s) => vec![map(s)],
297            BuildTargetConfigInner::Many(v) => v.iter().map(map).collect(),
298        };
299        Ok(values)
300    }
301}
302
303/// The `[resolver]` table.
304///
305/// Example configuration:
306///
307/// ```toml
308/// [resolver]
309/// incompatible-rust-versions = "fallback"
310/// incompatible-publish-age = "deny"
311/// feature-unification = "workspace"
312/// lockfile-path = "my/Cargo.lock"
313/// ```
314#[derive(Debug, Deserialize)]
315#[serde(rename_all = "kebab-case")]
316pub struct CargoResolverConfig {
317    pub incompatible_rust_versions: Option<IncompatibleRustVersions>,
318    pub incompatible_publish_age: Option<IncompatiblePublishAge>,
319    pub feature_unification: Option<FeatureUnification>,
320    pub lockfile_path: Option<ConfigRelativePath>,
321}
322
323#[derive(Debug, Deserialize, PartialEq, Eq)]
324#[serde(rename_all = "kebab-case")]
325pub enum IncompatibleRustVersions {
326    Allow,
327    Fallback,
328}
329
330#[derive(Debug, Deserialize, PartialEq, Eq)]
331#[serde(rename_all = "kebab-case")]
332pub enum IncompatiblePublishAge {
333    Allow,
334    Deny,
335}
336
337#[derive(Copy, Clone, Debug, Deserialize)]
338#[serde(rename_all = "kebab-case")]
339pub enum FeatureUnification {
340    Package,
341    Selected,
342    Workspace,
343}
344
345/// The `[term]` table.
346///
347/// Example configuration:
348///
349/// ```toml
350/// [term]
351/// verbose = false
352/// quiet = false
353/// color = "auto"
354/// progress.when = "auto"
355/// ```
356#[derive(Debug, Deserialize, Default)]
357#[serde(rename_all = "kebab-case")]
358pub struct TermConfig {
359    pub verbose: Option<bool>,
360    pub quiet: Option<bool>,
361    pub color: Option<String>,
362    pub hyperlinks: Option<bool>,
363    pub unicode: Option<bool>,
364    pub progress: Option<ProgressConfig>,
365}
366
367/// The `term.progress` configuration.
368///
369/// Example configuration:
370///
371/// ```toml
372/// [term]
373/// progress.when = "never" # or "auto"
374/// ```
375///
376/// ```toml
377/// # `when = "always"` requires a `width` field
378/// [term]
379/// progress = { when = "always", width = 80 }
380/// ```
381#[derive(Debug, Default)]
382pub struct ProgressConfig {
383    pub when: ProgressWhen,
384    pub width: Option<usize>,
385    /// Communicate progress status with a terminal
386    pub term_integration: Option<bool>,
387}
388
389#[derive(Debug, Default, Deserialize)]
390#[serde(rename_all = "kebab-case")]
391pub enum ProgressWhen {
392    #[default]
393    Auto,
394    Never,
395    Always,
396}
397
398// We need this custom deserialization for validadting the rule of
399// `when = "always"` requiring a `width` field.
400impl<'de> Deserialize<'de> for ProgressConfig {
401    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
402    where
403        D: serde::Deserializer<'de>,
404    {
405        #[derive(Deserialize)]
406        #[serde(rename_all = "kebab-case")]
407        struct ProgressConfigInner {
408            #[serde(default)]
409            when: ProgressWhen,
410            width: Option<usize>,
411            term_integration: Option<bool>,
412        }
413
414        let pc = ProgressConfigInner::deserialize(deserializer)?;
415        if let ProgressConfigInner {
416            when: ProgressWhen::Always,
417            width: None,
418            ..
419        } = pc
420        {
421            return Err(serde::de::Error::custom(
422                "\"always\" progress requires a `width` key",
423            ));
424        }
425        Ok(ProgressConfig {
426            when: pc.when,
427            width: pc.width,
428            term_integration: pc.term_integration,
429        })
430    }
431}
432
433#[derive(Debug)]
434enum EnvConfigValueInner {
435    Simple(String),
436    WithOptions {
437        value: ConfigRelativePath,
438        force: bool,
439        relative: bool,
440    },
441}
442
443impl<'de> Deserialize<'de> for EnvConfigValueInner {
444    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
445    where
446        D: serde::Deserializer<'de>,
447    {
448        #[derive(Deserialize)]
449        struct WithOptions {
450            value: ConfigRelativePath,
451            #[serde(default)]
452            force: bool,
453            #[serde(default)]
454            relative: bool,
455        }
456
457        UntaggedEnumVisitor::new()
458            .string(|simple| Ok(EnvConfigValueInner::Simple(simple.to_owned())))
459            .map(|map| {
460                let with_options: WithOptions = map.deserialize()?;
461                Ok(EnvConfigValueInner::WithOptions {
462                    value: with_options.value,
463                    force: with_options.force,
464                    relative: with_options.relative,
465                })
466            })
467            .deserialize(deserializer)
468    }
469}
470
471/// Configuration value for environment variables in `[env]` section.
472///
473/// Supports two formats: simple string and with options.
474///
475/// ```toml
476/// [env]
477/// FOO = "value"
478/// ```
479///
480/// ```toml
481/// [env]
482/// BAR = { value = "relative/path", relative = true }
483/// BAZ = { value = "override", force = true }
484/// ```
485#[derive(Debug, Deserialize)]
486#[serde(transparent)]
487pub struct EnvConfigValue {
488    inner: EnvConfigValueInner,
489}
490
491impl EnvConfigValue {
492    /// Whether this value should override existing environment variables.
493    pub fn is_force(&self) -> bool {
494        match self.inner {
495            EnvConfigValueInner::Simple(_) => false,
496            EnvConfigValueInner::WithOptions { force, .. } => force,
497        }
498    }
499
500    /// Resolves the environment variable value.
501    ///
502    /// If `relative = true`,
503    /// the value is interpreted as a [`ConfigRelativePath`]-like path.
504    pub fn resolve<'a>(&'a self, cwd: &Path) -> Cow<'a, OsStr> {
505        match self.inner {
506            EnvConfigValueInner::Simple(ref s) => Cow::Borrowed(OsStr::new(s.as_str())),
507            EnvConfigValueInner::WithOptions {
508                ref value,
509                relative,
510                ..
511            } => {
512                if relative {
513                    let p = value.value().definition.root(cwd).join(value.raw_value());
514                    Cow::Owned(p.into_os_string())
515                } else {
516                    Cow::Borrowed(OsStr::new(value.raw_value()))
517                }
518            }
519        }
520    }
521}
522
523pub type EnvConfig = HashMap<String, EnvConfigValue>;
524
525/// `[registries.NAME]` tables.
526///
527/// The values here should be kept in sync with `GlobalRegistryConfig`
528#[derive(Deserialize, Clone, Debug)]
529#[serde(rename_all = "kebab-case")]
530pub struct RegistryConfig {
531    pub index: Option<String>,
532    pub token: OptValue<Secret<String>>,
533    pub credential_provider: Option<PathAndArgs>,
534    pub secret_key: OptValue<Secret<String>>,
535    pub secret_key_subject: Option<String>,
536    /// Minimum publish age threshold for RFC 3923
537    pub min_publish_age: Option<String>,
538    #[serde(rename = "protocol")]
539    _protocol: Option<String>,
540}
541
542/// The `[registry]` table, which has more keys than the `[registries.NAME]` tables.
543///
544/// Note: nesting `RegistryConfig` inside this struct and using `serde(flatten)` *should* work
545/// but fails with "invalid type: sequence, expected a value" when attempting to deserialize.
546#[derive(Deserialize)]
547#[serde(rename_all = "kebab-case")]
548pub struct GlobalRegistryConfig {
549    pub index: Option<String>,
550    pub token: OptValue<Secret<String>>,
551    pub credential_provider: Option<PathAndArgs>,
552    pub secret_key: OptValue<Secret<String>>,
553    pub secret_key_subject: Option<String>,
554    /// Minimum publish age threshold for RFC 3923
555    pub min_publish_age: Option<String>,
556    /// Global default Minimum publish age threshold for RFC 3923
557    pub global_min_publish_age: Option<String>,
558    #[serde(rename = "default")]
559    _default: Option<String>,
560    #[serde(rename = "global-credential-providers")]
561    _global_credential_providers: Option<Vec<String>>,
562}
563
564impl GlobalRegistryConfig {
565    pub fn to_registry_config(self) -> RegistryConfig {
566        RegistryConfig {
567            index: self.index,
568            token: self.token,
569            credential_provider: self.credential_provider,
570            secret_key: self.secret_key,
571            secret_key_subject: self.secret_key_subject,
572            min_publish_age: self.min_publish_age,
573            _protocol: None,
574        }
575    }
576}