diff --git a/src/cargo/core/compiler/compile_kind.rs b/src/cargo/core/compiler/compile_kind.rs index 36b41c90e8a..100ae168281 100644 --- a/src/cargo/core/compiler/compile_kind.rs +++ b/src/cargo/core/compiler/compile_kind.rs @@ -114,7 +114,7 @@ impl CompileKind { let kinds = match (fallback, &gctx.build_config()?.target) { (_, None) | (CompileKindFallback::JustHost, _) => Ok(vec![CompileKind::Host]), (CompileKindFallback::BuildConfig, Some(build_target_config)) => { - dedup(&build_target_config.values(gctx)?) + dedup(&build_target_config.values(gctx.cwd())?) } }; diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index e964ecda067..aa16dacc926 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -540,7 +540,7 @@ pub fn add_overrides<'a>( // The path listed next to the string is the config file in which the // key was located, so we want to pop off the `.cargo/config` component // to get the directory containing the `.cargo` folder. - (paths::normalize_path(&def.root(gctx).join(s)), def) + (paths::normalize_path(&def.root(gctx.cwd()).join(s)), def) }); for (path, definition) in paths { diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index e0c758076fb..6d7e93b9a59 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -20,6 +20,10 @@ //! - [`StringList`]: Get a value that is either a list or a whitespace split //! string. //! +//! # Config schemas +//! +//! Configuration schemas are defined in the [`schema`] module. +//! //! ## Config deserialization //! //! Cargo uses a two-layer deserialization approach: @@ -87,6 +91,7 @@ use crate::util::network::http::configure_http_handle; use crate::util::network::http::http_handle; use crate::util::{CanonicalUrl, closest_msg, internal}; use crate::util::{Filesystem, IntoUrl, IntoUrlWithBase, Rustc}; + use annotate_snippets::Level; use anyhow::{Context as _, anyhow, bail, format_err}; use cargo_credential::Secret; @@ -96,7 +101,6 @@ use curl::easy::Easy; use itertools::Itertools; use serde::Deserialize; use serde::de::IntoDeserializer as _; -use serde_untagged::UntaggedEnumVisitor; use time::OffsetDateTime; use toml_edit::Item; use url::Url; @@ -123,6 +127,9 @@ pub use target::{TargetCfgConfig, TargetConfig}; mod environment; use environment::Env; +mod schema; +pub use schema::*; + use super::auth::RegistryConfig; /// Helper macro for creating typed access methods. @@ -999,7 +1006,7 @@ impl GlobalContext { fn string_to_path(&self, value: &str, definition: &Definition) -> PathBuf { let is_path = value.contains('/') || (cfg!(windows) && value.contains('\\')); if is_path { - definition.root(self).join(value) + definition.root(self.cwd()).join(value) } else { // A pathless name. PathBuf::from(value) @@ -1672,7 +1679,7 @@ impl GlobalContext { // This handles relative file: URLs, relative to the config definition. let base = index .definition - .root(self) + .root(self.cwd()) .join("truncated-by-url_with_base"); // Parse val to check it is a URL, not a relative path without a protocol. let _parsed = index.val.into_url()?; @@ -1918,7 +1925,7 @@ impl GlobalContext { .into_iter() .filter_map(|(k, v)| { if v.is_force() || self.get_env_os(&k).is_none() { - Some((k, v.resolve(self).to_os_string())) + Some((k, v.resolve(self.cwd()).to_os_string())) } else { None } @@ -2562,426 +2569,6 @@ impl ConfigInclude { } } -#[derive(Debug, Default, Deserialize, PartialEq)] -#[serde(rename_all = "kebab-case")] -pub struct CargoHttpConfig { - pub proxy: Option, - pub low_speed_limit: Option, - pub timeout: Option, - pub cainfo: Option, - pub proxy_cainfo: Option, - pub check_revoke: Option, - pub user_agent: Option, - pub debug: Option, - pub multiplexing: Option, - pub ssl_version: Option, -} - -#[derive(Debug, Default, Deserialize, PartialEq)] -#[serde(rename_all = "kebab-case")] -pub struct CargoFutureIncompatConfig { - frequency: Option, -} - -#[derive(Debug, Default, Deserialize, PartialEq)] -#[serde(rename_all = "kebab-case")] -pub enum CargoFutureIncompatFrequencyConfig { - #[default] - Always, - Never, -} - -impl CargoFutureIncompatConfig { - pub fn should_display_message(&self) -> bool { - use CargoFutureIncompatFrequencyConfig::*; - - let frequency = self.frequency.as_ref().unwrap_or(&Always); - match frequency { - Always => true, - Never => false, - } - } -} - -/// Configuration for `ssl-version` in `http` section -/// There are two ways to configure: -/// -/// ```text -/// [http] -/// ssl-version = "tlsv1.3" -/// ``` -/// -/// ```text -/// [http] -/// ssl-version.min = "tlsv1.2" -/// ssl-version.max = "tlsv1.3" -/// ``` -#[derive(Clone, Debug, PartialEq)] -pub enum SslVersionConfig { - Single(String), - Range(SslVersionConfigRange), -} - -impl<'de> Deserialize<'de> for SslVersionConfig { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .string(|single| Ok(SslVersionConfig::Single(single.to_owned()))) - .map(|map| map.deserialize().map(SslVersionConfig::Range)) - .deserialize(deserializer) - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "kebab-case")] -pub struct SslVersionConfigRange { - pub min: Option, - pub max: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct CargoNetConfig { - pub retry: Option, - pub offline: Option, - pub git_fetch_with_cli: Option, - pub ssh: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct CargoSshConfig { - pub known_hosts: Option>>, -} - -/// Configuration for `jobs` in `build` section. There are two -/// ways to configure: An integer or a simple string expression. -/// -/// ```toml -/// [build] -/// jobs = 1 -/// ``` -/// -/// ```toml -/// [build] -/// jobs = "default" # Currently only support "default". -/// ``` -#[derive(Debug, Clone)] -pub enum JobsConfig { - Integer(i32), - String(String), -} - -impl<'de> Deserialize<'de> for JobsConfig { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .i32(|int| Ok(JobsConfig::Integer(int))) - .string(|string| Ok(JobsConfig::String(string.to_owned()))) - .deserialize(deserializer) - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct CargoBuildConfig { - // deprecated, but preserved for compatibility - pub pipelining: Option, - pub dep_info_basedir: Option, - pub target_dir: Option, - pub build_dir: Option, - pub incremental: Option, - pub target: Option, - pub jobs: Option, - pub rustflags: Option, - pub rustdocflags: Option, - pub rustc_wrapper: Option, - pub rustc_workspace_wrapper: Option, - pub rustc: Option, - pub rustdoc: Option, - // deprecated alias for artifact-dir - pub out_dir: Option, - pub artifact_dir: Option, - pub warnings: Option, - /// Unstable feature `-Zsbom`. - pub sbom: Option, - /// Unstable feature `-Zbuild-analysis`. - pub analysis: Option, -} - -/// Metrics collection for build analysis. -#[derive(Debug, Deserialize, Default)] -#[serde(rename_all = "kebab-case")] -pub struct CargoBuildAnalysis { - pub enabled: bool, -} - -/// Whether warnings should warn, be allowed, or cause an error. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Default)] -#[serde(rename_all = "kebab-case")] -pub enum WarningHandling { - #[default] - /// Output warnings. - Warn, - /// Allow warnings (do not output them). - Allow, - /// Error if warnings are emitted. - Deny, -} - -/// Configuration for `build.target`. -/// -/// Accepts in the following forms: -/// -/// ```toml -/// target = "a" -/// target = ["a"] -/// target = ["a", "b"] -/// ``` -#[derive(Debug, Deserialize)] -#[serde(transparent)] -pub struct BuildTargetConfig { - inner: Value, -} - -#[derive(Debug)] -enum BuildTargetConfigInner { - One(String), - Many(Vec), -} - -impl<'de> Deserialize<'de> for BuildTargetConfigInner { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - UntaggedEnumVisitor::new() - .string(|one| Ok(BuildTargetConfigInner::One(one.to_owned()))) - .seq(|many| many.deserialize().map(BuildTargetConfigInner::Many)) - .deserialize(deserializer) - } -} - -impl BuildTargetConfig { - /// Gets values of `build.target` as a list of strings. - pub fn values(&self, gctx: &GlobalContext) -> CargoResult> { - let map = |s: &String| { - if s.ends_with(".json") { - // Path to a target specification file (in JSON). - // - self.inner - .definition - .root(gctx) - .join(s) - .to_str() - .expect("must be utf-8 in toml") - .to_string() - } else { - // A string. Probably a target triple. - s.to_string() - } - }; - let values = match &self.inner.val { - BuildTargetConfigInner::One(s) => vec![map(s)], - BuildTargetConfigInner::Many(v) => v.iter().map(map).collect(), - }; - Ok(values) - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct CargoResolverConfig { - pub incompatible_rust_versions: Option, - pub feature_unification: Option, -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum IncompatibleRustVersions { - Allow, - Fallback, -} - -#[derive(Copy, Clone, Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum FeatureUnification { - Package, - Selected, - Workspace, -} - -#[derive(Deserialize, Default)] -#[serde(rename_all = "kebab-case")] -pub struct TermConfig { - pub verbose: Option, - pub quiet: Option, - pub color: Option, - pub hyperlinks: Option, - pub unicode: Option, - #[serde(default)] - #[serde(deserialize_with = "progress_or_string")] - pub progress: Option, -} - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct ProgressConfig { - #[serde(default)] - pub when: ProgressWhen, - pub width: Option, - /// Communicate progress status with a terminal - pub term_integration: Option, -} - -#[derive(Debug, Default, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum ProgressWhen { - #[default] - Auto, - Never, - Always, -} - -fn progress_or_string<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::de::Deserializer<'de>, -{ - struct ProgressVisitor; - - impl<'de> serde::de::Visitor<'de> for ProgressVisitor { - type Value = Option; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("a string (\"auto\" or \"never\") or a table") - } - - fn visit_str(self, s: &str) -> Result - where - E: serde::de::Error, - { - match s { - "auto" => Ok(Some(ProgressConfig { - when: ProgressWhen::Auto, - width: None, - term_integration: None, - })), - "never" => Ok(Some(ProgressConfig { - when: ProgressWhen::Never, - width: None, - term_integration: None, - })), - "always" => Err(E::custom("\"always\" progress requires a `width` key")), - _ => Err(E::unknown_variant(s, &["auto", "never"])), - } - } - - fn visit_none(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } - - fn visit_some(self, deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - let pc = ProgressConfig::deserialize(deserializer)?; - if let ProgressConfig { - when: ProgressWhen::Always, - width: None, - .. - } = pc - { - return Err(serde::de::Error::custom( - "\"always\" progress requires a `width` key", - )); - } - Ok(Some(pc)) - } - } - - deserializer.deserialize_option(ProgressVisitor) -} - -#[derive(Debug)] -enum EnvConfigValueInner { - Simple(String), - WithOptions { - value: String, - force: bool, - relative: bool, - }, -} - -impl<'de> Deserialize<'de> for EnvConfigValueInner { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct WithOptions { - value: String, - #[serde(default)] - force: bool, - #[serde(default)] - relative: bool, - } - - UntaggedEnumVisitor::new() - .string(|simple| Ok(EnvConfigValueInner::Simple(simple.to_owned()))) - .map(|map| { - let with_options: WithOptions = map.deserialize()?; - Ok(EnvConfigValueInner::WithOptions { - value: with_options.value, - force: with_options.force, - relative: with_options.relative, - }) - }) - .deserialize(deserializer) - } -} - -#[derive(Debug, Deserialize)] -#[serde(transparent)] -pub struct EnvConfigValue { - inner: Value, -} - -impl EnvConfigValue { - pub fn is_force(&self) -> bool { - match self.inner.val { - EnvConfigValueInner::Simple(_) => false, - EnvConfigValueInner::WithOptions { force, .. } => force, - } - } - - pub fn resolve<'a>(&'a self, gctx: &GlobalContext) -> Cow<'a, OsStr> { - match self.inner.val { - EnvConfigValueInner::Simple(ref s) => Cow::Borrowed(OsStr::new(s.as_str())), - EnvConfigValueInner::WithOptions { - ref value, - relative, - .. - } => { - if relative { - let p = self.inner.definition.root(gctx).join(&value); - Cow::Owned(p.into_os_string()) - } else { - Cow::Borrowed(OsStr::new(value.as_str())) - } - } - } - } -} - -pub type EnvConfig = HashMap; - fn parse_document(toml: &str, _file: &Path, _gctx: &GlobalContext) -> CargoResult { // At the moment, no compatibility checks are needed. toml.parse().map_err(Into::into) diff --git a/src/cargo/util/context/path.rs b/src/cargo/util/context/path.rs index d69be6656a8..de582fa8808 100644 --- a/src/cargo/util/context/path.rs +++ b/src/cargo/util/context/path.rs @@ -30,7 +30,7 @@ impl ConfigRelativePath { /// This will always return an absolute path where it's relative to the /// location for configuration for this value. pub fn resolve_path(&self, gctx: &GlobalContext) -> PathBuf { - self.0.definition.root(gctx).join(&self.0.val) + self.0.definition.root(gctx.cwd()).join(&self.0.val) } /// Same as [`Self::resolve_path`] but will make string replacements @@ -72,7 +72,7 @@ impl ConfigRelativePath { }); } - Ok(self.0.definition.root(gctx).join(&value)) + Ok(self.0.definition.root(gctx.cwd()).join(&value)) } /// Resolves this configuration-relative path to either an absolute path or diff --git a/src/cargo/util/context/schema.rs b/src/cargo/util/context/schema.rs new file mode 100644 index 00000000000..bcbfc61d808 --- /dev/null +++ b/src/cargo/util/context/schema.rs @@ -0,0 +1,543 @@ +//! Cargo configuration schemas. +//! +//! This module contains types that define the schema for various configuration +//! sections found in Cargo configuration. +//! +//! These types are mostly used by [`GlobalContext::get`](super::GlobalContext::get) +//! to deserialize configuration values from TOML files, environment variables, +//! and CLI arguments. +//! +//! Schema types here should only contain data and simple accessor methods. +//! Avoid depending on [`GlobalContext`](super::GlobalContext) directly. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fmt; + +use serde::Deserialize; +use serde_untagged::UntaggedEnumVisitor; + +use std::path::Path; + +use crate::CargoResult; + +use super::StringList; +use super::Value; +use super::path::ConfigRelativePath; + +/// The `[http]` table. +/// +/// Example configuration: +/// +/// ```toml +/// [http] +/// proxy = "host:port" +/// timeout = 30 +/// cainfo = "/path/to/ca-bundle.crt" +/// check-revoke = true +/// multiplexing = true +/// ssl-version = "tlsv1.3" +/// ``` +#[derive(Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub struct CargoHttpConfig { + pub proxy: Option, + pub low_speed_limit: Option, + pub timeout: Option, + pub cainfo: Option, + pub proxy_cainfo: Option, + pub check_revoke: Option, + pub user_agent: Option, + pub debug: Option, + pub multiplexing: Option, + pub ssl_version: Option, +} + +/// The `[future-incompat-report]` stable +/// +/// Example configuration: +/// +/// ```toml +/// [future-incompat-report] +/// frequency = "always" +/// ``` +#[derive(Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub struct CargoFutureIncompatConfig { + frequency: Option, +} + +#[derive(Debug, Default, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum CargoFutureIncompatFrequencyConfig { + #[default] + Always, + Never, +} + +impl CargoFutureIncompatConfig { + pub fn should_display_message(&self) -> bool { + use CargoFutureIncompatFrequencyConfig::*; + + let frequency = self.frequency.as_ref().unwrap_or(&Always); + match frequency { + Always => true, + Never => false, + } + } +} + +/// Configuration for `ssl-version` in `http` section +/// There are two ways to configure: +/// +/// ```text +/// [http] +/// ssl-version = "tlsv1.3" +/// ``` +/// +/// ```text +/// [http] +/// ssl-version.min = "tlsv1.2" +/// ssl-version.max = "tlsv1.3" +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub enum SslVersionConfig { + Single(String), + Range(SslVersionConfigRange), +} + +impl<'de> Deserialize<'de> for SslVersionConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .string(|single| Ok(SslVersionConfig::Single(single.to_owned()))) + .map(|map| map.deserialize().map(SslVersionConfig::Range)) + .deserialize(deserializer) + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub struct SslVersionConfigRange { + pub min: Option, + pub max: Option, +} + +/// The `[net]` table. +/// +/// Example configuration: +/// +/// ```toml +/// [net] +/// retry = 2 +/// offline = false +/// git-fetch-with-cli = true +/// ``` +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct CargoNetConfig { + pub retry: Option, + pub offline: Option, + pub git_fetch_with_cli: Option, + pub ssh: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct CargoSshConfig { + pub known_hosts: Option>>, +} + +/// Configuration for `jobs` in `build` section. There are two +/// ways to configure: An integer or a simple string expression. +/// +/// ```toml +/// [build] +/// jobs = 1 +/// ``` +/// +/// ```toml +/// [build] +/// jobs = "default" # Currently only support "default". +/// ``` +#[derive(Debug, Clone)] +pub enum JobsConfig { + Integer(i32), + String(String), +} + +impl<'de> Deserialize<'de> for JobsConfig { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .i32(|int| Ok(JobsConfig::Integer(int))) + .string(|string| Ok(JobsConfig::String(string.to_owned()))) + .deserialize(deserializer) + } +} + +/// The `[build]` table. +/// +/// Example configuration: +/// +/// ```toml +/// [build] +/// jobs = 4 +/// target = "x86_64-unknown-linux-gnu" +/// target-dir = "target" +/// rustflags = ["-C", "link-arg=-fuse-ld=lld"] +/// incremental = true +/// ``` +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct CargoBuildConfig { + // deprecated, but preserved for compatibility + pub pipelining: Option, + pub dep_info_basedir: Option, + pub target_dir: Option, + pub build_dir: Option, + pub incremental: Option, + pub target: Option, + pub jobs: Option, + pub rustflags: Option, + pub rustdocflags: Option, + pub rustc_wrapper: Option, + pub rustc_workspace_wrapper: Option, + pub rustc: Option, + pub rustdoc: Option, + // deprecated alias for artifact-dir + pub out_dir: Option, + pub artifact_dir: Option, + pub warnings: Option, + /// Unstable feature `-Zsbom`. + pub sbom: Option, + /// Unstable feature `-Zbuild-analysis`. + pub analysis: Option, +} + +/// Metrics collection for build analysis. +#[derive(Debug, Deserialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct CargoBuildAnalysis { + pub enabled: bool, +} + +/// Whether warnings should warn, be allowed, or cause an error. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Default)] +#[serde(rename_all = "kebab-case")] +pub enum WarningHandling { + #[default] + /// Output warnings. + Warn, + /// Allow warnings (do not output them). + Allow, + /// Error if warnings are emitted. + Deny, +} + +/// Configuration for `build.target`. +/// +/// Accepts in the following forms: +/// +/// ```toml +/// target = "a" +/// target = ["a"] +/// target = ["a", "b"] +/// ``` +#[derive(Debug, Deserialize)] +#[serde(transparent)] +pub struct BuildTargetConfig { + inner: Value, +} + +#[derive(Debug)] +enum BuildTargetConfigInner { + One(String), + Many(Vec), +} + +impl<'de> Deserialize<'de> for BuildTargetConfigInner { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .string(|one| Ok(BuildTargetConfigInner::One(one.to_owned()))) + .seq(|many| many.deserialize().map(BuildTargetConfigInner::Many)) + .deserialize(deserializer) + } +} + +impl BuildTargetConfig { + /// Gets values of `build.target` as a list of strings. + pub fn values(&self, cwd: &Path) -> CargoResult> { + let map = |s: &String| { + if s.ends_with(".json") { + // Path to a target specification file (in JSON). + // + self.inner + .definition + .root(cwd) + .join(s) + .to_str() + .expect("must be utf-8 in toml") + .to_string() + } else { + // A string. Probably a target triple. + s.to_string() + } + }; + let values = match &self.inner.val { + BuildTargetConfigInner::One(s) => vec![map(s)], + BuildTargetConfigInner::Many(v) => v.iter().map(map).collect(), + }; + Ok(values) + } +} + +/// The `[resolver]` table. +/// +/// Example configuration: +/// +/// ```toml +/// [resolver] +/// incompatible-rust-versions = "fallback" +/// feature-unification = "workspace" +/// ``` +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct CargoResolverConfig { + pub incompatible_rust_versions: Option, + pub feature_unification: Option, +} + +#[derive(Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum IncompatibleRustVersions { + Allow, + Fallback, +} + +#[derive(Copy, Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum FeatureUnification { + Package, + Selected, + Workspace, +} + +/// The `[term]` table. +/// +/// Example configuration: +/// +/// ```toml +/// [term] +/// verbose = false +/// quiet = false +/// color = "auto" +/// progress.when = "auto" +/// ``` +#[derive(Deserialize, Default)] +#[serde(rename_all = "kebab-case")] +pub struct TermConfig { + pub verbose: Option, + pub quiet: Option, + pub color: Option, + pub hyperlinks: Option, + pub unicode: Option, + #[serde(default)] + #[serde(deserialize_with = "progress_or_string")] + pub progress: Option, +} + +/// The `term.progress` configuration. +/// +/// Example configuration: +/// +/// ```toml +/// [term] +/// progress.when = "never" # or "auto" +/// ``` +/// +/// ```toml +/// # `when = "always"` requires a `width` field +/// [term] +/// progress = { when = "always", width = 80 } +/// ``` +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ProgressConfig { + #[serde(default)] + pub when: ProgressWhen, + pub width: Option, + /// Communicate progress status with a terminal + pub term_integration: Option, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ProgressWhen { + #[default] + Auto, + Never, + Always, +} + +fn progress_or_string<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::de::Deserializer<'de>, +{ + struct ProgressVisitor; + + impl<'de> serde::de::Visitor<'de> for ProgressVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a string (\"auto\" or \"never\") or a table") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + match s { + "auto" => Ok(Some(ProgressConfig { + when: ProgressWhen::Auto, + width: None, + term_integration: None, + })), + "never" => Ok(Some(ProgressConfig { + when: ProgressWhen::Never, + width: None, + term_integration: None, + })), + "always" => Err(E::custom("\"always\" progress requires a `width` key")), + _ => Err(E::unknown_variant(s, &["auto", "never"])), + } + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + let pc = ProgressConfig::deserialize(deserializer)?; + if let ProgressConfig { + when: ProgressWhen::Always, + width: None, + .. + } = pc + { + return Err(serde::de::Error::custom( + "\"always\" progress requires a `width` key", + )); + } + Ok(Some(pc)) + } + } + + deserializer.deserialize_option(ProgressVisitor) +} + +#[derive(Debug)] +enum EnvConfigValueInner { + Simple(String), + WithOptions { + value: String, + force: bool, + relative: bool, + }, +} + +impl<'de> Deserialize<'de> for EnvConfigValueInner { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct WithOptions { + value: String, + #[serde(default)] + force: bool, + #[serde(default)] + relative: bool, + } + + UntaggedEnumVisitor::new() + .string(|simple| Ok(EnvConfigValueInner::Simple(simple.to_owned()))) + .map(|map| { + let with_options: WithOptions = map.deserialize()?; + Ok(EnvConfigValueInner::WithOptions { + value: with_options.value, + force: with_options.force, + relative: with_options.relative, + }) + }) + .deserialize(deserializer) + } +} + +/// Configuration value for environment variables in `[env]` section. +/// +/// Supports two formats: simple string and with options. +/// +/// ```toml +/// [env] +/// FOO = "value" +/// ``` +/// +/// ```toml +/// [env] +/// BAR = { value = "relative/path", relative = true } +/// BAZ = { value = "override", force = true } +/// ``` +#[derive(Debug, Deserialize)] +#[serde(transparent)] +pub struct EnvConfigValue { + inner: Value, +} + +impl EnvConfigValue { + /// Whether this value should override existing environment variables. + pub fn is_force(&self) -> bool { + match self.inner.val { + EnvConfigValueInner::Simple(_) => false, + EnvConfigValueInner::WithOptions { force, .. } => force, + } + } + + /// Resolves the environment variable value. + /// + /// If `relative = true`, + /// the value is interpreted as a [`ConfigRelativePath`]-like path. + pub fn resolve<'a>(&'a self, cwd: &Path) -> Cow<'a, OsStr> { + match self.inner.val { + EnvConfigValueInner::Simple(ref s) => Cow::Borrowed(OsStr::new(s.as_str())), + EnvConfigValueInner::WithOptions { + ref value, + relative, + .. + } => { + if relative { + let p = self.inner.definition.root(cwd).join(&value); + Cow::Owned(p.into_os_string()) + } else { + Cow::Borrowed(OsStr::new(value.as_str())) + } + } + } + } +} + +pub type EnvConfig = HashMap; diff --git a/src/cargo/util/context/value.rs b/src/cargo/util/context/value.rs index 0ea9eab8423..1e8bb3a5d5f 100644 --- a/src/cargo/util/context/value.rs +++ b/src/cargo/util/context/value.rs @@ -42,7 +42,6 @@ //! //! [`de`]: crate::util::context::de -use crate::util::context::GlobalContext; use serde::de; use std::cmp::Ordering; use std::fmt; @@ -104,12 +103,12 @@ impl Ord for Definition { impl Definition { /// Root directory where this is defined. /// - /// If from a file, it is the directory above `.cargo/config`. - /// CLI and env are the current working directory. - pub fn root<'a>(&'a self, gctx: &'a GlobalContext) -> &'a Path { + /// If from a file, it is the directory above `.cargo/config.toml`. + /// CLI and env use the provided current working directory. + pub fn root<'a>(&'a self, cwd: &'a Path) -> &'a Path { match self { Definition::Path(p) | Definition::Cli(Some(p)) => p.parent().unwrap().parent().unwrap(), - Definition::Environment(_) | Definition::Cli(None) | Definition::BuiltIn => gctx.cwd(), + Definition::Environment(_) | Definition::Cli(None) | Definition::BuiltIn => cwd, } }