From 98e345674e4e744755aa16ea58ced718c12f69fa Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Thu, 25 Mar 2021 11:47:52 -0700 Subject: [PATCH 1/4] Move script to bottom of page. --- src/doc/src/reference/unstable.md | 64 +++++++++++++++---------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index b1ef069074c..0c46ddcbd32 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -1091,38 +1091,6 @@ The 2021 edition will set the default [resolver version] to "2". [edition]: ../../edition-guide/index.html [resolver version]: resolver.md#resolver-versions - - ### future incompat report * RFC: [#2834](https://github.com/rust-lang/rfcs/blob/master/text/2834-cargo-report-future-incompat.md) * rustc Tracking Issue: [#71249](https://github.com/rust-lang/rust/issues/71249) @@ -1181,3 +1149,35 @@ lowest precedence. Relative `path` dependencies in such a `[patch]` section are resolved relative to the configuration file they appear in. + + From 96a5642217f6e05e439c3e0d65afa65ee66b3a9b Mon Sep 17 00:00:00 2001 From: Eric Huss Date: Thu, 25 Mar 2021 12:56:24 -0700 Subject: [PATCH 2/4] Add `cargo config` subcommand. --- Cargo.toml | 1 + crates/cargo-test-support/src/lib.rs | 29 +- crates/cargo-test-support/src/publish.rs | 2 +- src/bin/cargo/commands/config.rs | 48 +++ src/bin/cargo/commands/logout.rs | 29 +- src/bin/cargo/commands/mod.rs | 3 + src/cargo/core/features.rs | 42 +- src/cargo/ops/cargo_config.rs | 314 ++++++++++++++ src/cargo/ops/mod.rs | 1 + src/cargo/util/config/de.rs | 58 +-- src/cargo/util/config/key.rs | 25 +- src/cargo/util/config/mod.rs | 242 +++++++++-- src/doc/src/reference/unstable.md | 17 + tests/testsuite/bad_config.rs | 8 +- tests/testsuite/cargo_config.rs | 510 +++++++++++++++++++++++ tests/testsuite/main.rs | 1 + 16 files changed, 1187 insertions(+), 143 deletions(-) create mode 100644 src/bin/cargo/commands/config.rs create mode 100644 src/cargo/ops/cargo_config.rs create mode 100644 tests/testsuite/cargo_config.rs diff --git a/Cargo.toml b/Cargo.toml index 12e4fa46487..56204e6e82c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ semver = { version = "0.10", features = ["serde"] } serde = { version = "1.0.123", features = ["derive"] } serde_ignored = "0.1.0" serde_json = { version = "1.0.30", features = ["raw_value"] } +shell-escape = "0.1.4" strip-ansi-escapes = "0.1.0" tar = { version = "0.4.26", default-features = false } tempfile = "3.0" diff --git a/crates/cargo-test-support/src/lib.rs b/crates/cargo-test-support/src/lib.rs index c0d05ce36ae..dfcb83809ca 100644 --- a/crates/cargo-test-support/src/lib.rs +++ b/crates/cargo-test-support/src/lib.rs @@ -1144,8 +1144,6 @@ impl Execs { } fn match_json(&self, expected: &str, line: &str) -> MatchResult { - let expected = self.normalize_matcher(expected); - let line = self.normalize_matcher(line); let actual = match line.parse() { Err(e) => return Err(format!("invalid json, {}:\n`{}`", e, line)), Ok(actual) => actual, @@ -1155,7 +1153,8 @@ impl Execs { Ok(expected) => expected, }; - find_json_mismatch(&expected, &actual) + let cwd = self.process_builder.as_ref().and_then(|p| p.get_cwd()); + find_json_mismatch(&expected, &actual, cwd) } fn diff_lines<'a>( @@ -1333,8 +1332,12 @@ fn lines_match_works() { /// as paths). You can use a `"{...}"` string literal as a wildcard for /// arbitrary nested JSON (useful for parts of object emitted by other programs /// (e.g., rustc) rather than Cargo itself). -pub fn find_json_mismatch(expected: &Value, actual: &Value) -> Result<(), String> { - match find_json_mismatch_r(expected, actual) { +pub fn find_json_mismatch( + expected: &Value, + actual: &Value, + cwd: Option<&Path>, +) -> Result<(), String> { + match find_json_mismatch_r(expected, actual, cwd) { Some((expected_part, actual_part)) => Err(format!( "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n", serde_json::to_string_pretty(expected).unwrap(), @@ -1349,12 +1352,21 @@ pub fn find_json_mismatch(expected: &Value, actual: &Value) -> Result<(), String fn find_json_mismatch_r<'a>( expected: &'a Value, actual: &'a Value, + cwd: Option<&Path>, ) -> Option<(&'a Value, &'a Value)> { use serde_json::Value::*; match (expected, actual) { (&Number(ref l), &Number(ref r)) if l == r => None, (&Bool(l), &Bool(r)) if l == r => None, - (&String(ref l), &String(ref r)) if lines_match(l, r) => None, + (&String(ref l), _) if l == "{...}" => None, + (&String(ref l), &String(ref r)) => { + let normalized = normalize_matcher(r, cwd); + if lines_match(l, &normalized) { + None + } else { + Some((expected, actual)) + } + } (&Array(ref l), &Array(ref r)) => { if l.len() != r.len() { return Some((expected, actual)); @@ -1362,7 +1374,7 @@ fn find_json_mismatch_r<'a>( l.iter() .zip(r.iter()) - .filter_map(|(l, r)| find_json_mismatch_r(l, r)) + .filter_map(|(l, r)| find_json_mismatch_r(l, r, cwd)) .next() } (&Object(ref l), &Object(ref r)) => { @@ -1373,12 +1385,11 @@ fn find_json_mismatch_r<'a>( l.values() .zip(r.values()) - .filter_map(|(l, r)| find_json_mismatch_r(l, r)) + .filter_map(|(l, r)| find_json_mismatch_r(l, r, cwd)) .next() } (&Null, &Null) => None, // Magic string literal `"{...}"` acts as wildcard for any sub-JSON. - (&String(ref l), _) if l == "{...}" => None, _ => Some((expected, actual)), } } diff --git a/crates/cargo-test-support/src/publish.rs b/crates/cargo-test-support/src/publish.rs index e66f1902762..6a4549f158a 100644 --- a/crates/cargo-test-support/src/publish.rs +++ b/crates/cargo-test-support/src/publish.rs @@ -76,7 +76,7 @@ fn _validate_upload( let actual_json = serde_json::from_slice(&json_bytes).expect("uploaded JSON should be valid"); let expected_json = serde_json::from_str(expected_json).expect("expected JSON does not parse"); - if let Err(e) = find_json_mismatch(&expected_json, &actual_json) { + if let Err(e) = find_json_mismatch(&expected_json, &actual_json, None) { panic!("{}", e); } diff --git a/src/bin/cargo/commands/config.rs b/src/bin/cargo/commands/config.rs new file mode 100644 index 00000000000..61938dfc2ca --- /dev/null +++ b/src/bin/cargo/commands/config.rs @@ -0,0 +1,48 @@ +use crate::command_prelude::*; +use cargo::ops::cargo_config; + +pub fn cli() -> App { + subcommand("config") + .about("Inspect configuration values") + .after_help("Run `cargo help config` for more detailed information.\n") + .setting(clap::AppSettings::SubcommandRequiredElseHelp) + .subcommand( + subcommand("get") + .arg(Arg::with_name("key").help("The config key to display")) + .arg( + opt("format", "Display format") + .possible_values(cargo_config::ConfigFormat::POSSIBLE_VALUES) + .default_value("toml"), + ) + .arg(opt( + "show-origin", + "Display where the config value is defined", + )) + .arg( + opt("merged", "Whether or not to merge config values") + .possible_values(&["yes", "no"]) + .default_value("yes"), + ), + ) +} + +pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult { + config + .cli_unstable() + .fail_if_stable_command(config, "config", 9301)?; + match args.subcommand() { + ("get", Some(args)) => { + let opts = cargo_config::GetOptions { + key: args.value_of("key"), + format: args.value_of("format").unwrap().parse()?, + show_origin: args.is_present("show-origin"), + merged: args.value_of("merged") == Some("yes"), + }; + cargo_config::get(config, &opts)?; + } + (cmd, _) => { + panic!("unexpected command `{}`", cmd) + } + } + Ok(()) +} diff --git a/src/bin/cargo/commands/logout.rs b/src/bin/cargo/commands/logout.rs index b3fc67958ce..417063b488d 100644 --- a/src/bin/cargo/commands/logout.rs +++ b/src/bin/cargo/commands/logout.rs @@ -1,6 +1,4 @@ use crate::command_prelude::*; -use anyhow::format_err; -use cargo::core::features; use cargo::ops; pub fn cli() -> App { @@ -12,29 +10,10 @@ pub fn cli() -> App { } pub fn exec(config: &mut Config, args: &ArgMatches<'_>) -> CliResult { - let unstable = config.cli_unstable(); - if !(unstable.credential_process || unstable.unstable_options) { - const SEE: &str = "See https://github.com/rust-lang/cargo/issues/8933 for more \ - information about the `cargo logout` command."; - if config.nightly_features_allowed { - return Err(format_err!( - "the `cargo logout` command is unstable, pass `-Z unstable-options` to enable it\n\ - {}", - SEE - ) - .into()); - } else { - return Err(format_err!( - "the `cargo logout` command is unstable, and only available on the \ - nightly channel of Cargo, but this is the `{}` channel\n\ - {}\n\ - {}", - features::channel(), - features::SEE_CHANNELS, - SEE - ) - .into()); - } + if !config.cli_unstable().credential_process { + config + .cli_unstable() + .fail_if_stable_command(config, "logout", 8933)?; } config.load_credentials()?; ops::registry_logout(config, args.value_of("registry").map(String::from))?; diff --git a/src/bin/cargo/commands/mod.rs b/src/bin/cargo/commands/mod.rs index 56ca19bb117..3351decfafa 100644 --- a/src/bin/cargo/commands/mod.rs +++ b/src/bin/cargo/commands/mod.rs @@ -6,6 +6,7 @@ pub fn builtin() -> Vec { build::cli(), check::cli(), clean::cli(), + config::cli(), describe_future_incompatibilities::cli(), doc::cli(), fetch::cli(), @@ -45,6 +46,7 @@ pub fn builtin_exec(cmd: &str) -> Option) -> Cli "build" => build::exec, "check" => check::exec, "clean" => clean::exec, + "config" => config::exec, "describe-future-incompatibilities" => describe_future_incompatibilities::exec, "doc" => doc::exec, "fetch" => fetch::exec, @@ -84,6 +86,7 @@ pub mod bench; pub mod build; pub mod check; pub mod clean; +pub mod config; pub mod describe_future_incompatibilities; pub mod doc; pub mod fetch; diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index b2030b2b201..67a32a9d532 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -765,9 +765,8 @@ impl CliUnstable { Ok(()) } - /// Generates an error if `-Z unstable-options` was not used. - /// Intended to be used when a user passes a command-line flag that - /// requires `-Z unstable-options`. + /// Generates an error if `-Z unstable-options` was not used for a new, + /// unstable command-line flag. pub fn fail_if_stable_opt(&self, flag: &str, issue: u32) -> CargoResult<()> { if !self.unstable_options { let see = format!( @@ -799,6 +798,43 @@ impl CliUnstable { } Ok(()) } + + /// Generates an error if `-Z unstable-options` was not used for a new, + /// unstable subcommand. + pub fn fail_if_stable_command( + &self, + config: &Config, + command: &str, + issue: u32, + ) -> CargoResult<()> { + if self.unstable_options { + return Ok(()); + } + let see = format!( + "See https://github.com/rust-lang/cargo/issues/{} for more \ + information about the `cargo {}` command.", + issue, command + ); + if config.nightly_features_allowed { + bail!( + "the `cargo {}` command is unstable, pass `-Z unstable-options` to enable it\n\ + {}", + command, + see + ); + } else { + bail!( + "the `cargo {}` command is unstable, and only available on the \ + nightly channel of Cargo, but this is the `{}` channel\n\ + {}\n\ + {}", + command, + channel(), + SEE_CHANNELS, + see + ); + } + } } /// Returns the current release channel ("stable", "beta", "nightly", "dev"). diff --git a/src/cargo/ops/cargo_config.rs b/src/cargo/ops/cargo_config.rs new file mode 100644 index 00000000000..13f92ae9d04 --- /dev/null +++ b/src/cargo/ops/cargo_config.rs @@ -0,0 +1,314 @@ +//! Implementation of `cargo config` subcommand. + +use crate::drop_println; +use crate::util::config::{Config, ConfigKey, ConfigValue as CV, Definition}; +use crate::util::errors::CargoResult; +use anyhow::{bail, format_err, Error}; +use serde_json::json; +use std::borrow::Cow; +use std::fmt; +use std::str::FromStr; + +pub enum ConfigFormat { + Toml, + Json, + JsonValue, +} + +impl ConfigFormat { + /// For clap. + pub const POSSIBLE_VALUES: &'static [&'static str] = &["toml", "json", "json-value"]; +} + +impl FromStr for ConfigFormat { + type Err = Error; + fn from_str(s: &str) -> CargoResult { + match s { + "toml" => Ok(ConfigFormat::Toml), + "json" => Ok(ConfigFormat::Json), + "json-value" => Ok(ConfigFormat::JsonValue), + f => bail!("unknown config format `{}`", f), + } + } +} + +impl fmt::Display for ConfigFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + ConfigFormat::Toml => write!(f, "toml"), + ConfigFormat::Json => write!(f, "json"), + ConfigFormat::JsonValue => write!(f, "json-value"), + } + } +} + +/// Options for `cargo config get`. +pub struct GetOptions<'a> { + pub key: Option<&'a str>, + pub format: ConfigFormat, + pub show_origin: bool, + pub merged: bool, +} + +pub fn get(config: &Config, opts: &GetOptions<'_>) -> CargoResult<()> { + if opts.show_origin { + if !matches!(opts.format, ConfigFormat::Toml) { + bail!( + "the `{}` format does not support --show-origin, try the `toml` format instead", + opts.format + ); + } + } + let key = match opts.key { + Some(key) => ConfigKey::from_str(key), + None => ConfigKey::new(), + }; + if opts.merged { + let cv = config + .get_cv_with_env(&key)? + .ok_or_else(|| format_err!("config value `{}` is not set", key))?; + match opts.format { + ConfigFormat::Toml => { + print_toml(config, opts, &key, &cv); + if let Some(env) = maybe_env(config, &key, &cv) { + print_toml_env(config, &env); + } + } + ConfigFormat::Json => print_json(config, &key, &cv, true), + ConfigFormat::JsonValue => print_json(config, &key, &cv, false), + } + } else { + match &opts.format { + ConfigFormat::Toml => print_toml_unmerged(config, opts, &key)?, + format => bail!( + "the `{}` format does not support --merged=no, try the `toml` format instead", + format + ), + } + } + Ok(()) +} + +/// Checks for environment variables that might be used. +fn maybe_env<'config>( + config: &'config Config, + key: &ConfigKey, + cv: &CV, +) -> Option> { + // Only fetching a table is unable to load env values. Leaf entries should + // work properly. + match cv { + CV::Table(_map, _def) => {} + _ => return None, + } + let mut env: Vec<_> = config + .env() + .iter() + .filter(|(env_key, _val)| env_key.starts_with(&format!("{}_", key.as_env_key()))) + .collect(); + env.sort_by_key(|x| x.0); + if env.is_empty() { + None + } else { + Some(env) + } +} + +fn print_toml(config: &Config, opts: &GetOptions<'_>, key: &ConfigKey, cv: &CV) { + let origin = |def: &Definition| -> String { + if !opts.show_origin { + return "".to_string(); + } + format!(" # {}", def) + }; + match cv { + CV::Boolean(val, def) => drop_println!(config, "{} = {}{}", key, val, origin(&def)), + CV::Integer(val, def) => drop_println!(config, "{} = {}{}", key, val, origin(&def)), + CV::String(val, def) => drop_println!( + config, + "{} = {}{}", + key, + toml::to_string(&val).unwrap(), + origin(&def) + ), + CV::List(vals, _def) => { + if opts.show_origin { + drop_println!(config, "{} = [", key); + for (val, def) in vals { + drop_println!(config, " {}, # {}", toml::to_string(&val).unwrap(), def); + } + drop_println!(config, "]"); + } else { + let vals: Vec<&String> = vals.iter().map(|x| &x.0).collect(); + drop_println!(config, "{} = {}", key, toml::to_string(&vals).unwrap()); + } + } + CV::Table(table, _def) => { + let mut key_vals: Vec<_> = table.into_iter().collect(); + key_vals.sort_by(|a, b| a.0.cmp(&b.0)); + for (table_key, val) in key_vals { + let mut subkey = key.clone(); + // push or push_sensitive shouldn't matter here, since this is + // not dealing with environment variables. + subkey.push(&table_key); + print_toml(config, opts, &subkey, val); + } + } + } +} + +fn print_toml_env(config: &Config, env: &[(&String, &String)]) { + drop_println!( + config, + "# The following environment variables may affect the loaded values." + ); + for (env_key, env_value) in env { + let val = shell_escape::escape(Cow::Borrowed(env_value)); + drop_println!(config, "# {}={}", env_key, val); + } +} + +fn print_json(config: &Config, key: &ConfigKey, cv: &CV, include_key: bool) { + let json_value = if key.is_root() || !include_key { + match cv { + CV::Boolean(val, _def) => json!(val), + CV::Integer(val, _def) => json!(val), + CV::String(val, _def) => json!(val), + CV::List(vals, _def) => { + let jvals: Vec<_> = vals.into_iter().map(|(val, _def)| json!(val)).collect(); + json!(jvals) + } + CV::Table(map, _def) => { + let mut root_table = json!({}); + for (key, val) in map { + json_add(&mut root_table, key, val); + } + root_table + } + } + } else { + let mut parts: Vec<_> = key.parts().collect(); + let last_part = parts.pop().unwrap(); + let mut root_table = json!({}); + // Create a JSON object with nested keys up to the value being displayed. + let mut table = &mut root_table; + for part in parts { + table[part] = json!({}); + table = table.get_mut(part).unwrap(); + } + json_add(table, last_part, cv); + root_table + }; + drop_println!(config, "{}", serde_json::to_string(&json_value).unwrap()); + + // Helper for recursively converting a CV to JSON. + fn json_add(table: &mut serde_json::Value, key: &str, cv: &CV) { + match cv { + CV::Boolean(val, _def) => table[key] = json!(val), + CV::Integer(val, _def) => table[key] = json!(val), + CV::String(val, _def) => table[key] = json!(val), + CV::List(vals, _def) => { + let jvals: Vec<_> = vals.into_iter().map(|(val, _def)| json!(val)).collect(); + table[key] = json!(jvals); + } + CV::Table(val, _def) => { + table + .as_object_mut() + .unwrap() + .insert(key.to_string(), json!({})); + let inner_table = &mut table[&key]; + for (t_key, t_cv) in val { + json_add(inner_table, t_key, t_cv); + } + } + } + } +} + +fn print_toml_unmerged(config: &Config, opts: &GetOptions<'_>, key: &ConfigKey) -> CargoResult<()> { + let print_table = |cv: &CV| { + drop_println!(config, "# {}", cv.definition()); + print_toml(config, opts, &ConfigKey::new(), cv); + drop_println!(config, ""); + }; + // This removes entries from the given CV so that all that remains is the + // given key. Returns false if no entries were found. + fn trim_cv(mut cv: &mut CV, key: &ConfigKey) -> CargoResult { + for (i, part) in key.parts().enumerate() { + match cv { + CV::Table(map, _def) => { + map.retain(|key, _value| key == part); + match map.get_mut(part) { + Some(val) => cv = val, + None => return Ok(false), + } + } + _ => { + let mut key_so_far = ConfigKey::new(); + for part in key.parts().take(i) { + key_so_far.push(part); + } + bail!( + "expected table for configuration key `{}`, \ + but found {} in {}", + key_so_far, + cv.desc(), + cv.definition() + ) + } + } + } + Ok(match cv { + CV::Table(map, _def) => !map.is_empty(), + _ => true, + }) + } + + let mut cli_args = config.cli_args_as_table()?; + if trim_cv(&mut cli_args, key)? { + print_table(&cli_args); + } + + // This slurps up some extra env vars that aren't technically part of the + // "config" (or are special-cased). I'm personally fine with just keeping + // them here, though it might be confusing. The vars I'm aware of: + // + // * CARGO + // * CARGO_HOME + // * CARGO_NAME + // * CARGO_EMAIL + // * CARGO_INCREMENTAL + // * CARGO_TARGET_DIR + // * CARGO_CACHE_RUSTC_INFO + // + // All of these except CARGO, CARGO_HOME, and CARGO_CACHE_RUSTC_INFO are + // actually part of the config, but they are special-cased in the code. + // + // TODO: It might be a good idea to teach the Config loader to support + // environment variable aliases so that these special cases are less + // special, and will just naturally get loaded as part of the config. + let mut env: Vec<_> = config + .env() + .iter() + .filter(|(env_key, _val)| env_key.starts_with(key.as_env_key())) + .collect(); + if !env.is_empty() { + env.sort_by_key(|x| x.0); + drop_println!(config, "# Environment variables"); + for (key, value) in env { + // Displaying this in "shell" syntax instead of TOML, since that + // somehow makes more sense to me. + let val = shell_escape::escape(Cow::Borrowed(value)); + drop_println!(config, "# {}={}", key, val); + } + drop_println!(config, ""); + } + + let unmerged = config.load_values_unmerged()?; + for mut cv in unmerged { + if trim_cv(&mut cv, key)? { + print_table(&cv); + } + } + Ok(()) +} diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 5b5894f991b..ee0ea06f11a 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -31,6 +31,7 @@ pub use self::vendor::{vendor, VendorOptions}; mod cargo_clean; mod cargo_compile; +pub mod cargo_config; mod cargo_doc; mod cargo_fetch; mod cargo_generate_lockfile; diff --git a/src/cargo/util/config/de.rs b/src/cargo/util/config/de.rs index a70cd0dce80..26b149c79e2 100644 --- a/src/cargo/util/config/de.rs +++ b/src/cargo/util/config/de.rs @@ -40,62 +40,6 @@ macro_rules! deserialize_method { }; } -impl<'config> Deserializer<'config> { - /// This is a helper for getting a CV from a file or env var. - /// - /// If this returns CV::List, then don't look at the value. Handling lists - /// is deferred to ConfigSeqAccess. - fn get_cv_with_env(&self) -> Result, ConfigError> { - // Determine if value comes from env, cli, or file, and merge env if - // possible. - let cv = self.config.get_cv(&self.key)?; - let env = self.config.env.get(self.key.as_env_key()); - let env_def = Definition::Environment(self.key.as_env_key().to_string()); - let use_env = match (&cv, env) { - (Some(cv), Some(_)) => env_def.is_higher_priority(cv.definition()), - (None, Some(_)) => true, - _ => false, - }; - - if !use_env { - return Ok(cv); - } - - // Future note: If you ever need to deserialize a non-self describing - // map type, this should implement a starts_with check (similar to how - // ConfigMapAccess does). - let env = env.unwrap(); - if env == "true" { - Ok(Some(CV::Boolean(true, env_def))) - } else if env == "false" { - Ok(Some(CV::Boolean(false, env_def))) - } else if let Ok(i) = env.parse::() { - Ok(Some(CV::Integer(i, env_def))) - } else if self.config.cli_unstable().advanced_env - && env.starts_with('[') - && env.ends_with(']') - { - // Parsing is deferred to ConfigSeqAccess. - Ok(Some(CV::List(Vec::new(), env_def))) - } else { - // Try to merge if possible. - match cv { - Some(CV::List(cv_list, _cv_def)) => { - // Merging is deferred to ConfigSeqAccess. - Ok(Some(CV::List(cv_list, env_def))) - } - _ => { - // Note: CV::Table merging is not implemented, as env - // vars do not support table values. In the future, we - // could check for `{}`, and interpret it as TOML if - // that seems useful. - Ok(Some(CV::String(env.to_string(), env_def))) - } - } - } - } -} - impl<'de, 'config> de::Deserializer<'de> for Deserializer<'config> { type Error = ConfigError; @@ -103,7 +47,7 @@ impl<'de, 'config> de::Deserializer<'de> for Deserializer<'config> { where V: de::Visitor<'de>, { - let cv = self.get_cv_with_env()?; + let cv = self.config.get_cv_with_env(&self.key)?; if let Some(cv) = cv { let res: (Result, Definition) = match cv { CV::Integer(i, def) => (visitor.visit_i64(i), def), diff --git a/src/cargo/util/config/key.rs b/src/cargo/util/config/key.rs index 09f812528f9..27a7600996f 100644 --- a/src/cargo/util/config/key.rs +++ b/src/cargo/util/config/key.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt; /// Key for a configuration variable. @@ -84,16 +85,32 @@ impl ConfigKey { } /// Returns an iterator of the key parts as strings. - pub(super) fn parts(&self) -> impl Iterator { + pub(crate) fn parts(&self) -> impl Iterator { self.parts.iter().map(|p| p.0.as_ref()) } + + /// Returns whether or not this is a key for the root table. + pub fn is_root(&self) -> bool { + self.parts.is_empty() + } } impl fmt::Display for ConfigKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Note: This is not a perfect TOML representation. This really should - // check if the parts should be quoted. - let parts: Vec<&str> = self.parts().collect(); + let parts: Vec<_> = self.parts().map(|part| escape_key_part(part)).collect(); parts.join(".").fmt(f) } } + +fn escape_key_part<'a>(part: &'a str) -> Cow<'a, str> { + let ok = part.chars().all(|c| match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => true, + _ => false, + }); + if ok { + Cow::Borrowed(part) + } else { + // This is a bit messy, but toml doesn't expose a function to do this. + Cow::Owned(toml::to_string(&toml::Value::String(part.to_string())).unwrap()) + } +} diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index 06452f1881a..8d7961f2f97 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -88,7 +88,7 @@ mod value; pub use value::{Definition, OptValue, Value}; mod key; -use key::ConfigKey; +pub use key::ConfigKey; mod path; pub use path::{ConfigRelativePath, PathAndArgs}; @@ -522,6 +522,14 @@ impl Config { fn get_cv(&self, key: &ConfigKey) -> CargoResult> { log::trace!("get cv {:?}", key); let vals = self.values()?; + if key.is_root() { + // Returning the entire root table (for example `cargo config get` + // with no key). The definition here shouldn't matter. + return Ok(Some(CV::Table( + vals.clone(), + Definition::Path(PathBuf::new()), + ))); + } let mut parts = key.parts().enumerate(); let mut val = match vals.get(parts.next().unwrap().1) { Some(val) => val, @@ -539,12 +547,14 @@ impl Config { | CV::String(_, def) | CV::List(_, def) | CV::Boolean(_, def) => { - let key_so_far: Vec<&str> = key.parts().take(i).collect(); + let mut key_so_far = ConfigKey::new(); + for part in key.parts().take(i) { + key_so_far.push(part); + } bail!( "expected table for configuration key `{}`, \ but found {} in {}", - // This join doesn't handle quoting properly. - key_so_far.join("."), + key_so_far, val.desc(), def ) @@ -554,11 +564,94 @@ impl Config { Ok(Some(val.clone())) } + /// This is a helper for getting a CV from a file or env var. + pub(crate) fn get_cv_with_env(&self, key: &ConfigKey) -> CargoResult> { + // Determine if value comes from env, cli, or file, and merge env if + // possible. + let cv = self.get_cv(key)?; + if key.is_root() { + // Root table can't have env value. + return Ok(cv); + } + let env = self.env.get(key.as_env_key()); + let env_def = Definition::Environment(key.as_env_key().to_string()); + let use_env = match (&cv, env) { + // Lists are always merged. + (Some(CV::List(..)), Some(_)) => true, + (Some(cv), Some(_)) => env_def.is_higher_priority(cv.definition()), + (None, Some(_)) => true, + _ => false, + }; + + if !use_env { + return Ok(cv); + } + + // Future note: If you ever need to deserialize a non-self describing + // map type, this should implement a starts_with check (similar to how + // ConfigMapAccess does). + let env = env.unwrap(); + if env == "true" { + Ok(Some(CV::Boolean(true, env_def))) + } else if env == "false" { + Ok(Some(CV::Boolean(false, env_def))) + } else if let Ok(i) = env.parse::() { + Ok(Some(CV::Integer(i, env_def))) + } else if self.cli_unstable().advanced_env && env.starts_with('[') && env.ends_with(']') { + match cv { + Some(CV::List(mut cv_list, cv_def)) => { + // Merge with config file. + self.get_env_list(key, &mut cv_list)?; + Ok(Some(CV::List(cv_list, cv_def))) + } + Some(cv) => { + // This can't assume StringList or UnmergedStringList. + // Return an error, which is the behavior of merging + // multiple config.toml files with the same scenario. + bail!( + "unable to merge array env for config `{}`\n\ + file: {:?}\n\ + env: {}", + key, + cv, + env + ); + } + None => { + let mut cv_list = Vec::new(); + self.get_env_list(key, &mut cv_list)?; + Ok(Some(CV::List(cv_list, env_def))) + } + } + } else { + // Try to merge if possible. + match cv { + Some(CV::List(mut cv_list, cv_def)) => { + // Merge with config file. + self.get_env_list(key, &mut cv_list)?; + Ok(Some(CV::List(cv_list, cv_def))) + } + _ => { + // Note: CV::Table merging is not implemented, as env + // vars do not support table values. In the future, we + // could check for `{}`, and interpret it as TOML if + // that seems useful. + Ok(Some(CV::String(env.to_string(), env_def))) + } + } + } + } + /// Helper primarily for testing. pub fn set_env(&mut self, env: HashMap) { self.env = env; } + /// Returns all environment variables. + pub(crate) fn env(&self) -> &HashMap { + &self.env + } + fn get_env(&self, key: &ConfigKey) -> Result, ConfigError> where T: FromStr, @@ -912,6 +1005,39 @@ impl Config { self.load_values_from(&self.cwd) } + pub(crate) fn load_values_unmerged(&self) -> CargoResult> { + let mut result = Vec::new(); + let mut seen = HashSet::new(); + let home = self.home_path.clone().into_path_unlocked(); + self.walk_tree(&self.cwd, &home, |path| { + let mut cv = self._load_file(path, &mut seen, false)?; + if self.cli_unstable().config_include { + self.load_unmerged_include(&mut cv, &mut seen, &mut result)?; + } + result.push(cv); + Ok(()) + }) + .chain_err(|| "could not load Cargo configuration")?; + Ok(result) + } + + fn load_unmerged_include( + &self, + cv: &mut CV, + seen: &mut HashSet, + output: &mut Vec, + ) -> CargoResult<()> { + let includes = self.include_paths(cv, false)?; + for (path, abs_path, def) in includes { + let mut cv = self + ._load_file(&abs_path, seen, false) + .chain_err(|| format!("failed to load config include `{}` from `{}`", path, def))?; + self.load_unmerged_include(&mut cv, seen, output)?; + output.push(cv); + } + Ok(()) + } + fn load_values_from(&self, path: &Path) -> CargoResult> { // This definition path is ignored, this is just a temporary container // representing the entire file. @@ -919,7 +1045,7 @@ impl Config { let home = self.home_path.clone().into_path_unlocked(); self.walk_tree(path, &home, |path| { - let value = self.load_file(path)?; + let value = self.load_file(path, true)?; cfg.merge(value, false) .chain_err(|| format!("failed to merge configuration at `{}`", path.display()))?; Ok(()) @@ -932,12 +1058,17 @@ impl Config { } } - fn load_file(&self, path: &Path) -> CargoResult { + fn load_file(&self, path: &Path, includes: bool) -> CargoResult { let mut seen = HashSet::new(); - self._load_file(path, &mut seen) + self._load_file(path, &mut seen, includes) } - fn _load_file(&self, path: &Path, seen: &mut HashSet) -> CargoResult { + fn _load_file( + &self, + path: &Path, + seen: &mut HashSet, + includes: bool, + ) -> CargoResult { if !seen.insert(path.to_path_buf()) { bail!( "config `include` cycle detected with path `{}`", @@ -954,8 +1085,11 @@ impl Config { path.display() ) })?; - let value = self.load_includes(value, seen)?; - Ok(value) + if includes { + self.load_includes(value, seen) + } else { + Ok(value) + } } /// Load any `include` files listed in the given `value`. @@ -965,33 +1099,15 @@ impl Config { /// `seen` is used to check for cyclic includes. fn load_includes(&self, mut value: CV, seen: &mut HashSet) -> CargoResult { // Get the list of files to load. - let includes = match &mut value { - CV::Table(table, _def) => match table.remove("include") { - Some(CV::String(s, def)) => vec![(s, def.clone())], - Some(CV::List(list, _def)) => list, - Some(other) => bail!( - "`include` expected a string or list, but found {} in `{}`", - other.desc(), - other.definition() - ), - None => { - return Ok(value); - } - }, - _ => unreachable!(), - }; + let includes = self.include_paths(&mut value, true)?; // Check unstable. if !self.cli_unstable().config_include { return Ok(value); } // Accumulate all values here. let mut root = CV::Table(HashMap::new(), value.definition().clone()); - for (path, def) in includes { - let abs_path = match &def { - Definition::Path(p) => p.parent().unwrap().join(&path), - Definition::Environment(_) | Definition::Cli => self.cwd().join(&path), - }; - self._load_file(&abs_path, seen) + for (path, abs_path, def) in includes { + self._load_file(&abs_path, seen, true) .and_then(|include| root.merge(include, true)) .chain_err(|| format!("failed to load config include `{}` from `{}`", path, def))?; } @@ -999,13 +1115,54 @@ impl Config { Ok(root) } - /// Add config arguments passed on the command line. - fn merge_cli_args(&mut self) -> CargoResult<()> { + /// Converts the `include` config value to a list of absolute paths. + fn include_paths( + &self, + cv: &mut CV, + remove: bool, + ) -> CargoResult> { + let abs = |path: &String, def: &Definition| -> (String, PathBuf, Definition) { + let abs_path = match def { + Definition::Path(p) => p.parent().unwrap().join(&path), + Definition::Environment(_) | Definition::Cli => self.cwd().join(&path), + }; + (path.to_string(), abs_path, def.clone()) + }; + let table = match cv { + CV::Table(table, _def) => table, + _ => unreachable!(), + }; + let owned; + let include = if remove { + owned = table.remove("include"); + owned.as_ref() + } else { + table.get("include") + }; + let includes = match include { + Some(CV::String(s, def)) => { + vec![abs(s, def)] + } + Some(CV::List(list, _def)) => list.iter().map(|(s, def)| abs(s, def)).collect(), + Some(other) => bail!( + "`include` expected a string or list, but found {} in `{}`", + other.desc(), + other.definition() + ), + None => { + return Ok(Vec::new()); + } + }; + Ok(includes) + } + + /// Parses the CLI config args and returns them as a table. + pub(crate) fn cli_args_as_table(&self) -> CargoResult { + let mut loaded_args = CV::Table(HashMap::new(), Definition::Cli); let cli_args = match &self.cli_config { Some(cli_args) => cli_args, - None => return Ok(()), + None => return Ok(loaded_args), }; - let mut loaded_args = CV::Table(HashMap::new(), Definition::Cli); for arg in cli_args { let arg_as_path = self.cwd.join(arg); let tmp_table = if !arg.is_empty() && arg_as_path.exists() { @@ -1044,13 +1201,18 @@ impl Config { .merge(tmp_table, true) .chain_err(|| format!("failed to merge --config argument `{}`", arg))?; } - // Force values to be loaded. - let _ = self.values()?; - let values = self.values_mut()?; - let loaded_map = match loaded_args { + Ok(loaded_args) + } + + /// Add config arguments passed on the command line. + fn merge_cli_args(&mut self) -> CargoResult<()> { + let loaded_map = match self.cli_args_as_table()? { CV::Table(table, _def) => table, _ => unreachable!(), }; + // Force values to be loaded. + let _ = self.values()?; + let values = self.values_mut()?; for (key, value) in loaded_map.into_iter() { match values.entry(key) { Vacant(entry) => { @@ -1187,7 +1349,7 @@ impl Config { None => return Ok(()), }; - let mut value = self.load_file(&credentials)?; + let mut value = self.load_file(&credentials, true)?; // Backwards compatibility for old `.cargo/credentials` layout. { let (value_map, def) = match value { diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 0c46ddcbd32..03a114af2b7 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -1150,6 +1150,23 @@ lowest precedence. Relative `path` dependencies in such a `[patch]` section are resolved relative to the configuration file they appear in. +## `cargo config` + +* Original Issue: [#2362](https://github.com/rust-lang/cargo/issues/2362) +* Tracking Issue: [#9301](https://github.com/rust-lang/cargo/issues/9301) + +The `cargo config` subcommand provides a way to display the configuration +files that cargo loads. It currently includes the `get` subcommand which +can take an optional config value to display. + +```console +cargo +nightly -Zunstable-options config get build.rustflags +``` + +If no config value is included, it will display all config values. See the +`--help` output for more options available. + +