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