Skip to content

Commit 0c29d43

Browse files
committed
Auto merge of #11077 - weihanglo:issue-10992, r=ehuss
Config file loaded via CLI takes priority over env vars ### What does this PR try to resolve? Fixes #10992 Behaviour changes: After this commit, config files loaded via CLI take priority over env vars. Config files loaded via [`config-include`] feature also get a higher priority over env vars, if the initial config file is being loaded via CLI. Cargo knows why it loads a specific config file with `WhyLoad` enum, and store in the field of `Definition::Cli(…)`. With this additional data, config files loaded via `--config <path>` get a `Definition::Cli(Some(…))`. In contrast, `--config <value>` with ordinary values become `Definition::Cli(None)`. The error message of the `Definition::Cli(Some(…))` is identical to `Definition::Path(…)`, so we won't lose any path information to debug. [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include ### How should we test and review this PR? Reviewing commit by commit is probably fine. The first two commits adding tests to `config-cli` and `config-include`, which exercises the expected behaviour we are going to fix in the next commits. To check the precedence, set up a project with an extra config file, such as ``` # Expect to have a target-dir named `foo` CARGO_BUILD_TARGET_DIR='env' cargo c --config 'build.target-dir = "cli"' --config .cargo/foo.toml # Inside .cargo/foo.toml [build] target-dir = "foo" ``` ### Additional information This is a bit hacky IMO. I don't like leaking the path info to `Definition::Cli`. However, it is really tricky to provide information for deserialization before values get merged.
2 parents 25c5ced + e0502e4 commit 0c29d43

File tree

5 files changed

+187
-36
lines changed

5 files changed

+187
-36
lines changed

src/cargo/util/config/de.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,13 @@ impl<'de, 'config> de::MapAccess<'de> for ValueDeserializer<'config> {
473473
Definition::Environment(env) => {
474474
seed.deserialize(Tuple2Deserializer(1i32, env.as_str()))
475475
}
476-
Definition::Cli => seed.deserialize(Tuple2Deserializer(2i32, "")),
476+
Definition::Cli(path) => {
477+
let str = path
478+
.as_ref()
479+
.map(|p| p.to_string_lossy())
480+
.unwrap_or_default();
481+
seed.deserialize(Tuple2Deserializer(2i32, str))
482+
}
477483
}
478484
}
479485
}

src/cargo/util/config/mod.rs

Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,20 @@ macro_rules! get_value_typed {
121121
};
122122
}
123123

124+
/// Indicates why a config value is being loaded.
125+
#[derive(Clone, Copy, Debug)]
126+
enum WhyLoad {
127+
/// Loaded due to a request from the global cli arg `--config`
128+
///
129+
/// Indirect configs loaded via [`config-include`] are also seen as from cli args,
130+
/// if the initial config is being loaded from cli.
131+
///
132+
/// [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include
133+
Cli,
134+
/// Loaded due to config file discovery.
135+
FileDiscovery,
136+
}
137+
124138
/// Configuration information for cargo. This is not specific to a build, it is information
125139
/// relating to cargo itself.
126140
#[derive(Debug)]
@@ -1005,12 +1019,15 @@ impl Config {
10051019
self.load_values_from(&self.cwd)
10061020
}
10071021

1022+
/// Like [`load_values`](Config::load_values) but without merging config values.
1023+
///
1024+
/// This is primarily crafted for `cargo config` command.
10081025
pub(crate) fn load_values_unmerged(&self) -> CargoResult<Vec<ConfigValue>> {
10091026
let mut result = Vec::new();
10101027
let mut seen = HashSet::new();
10111028
let home = self.home_path.clone().into_path_unlocked();
10121029
self.walk_tree(&self.cwd, &home, |path| {
1013-
let mut cv = self._load_file(path, &mut seen, false)?;
1030+
let mut cv = self._load_file(path, &mut seen, false, WhyLoad::FileDiscovery)?;
10141031
if self.cli_unstable().config_include {
10151032
self.load_unmerged_include(&mut cv, &mut seen, &mut result)?;
10161033
}
@@ -1021,6 +1038,9 @@ impl Config {
10211038
Ok(result)
10221039
}
10231040

1041+
/// Like [`load_includes`](Config::load_includes) but without merging config values.
1042+
///
1043+
/// This is primarily crafted for `cargo config` command.
10241044
fn load_unmerged_include(
10251045
&self,
10261046
cv: &mut CV,
@@ -1029,23 +1049,26 @@ impl Config {
10291049
) -> CargoResult<()> {
10301050
let includes = self.include_paths(cv, false)?;
10311051
for (path, abs_path, def) in includes {
1032-
let mut cv = self._load_file(&abs_path, seen, false).with_context(|| {
1033-
format!("failed to load config include `{}` from `{}`", path, def)
1034-
})?;
1052+
let mut cv = self
1053+
._load_file(&abs_path, seen, false, WhyLoad::FileDiscovery)
1054+
.with_context(|| {
1055+
format!("failed to load config include `{}` from `{}`", path, def)
1056+
})?;
10351057
self.load_unmerged_include(&mut cv, seen, output)?;
10361058
output.push(cv);
10371059
}
10381060
Ok(())
10391061
}
10401062

1063+
/// Start a config file discovery from a path and merges all config values found.
10411064
fn load_values_from(&self, path: &Path) -> CargoResult<HashMap<String, ConfigValue>> {
10421065
// This definition path is ignored, this is just a temporary container
10431066
// representing the entire file.
10441067
let mut cfg = CV::Table(HashMap::new(), Definition::Path(PathBuf::from(".")));
10451068
let home = self.home_path.clone().into_path_unlocked();
10461069

10471070
self.walk_tree(path, &home, |path| {
1048-
let value = self.load_file(path, true)?;
1071+
let value = self.load_file(path)?;
10491072
cfg.merge(value, false).with_context(|| {
10501073
format!("failed to merge configuration at `{}`", path.display())
10511074
})?;
@@ -1059,15 +1082,28 @@ impl Config {
10591082
}
10601083
}
10611084

1062-
fn load_file(&self, path: &Path, includes: bool) -> CargoResult<ConfigValue> {
1063-
self._load_file(path, &mut HashSet::new(), includes)
1085+
/// Loads a config value from a path.
1086+
///
1087+
/// This is used during config file discovery.
1088+
fn load_file(&self, path: &Path) -> CargoResult<ConfigValue> {
1089+
self._load_file(path, &mut HashSet::new(), true, WhyLoad::FileDiscovery)
10641090
}
10651091

1092+
/// Loads a config value from a path with options.
1093+
///
1094+
/// This is actual implementation of loading a config value from a path.
1095+
///
1096+
/// * `includes` determines whether to load configs from [`config-include`].
1097+
/// * `seen` is used to check for cyclic includes.
1098+
/// * `why_load` tells why a config is being loaded.
1099+
///
1100+
/// [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include
10661101
fn _load_file(
10671102
&self,
10681103
path: &Path,
10691104
seen: &mut HashSet<PathBuf>,
10701105
includes: bool,
1106+
why_load: WhyLoad,
10711107
) -> CargoResult<ConfigValue> {
10721108
if !seen.insert(path.to_path_buf()) {
10731109
bail!(
@@ -1080,15 +1116,18 @@ impl Config {
10801116
let toml = cargo_toml::parse(&contents, path, self).with_context(|| {
10811117
format!("could not parse TOML configuration in `{}`", path.display())
10821118
})?;
1083-
let value =
1084-
CV::from_toml(Definition::Path(path.to_path_buf()), toml).with_context(|| {
1085-
format!(
1086-
"failed to load TOML configuration from `{}`",
1087-
path.display()
1088-
)
1089-
})?;
1119+
let def = match why_load {
1120+
WhyLoad::Cli => Definition::Cli(Some(path.into())),
1121+
WhyLoad::FileDiscovery => Definition::Path(path.into()),
1122+
};
1123+
let value = CV::from_toml(def, toml).with_context(|| {
1124+
format!(
1125+
"failed to load TOML configuration from `{}`",
1126+
path.display()
1127+
)
1128+
})?;
10901129
if includes {
1091-
self.load_includes(value, seen)
1130+
self.load_includes(value, seen, why_load)
10921131
} else {
10931132
Ok(value)
10941133
}
@@ -1098,8 +1137,14 @@ impl Config {
10981137
///
10991138
/// Returns `value` with the given include files merged into it.
11001139
///
1101-
/// `seen` is used to check for cyclic includes.
1102-
fn load_includes(&self, mut value: CV, seen: &mut HashSet<PathBuf>) -> CargoResult<CV> {
1140+
/// * `seen` is used to check for cyclic includes.
1141+
/// * `why_load` tells why a config is being loaded.
1142+
fn load_includes(
1143+
&self,
1144+
mut value: CV,
1145+
seen: &mut HashSet<PathBuf>,
1146+
why_load: WhyLoad,
1147+
) -> CargoResult<CV> {
11031148
// Get the list of files to load.
11041149
let includes = self.include_paths(&mut value, true)?;
11051150
// Check unstable.
@@ -1109,7 +1154,7 @@ impl Config {
11091154
// Accumulate all values here.
11101155
let mut root = CV::Table(HashMap::new(), value.definition().clone());
11111156
for (path, abs_path, def) in includes {
1112-
self._load_file(&abs_path, seen, true)
1157+
self._load_file(&abs_path, seen, true, why_load)
11131158
.and_then(|include| root.merge(include, true))
11141159
.with_context(|| {
11151160
format!("failed to load config include `{}` from `{}`", path, def)
@@ -1127,8 +1172,8 @@ impl Config {
11271172
) -> CargoResult<Vec<(String, PathBuf, Definition)>> {
11281173
let abs = |path: &str, def: &Definition| -> (String, PathBuf, Definition) {
11291174
let abs_path = match def {
1130-
Definition::Path(p) => p.parent().unwrap().join(&path),
1131-
Definition::Environment(_) | Definition::Cli => self.cwd().join(&path),
1175+
Definition::Path(p) | Definition::Cli(Some(p)) => p.parent().unwrap().join(&path),
1176+
Definition::Environment(_) | Definition::Cli(None) => self.cwd().join(&path),
11321177
};
11331178
(path.to_string(), abs_path, def.clone())
11341179
};
@@ -1162,7 +1207,7 @@ impl Config {
11621207

11631208
/// Parses the CLI config args and returns them as a table.
11641209
pub(crate) fn cli_args_as_table(&self) -> CargoResult<ConfigValue> {
1165-
let mut loaded_args = CV::Table(HashMap::new(), Definition::Cli);
1210+
let mut loaded_args = CV::Table(HashMap::new(), Definition::Cli(None));
11661211
let cli_args = match &self.cli_config {
11671212
Some(cli_args) => cli_args,
11681213
None => return Ok(loaded_args),
@@ -1178,7 +1223,7 @@ impl Config {
11781223
anyhow::format_err!("config path {:?} is not utf-8", arg_as_path)
11791224
})?
11801225
.to_string();
1181-
self._load_file(&self.cwd().join(&str_path), &mut seen, true)
1226+
self._load_file(&self.cwd().join(&str_path), &mut seen, true, WhyLoad::Cli)
11821227
.with_context(|| format!("failed to load config from `{}`", str_path))?
11831228
} else {
11841229
// We only want to allow "dotted key" (see https://toml.io/en/v1.0.0#keys)
@@ -1273,11 +1318,11 @@ impl Config {
12731318
);
12741319
}
12751320

1276-
CV::from_toml(Definition::Cli, toml_v)
1321+
CV::from_toml(Definition::Cli(None), toml_v)
12771322
.with_context(|| format!("failed to convert --config argument `{arg}`"))?
12781323
};
12791324
let tmp_table = self
1280-
.load_includes(tmp_table, &mut HashSet::new())
1325+
.load_includes(tmp_table, &mut HashSet::new(), WhyLoad::Cli)
12811326
.with_context(|| "failed to load --config include".to_string())?;
12821327
loaded_args
12831328
.merge(tmp_table, true)
@@ -1431,7 +1476,7 @@ impl Config {
14311476
None => return Ok(()),
14321477
};
14331478

1434-
let mut value = self.load_file(&credentials, true)?;
1479+
let mut value = self.load_file(&credentials)?;
14351480
// Backwards compatibility for old `.cargo/credentials` layout.
14361481
{
14371482
let (value_map, def) = match value {

src/cargo/util/config/value.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ pub enum Definition {
5959
/// Defined in an environment variable, includes the environment key.
6060
Environment(String),
6161
/// Passed in on the command line.
62-
Cli,
62+
/// A path is attached when the config value is a path to a config file.
63+
Cli(Option<PathBuf>),
6364
}
6465

6566
impl Definition {
@@ -69,8 +70,8 @@ impl Definition {
6970
/// CLI and env are the current working directory.
7071
pub fn root<'a>(&'a self, config: &'a Config) -> &'a Path {
7172
match self {
72-
Definition::Path(p) => p.parent().unwrap().parent().unwrap(),
73-
Definition::Environment(_) | Definition::Cli => config.cwd(),
73+
Definition::Path(p) | Definition::Cli(Some(p)) => p.parent().unwrap().parent().unwrap(),
74+
Definition::Environment(_) | Definition::Cli(None) => config.cwd(),
7475
}
7576
}
7677

@@ -80,8 +81,8 @@ impl Definition {
8081
pub fn is_higher_priority(&self, other: &Definition) -> bool {
8182
matches!(
8283
(self, other),
83-
(Definition::Cli, Definition::Environment(_))
84-
| (Definition::Cli, Definition::Path(_))
84+
(Definition::Cli(_), Definition::Environment(_))
85+
| (Definition::Cli(_), Definition::Path(_))
8586
| (Definition::Environment(_), Definition::Path(_))
8687
)
8788
}
@@ -100,9 +101,9 @@ impl PartialEq for Definition {
100101
impl fmt::Display for Definition {
101102
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102103
match self {
103-
Definition::Path(p) => p.display().fmt(f),
104+
Definition::Path(p) | Definition::Cli(Some(p)) => p.display().fmt(f),
104105
Definition::Environment(key) => write!(f, "environment variable `{}`", key),
105-
Definition::Cli => write!(f, "--config cli option"),
106+
Definition::Cli(None) => write!(f, "--config cli option"),
106107
}
107108
}
108109
}
@@ -218,8 +219,11 @@ impl<'de> de::Deserialize<'de> for Definition {
218219
match discr {
219220
0 => Ok(Definition::Path(value.into())),
220221
1 => Ok(Definition::Environment(value)),
221-
2 => Ok(Definition::Cli),
222-
_ => panic!("unexpected discriminant {} value {}", discr, value),
222+
2 => {
223+
let path = (value.len() > 0).then_some(value.into());
224+
Ok(Definition::Cli(path))
225+
}
226+
_ => panic!("unexpected discriminant {discr} value {value}"),
223227
}
224228
}
225229
}

tests/testsuite/config_cli.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Tests for the --config CLI option.
22
3-
use super::config::{assert_error, assert_match, read_output, write_config, ConfigBuilder};
3+
use super::config::{
4+
assert_error, assert_match, read_output, write_config, write_config_at, ConfigBuilder,
5+
};
46
use cargo::util::config::Definition;
57
use cargo_test_support::paths;
68
use std::{collections::HashMap, fs};
@@ -53,6 +55,72 @@ fn cli_priority() {
5355
assert_eq!(config.get::<bool>("term.quiet").unwrap(), true);
5456
}
5557

58+
#[cargo_test]
59+
fn merge_primitives_for_multiple_cli_occurences() {
60+
let config_path0 = ".cargo/file0.toml";
61+
write_config_at(config_path0, "k = 'file0'");
62+
let config_path1 = ".cargo/file1.toml";
63+
write_config_at(config_path1, "k = 'file1'");
64+
65+
// k=env0
66+
let config = ConfigBuilder::new().env("CARGO_K", "env0").build();
67+
assert_eq!(config.get::<String>("k").unwrap(), "env0");
68+
69+
// k=env0
70+
// --config k='cli0'
71+
// --config k='cli1'
72+
let config = ConfigBuilder::new()
73+
.env("CARGO_K", "env0")
74+
.config_arg("k='cli0'")
75+
.config_arg("k='cli1'")
76+
.build();
77+
assert_eq!(config.get::<String>("k").unwrap(), "cli1");
78+
79+
// Env has a lower priority when comparing with file from CLI arg.
80+
//
81+
// k=env0
82+
// --config k='cli0'
83+
// --config k='cli1'
84+
// --config .cargo/file0.toml
85+
let config = ConfigBuilder::new()
86+
.env("CARGO_K", "env0")
87+
.config_arg("k='cli0'")
88+
.config_arg("k='cli1'")
89+
.config_arg(config_path0)
90+
.build();
91+
assert_eq!(config.get::<String>("k").unwrap(), "file0");
92+
93+
// k=env0
94+
// --config k='cli0'
95+
// --config k='cli1'
96+
// --config .cargo/file0.toml
97+
// --config k='cli2'
98+
let config = ConfigBuilder::new()
99+
.env("CARGO_K", "env0")
100+
.config_arg("k='cli0'")
101+
.config_arg("k='cli1'")
102+
.config_arg(config_path0)
103+
.config_arg("k='cli2'")
104+
.build();
105+
assert_eq!(config.get::<String>("k").unwrap(), "cli2");
106+
107+
// k=env0
108+
// --config k='cli0'
109+
// --config k='cli1'
110+
// --config .cargo/file0.toml
111+
// --config k='cli2'
112+
// --config .cargo/file1.toml
113+
let config = ConfigBuilder::new()
114+
.env("CARGO_K", "env0")
115+
.config_arg("k='cli0'")
116+
.config_arg("k='cli1'")
117+
.config_arg(config_path0)
118+
.config_arg("k='cli2'")
119+
.config_arg(config_path1)
120+
.build();
121+
assert_eq!(config.get::<String>("k").unwrap(), "file1");
122+
}
123+
56124
#[cargo_test]
57125
fn merges_array() {
58126
// Array entries are appended.

0 commit comments

Comments
 (0)