Skip to content

Commit f35ebaf

Browse files
committed
Auto merge of #8393 - ludumipsum:unstable_flags_in_config, r=alexcrichton
Allow configuring unstable flags via config file # Summary This fixes #8127 by mapping the `unstable` key in `.cargo/config` to Z flags. It should have no impact on stable/beta cargo, and on nightlies it gives folks the ability to configure Z flags for an entire project or workspace. This is meant to make it easier to try things like the [new features resolver](#8088) behavior, mtime-on-use, build-std, or timing reports across a whole project. I've also included a small (but entirely independent) ergonomics change -- treating dashes and underscores identically in Z flags. That's along for the ride in this PR as the last commit, and is included here because it makes for more idiomatic toml file keys (`print_im_a_teapot = yes` vs `print-im-a-teapot = yes`). Please let me know if y'all would prefer that be in a separate PR, or not happen at all. # Test Plan Apologies if I've missed anything -- this is my first cargo contrib and I've tried to hew to the contributing guide. If I've slipped up, please let me know how and I'll fix it. NB. My linux machine doesn't have multilib set up, so I disabled cross tests. * `cargo test` passes for each commit in the stack * I formatted each commit in the stack with `rustfmt` * New tests are included alongside the relevant change for each change * I've validated each test by locally undoing the code change they support and confirming failure. * The CLI wins, for both enable and disabling Z flags, as you'd expect. Keys in `unstable` which do not correspond with a Z flag will trigger an error indicating the invalid flag came from a config file read: ``` Invalid [unstable] entry in Cargo config Caused by: unknown `-Z` flag specified: an-invalid-flag ``` If you'd like to see a test case which isn't represented here, I'm happy to add it. Just let me know. # Documentation I've included commits in this stack updating the only docs page that seemed relevant to me, skimming the book -- `unstable.md`.
2 parents 0506d8d + d035daa commit f35ebaf

File tree

7 files changed

+246
-25
lines changed

7 files changed

+246
-25
lines changed

src/cargo/core/compiler/rustdoc.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ impl<'de> serde::de::Deserialize<'de> for RustdocExternMode {
5252
}
5353
}
5454

55-
#[derive(serde::Deserialize, Debug)]
55+
#[derive(serde::Deserialize, Debug, Default)]
56+
#[serde(default)]
5657
pub struct RustdocExternMap {
5758
registries: HashMap<String, String>,
5859
std: Option<RustdocExternMode>,

src/cargo/core/features.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,8 @@ impl Features {
333333
/// and then test for your flag or your value and act accordingly.
334334
///
335335
/// If you have any trouble with this, please let us know!
336-
#[derive(Default, Debug)]
336+
#[derive(Default, Debug, Deserialize)]
337+
#[serde(default, rename_all = "kebab-case")]
337338
pub struct CliUnstable {
338339
pub print_im_a_teapot: bool,
339340
pub unstable_options: bool,

src/cargo/util/config/de.rs

+34-16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::util::config::value;
44
use crate::util::config::{Config, ConfigError, ConfigKey};
55
use crate::util::config::{ConfigValue as CV, Definition, Value};
66
use serde::{de, de::IntoDeserializer};
7+
use std::collections::HashSet;
78
use std::vec;
89

910
/// Serde deserializer used to convert config values to a target type using
@@ -269,37 +270,54 @@ impl<'config> ConfigMapAccess<'config> {
269270

270271
fn new_struct(
271272
de: Deserializer<'config>,
272-
fields: &'static [&'static str],
273+
given_fields: &'static [&'static str],
273274
) -> Result<ConfigMapAccess<'config>, ConfigError> {
274-
let fields: Vec<KeyKind> = fields
275-
.iter()
276-
.map(|field| KeyKind::Normal(field.to_string()))
277-
.collect();
275+
let table = de.config.get_table(&de.key)?;
278276

279277
// Assume that if we're deserializing a struct it exhaustively lists all
280278
// possible fields on this key that we're *supposed* to use, so take
281279
// this opportunity to warn about any keys that aren't recognized as
282280
// fields and warn about them.
283-
if let Some(mut v) = de.config.get_table(&de.key)? {
284-
for (t_key, value) in v.val.drain() {
285-
if fields.iter().any(|k| match k {
286-
KeyKind::Normal(s) => s == &t_key,
287-
KeyKind::CaseSensitive(s) => s == &t_key,
288-
}) {
289-
continue;
290-
}
281+
if let Some(v) = table.as_ref() {
282+
let unused_keys = v
283+
.val
284+
.iter()
285+
.filter(|(k, _v)| !given_fields.iter().any(|gk| gk == k));
286+
for (unused_key, unused_value) in unused_keys {
291287
de.config.shell().warn(format!(
292288
"unused config key `{}.{}` in `{}`",
293289
de.key,
294-
t_key,
295-
value.definition()
290+
unused_key,
291+
unused_value.definition()
296292
))?;
297293
}
298294
}
299295

296+
let mut fields = HashSet::new();
297+
298+
// If the caller is interested in a field which we can provide from
299+
// the environment, get it from there.
300+
for field in given_fields {
301+
let mut field_key = de.key.clone();
302+
field_key.push(field);
303+
for env_key in de.config.env.keys() {
304+
if env_key.starts_with(field_key.as_env_key()) {
305+
fields.insert(KeyKind::Normal(field.to_string()));
306+
}
307+
}
308+
}
309+
310+
// Add everything from the config table we're interested in that we
311+
// haven't already provided via an environment variable
312+
if let Some(v) = table {
313+
for key in v.val.keys() {
314+
fields.insert(KeyKind::Normal(key.clone()));
315+
}
316+
}
317+
300318
Ok(ConfigMapAccess {
301319
de,
302-
fields,
320+
fields: fields.into_iter().collect(),
303321
field_index: 0,
304322
})
305323
}

src/cargo/util/config/mod.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -742,10 +742,17 @@ impl Config {
742742
.unwrap_or(false);
743743
self.target_dir = cli_target_dir;
744744

745+
// If nightly features are enabled, allow setting Z-flags from config
746+
// using the `unstable` table. Ignore that block otherwise.
745747
if nightly_features_allowed() {
746-
if let Some(val) = self.get::<Option<bool>>("unstable.mtime_on_use")? {
747-
self.unstable_flags.mtime_on_use |= val;
748+
if let Some(unstable_flags) = self.get::<Option<CliUnstable>>("unstable")? {
749+
self.unstable_flags = unstable_flags;
748750
}
751+
// NB. It's not ideal to parse these twice, but doing it again here
752+
// allows the CLI to override config files for both enabling
753+
// and disabling, and doing it up top allows CLI Zflags to
754+
// control config parsing behavior.
755+
self.unstable_flags.parse(unstable_flags)?;
749756
}
750757

751758
Ok(())

src/cargo/util/toml/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ impl<'de> de::Deserialize<'de> for U32OrBool {
394394
}
395395

396396
#[derive(Deserialize, Serialize, Clone, Debug, Default, Eq, PartialEq)]
397-
#[serde(rename_all = "kebab-case")]
397+
#[serde(default, rename_all = "kebab-case")]
398398
pub struct TomlProfile {
399399
pub opt_level: Option<TomlOptLevel>,
400400
pub lto: Option<StringOrBool>,

src/doc/src/reference/unstable.md

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ see a list of flags available.
77
`-Z unstable-options` is a generic flag for enabling other unstable
88
command-line flags. Options requiring this will be called out below.
99

10+
Anything which can be configured with a Z flag can also be set in the cargo
11+
config file (`.cargo/config.toml`) in the `unstable` table. For example:
12+
13+
```toml
14+
[unstable]
15+
mtime-on-use = 'yes'
16+
multitarget = 'yes'
17+
timings = 'yes'
18+
```
19+
1020
Some unstable features will require you to specify the `cargo-features` key in
1121
`Cargo.toml`.
1222

tests/testsuite/config.rs

+188-4
Original file line numberDiff line numberDiff line change
@@ -687,10 +687,7 @@ Caused by:
687687
f3: i64,
688688
big: i64,
689689
}
690-
assert_error(
691-
config.get::<S>("S").unwrap_err(),
692-
"missing config key `S.f3`",
693-
);
690+
assert_error(config.get::<S>("S").unwrap_err(), "missing field `f3`");
694691
}
695692

696693
#[cargo_test]
@@ -1094,6 +1091,78 @@ Caused by:
10941091
.is_none());
10951092
}
10961093

1094+
#[cargo_test]
1095+
/// Assert that unstable options can be configured with the `unstable` table in
1096+
/// cargo config files
1097+
fn unstable_table_notation() {
1098+
cargo::core::enable_nightly_features();
1099+
write_config(
1100+
"\
1101+
[unstable]
1102+
print-im-a-teapot = true
1103+
",
1104+
);
1105+
let config = ConfigBuilder::new().build();
1106+
assert_eq!(config.cli_unstable().print_im_a_teapot, true);
1107+
}
1108+
1109+
#[cargo_test]
1110+
/// Assert that dotted notation works for configuring unstable options
1111+
fn unstable_dotted_notation() {
1112+
cargo::core::enable_nightly_features();
1113+
write_config(
1114+
"\
1115+
unstable.print-im-a-teapot = true
1116+
",
1117+
);
1118+
let config = ConfigBuilder::new().build();
1119+
assert_eq!(config.cli_unstable().print_im_a_teapot, true);
1120+
}
1121+
1122+
#[cargo_test]
1123+
/// Assert that Zflags on the CLI take precedence over those from config
1124+
fn unstable_cli_precedence() {
1125+
cargo::core::enable_nightly_features();
1126+
write_config(
1127+
"\
1128+
unstable.print-im-a-teapot = true
1129+
",
1130+
);
1131+
let config = ConfigBuilder::new().build();
1132+
assert_eq!(config.cli_unstable().print_im_a_teapot, true);
1133+
1134+
let config = ConfigBuilder::new()
1135+
.unstable_flag("print-im-a-teapot=no")
1136+
.build();
1137+
assert_eq!(config.cli_unstable().print_im_a_teapot, false);
1138+
}
1139+
1140+
#[cargo_test]
1141+
/// Assert that atempting to set an unstable flag that doesn't exist via config
1142+
/// is ignored on stable
1143+
fn unstable_invalid_flag_ignored_on_stable() {
1144+
write_config(
1145+
"\
1146+
unstable.an-invalid-flag = 'yes'
1147+
",
1148+
);
1149+
assert!(ConfigBuilder::new().build_err().is_ok());
1150+
}
1151+
1152+
#[cargo_test]
1153+
/// Assert that unstable options can be configured with the `unstable` table in
1154+
/// cargo config files
1155+
fn unstable_flags_ignored_on_stable() {
1156+
write_config(
1157+
"\
1158+
[unstable]
1159+
print-im-a-teapot = true
1160+
",
1161+
);
1162+
let config = ConfigBuilder::new().build();
1163+
assert_eq!(config.cli_unstable().print_im_a_teapot, false);
1164+
}
1165+
10971166
#[cargo_test]
10981167
fn table_merge_failure() {
10991168
// Config::merge fails to merge entries in two tables.
@@ -1177,6 +1246,27 @@ fn struct_with_opt_inner_struct() {
11771246
assert_eq!(f.inner.unwrap().value.unwrap(), 12);
11781247
}
11791248

1249+
#[cargo_test]
1250+
fn struct_with_default_inner_struct() {
1251+
// Struct with serde defaults.
1252+
// Check that can be defined with environment variable.
1253+
#[derive(Deserialize, Default)]
1254+
#[serde(default)]
1255+
struct Inner {
1256+
value: i32,
1257+
}
1258+
#[derive(Deserialize, Default)]
1259+
#[serde(default)]
1260+
struct Foo {
1261+
inner: Inner,
1262+
}
1263+
let config = ConfigBuilder::new()
1264+
.env("CARGO_FOO_INNER_VALUE", "12")
1265+
.build();
1266+
let f: Foo = config.get("foo").unwrap();
1267+
assert_eq!(f.inner.value, 12);
1268+
}
1269+
11801270
#[cargo_test]
11811271
fn overlapping_env_config() {
11821272
// Issue where one key is a prefix of another.
@@ -1208,6 +1298,100 @@ fn overlapping_env_config() {
12081298
assert_eq!(s.debug, Some(1));
12091299
}
12101300

1301+
#[cargo_test]
1302+
fn overlapping_env_with_defaults_errors_out() {
1303+
// Issue where one key is a prefix of another.
1304+
// This is a limitation of mapping environment variables on to a hierarchy.
1305+
// Check that we error out when we hit ambiguity in this way, rather than
1306+
// the more-surprising defaulting through.
1307+
// If, in the future, we can handle this more correctly, feel free to delete
1308+
// this test.
1309+
#[derive(Deserialize, Default)]
1310+
#[serde(default, rename_all = "kebab-case")]
1311+
struct Ambig {
1312+
debug: u32,
1313+
debug_assertions: bool,
1314+
}
1315+
let config = ConfigBuilder::new()
1316+
.env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true")
1317+
.build();
1318+
let err = config.get::<Ambig>("ambig").err().unwrap();
1319+
assert!(format!("{}", err).contains("missing config key `ambig.debug`"));
1320+
1321+
let config = ConfigBuilder::new().env("CARGO_AMBIG_DEBUG", "5").build();
1322+
let s: Ambig = config.get("ambig").unwrap();
1323+
assert_eq!(s.debug_assertions, bool::default());
1324+
assert_eq!(s.debug, 5);
1325+
1326+
let config = ConfigBuilder::new()
1327+
.env("CARGO_AMBIG_DEBUG", "1")
1328+
.env("CARGO_AMBIG_DEBUG_ASSERTIONS", "true")
1329+
.build();
1330+
let s: Ambig = config.get("ambig").unwrap();
1331+
assert_eq!(s.debug_assertions, true);
1332+
assert_eq!(s.debug, 1);
1333+
}
1334+
1335+
#[cargo_test]
1336+
fn struct_with_overlapping_inner_struct_and_defaults() {
1337+
// Struct with serde defaults.
1338+
// Check that can be defined with environment variable.
1339+
#[derive(Deserialize, Default)]
1340+
#[serde(default)]
1341+
struct Inner {
1342+
value: i32,
1343+
}
1344+
1345+
// Containing struct with a prefix of inner
1346+
//
1347+
// This is a limitation of mapping environment variables on to a hierarchy.
1348+
// Check that we error out when we hit ambiguity in this way, rather than
1349+
// the more-surprising defaulting through.
1350+
// If, in the future, we can handle this more correctly, feel free to delete
1351+
// this case.
1352+
#[derive(Deserialize, Default)]
1353+
#[serde(default)]
1354+
struct PrefixContainer {
1355+
inn: bool,
1356+
inner: Inner,
1357+
}
1358+
let config = ConfigBuilder::new()
1359+
.env("CARGO_PREFIXCONTAINER_INNER_VALUE", "12")
1360+
.build();
1361+
let err = config
1362+
.get::<PrefixContainer>("prefixcontainer")
1363+
.err()
1364+
.unwrap();
1365+
assert!(format!("{}", err).contains("missing config key `prefixcontainer.inn`"));
1366+
let config = ConfigBuilder::new()
1367+
.env("CARGO_PREFIXCONTAINER_INNER_VALUE", "12")
1368+
.env("CARGO_PREFIXCONTAINER_INN", "true")
1369+
.build();
1370+
let f: PrefixContainer = config.get("prefixcontainer").unwrap();
1371+
assert_eq!(f.inner.value, 12);
1372+
assert_eq!(f.inn, true);
1373+
1374+
// Containing struct where the inner value's field is a prefix of another
1375+
//
1376+
// This is a limitation of mapping environment variables on to a hierarchy.
1377+
// Check that we error out when we hit ambiguity in this way, rather than
1378+
// the more-surprising defaulting through.
1379+
// If, in the future, we can handle this more correctly, feel free to delete
1380+
// this case.
1381+
#[derive(Deserialize, Default)]
1382+
#[serde(default)]
1383+
struct InversePrefixContainer {
1384+
inner_field: bool,
1385+
inner: Inner,
1386+
}
1387+
let config = ConfigBuilder::new()
1388+
.env("CARGO_INVERSEPREFIXCONTAINER_INNER_VALUE", "12")
1389+
.build();
1390+
let f: InversePrefixContainer = config.get("inverseprefixcontainer").unwrap();
1391+
assert_eq!(f.inner_field, bool::default());
1392+
assert_eq!(f.inner.value, 12);
1393+
}
1394+
12111395
#[cargo_test]
12121396
fn string_list_tricky_env() {
12131397
// Make sure StringList handles typed env values.

0 commit comments

Comments
 (0)