Skip to main content

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