Skip to content

Commit 7a7cbe0

Browse files
committed
Override config.toml options from command line
1 parent 3603a84 commit 7a7cbe0

File tree

6 files changed

+303
-47
lines changed

6 files changed

+303
-47
lines changed

src/bootstrap/config.rs

+124-26
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ impl SplitDebuginfo {
349349
}
350350

351351
/// LTO mode used for compiling rustc itself.
352-
#[derive(Default, Clone, PartialEq)]
352+
#[derive(Default, Clone, PartialEq, Debug)]
353353
pub enum RustcLto {
354354
Off,
355355
#[default]
@@ -507,29 +507,42 @@ struct TomlConfig {
507507
profile: Option<String>,
508508
}
509509

510+
/// Describes how to handle conflicts in merging two [`TomlConfig`]
511+
#[derive(Copy, Clone, Debug)]
512+
enum ReplaceOpt {
513+
/// Silently ignore a duplicated value
514+
IgnoreDuplicate,
515+
/// Override the current value, even if it's `Some`
516+
Override,
517+
/// Exit with an error on duplicate values
518+
ErrorOnDuplicate,
519+
}
520+
510521
trait Merge {
511-
fn merge(&mut self, other: Self);
522+
fn merge(&mut self, other: Self, replace: ReplaceOpt);
512523
}
513524

514525
impl Merge for TomlConfig {
515526
fn merge(
516527
&mut self,
517-
TomlConfig { build, install, llvm, rust, dist, target, profile: _, changelog_seen: _ }: Self,
528+
TomlConfig { build, install, llvm, rust, dist, target, profile: _, changelog_seen }: Self,
529+
replace: ReplaceOpt,
518530
) {
519-
fn do_merge<T: Merge>(x: &mut Option<T>, y: Option<T>) {
531+
fn do_merge<T: Merge>(x: &mut Option<T>, y: Option<T>, replace: ReplaceOpt) {
520532
if let Some(new) = y {
521533
if let Some(original) = x {
522-
original.merge(new);
534+
original.merge(new, replace);
523535
} else {
524536
*x = Some(new);
525537
}
526538
}
527539
}
528-
do_merge(&mut self.build, build);
529-
do_merge(&mut self.install, install);
530-
do_merge(&mut self.llvm, llvm);
531-
do_merge(&mut self.rust, rust);
532-
do_merge(&mut self.dist, dist);
540+
self.changelog_seen.merge(changelog_seen, replace);
541+
do_merge(&mut self.build, build, replace);
542+
do_merge(&mut self.install, install, replace);
543+
do_merge(&mut self.llvm, llvm, replace);
544+
do_merge(&mut self.rust, rust, replace);
545+
do_merge(&mut self.dist, dist, replace);
533546
assert!(target.is_none(), "merging target-specific config is not currently supported");
534547
}
535548
}
@@ -546,10 +559,33 @@ macro_rules! define_config {
546559
}
547560

548561
impl Merge for $name {
549-
fn merge(&mut self, other: Self) {
562+
fn merge(&mut self, other: Self, replace: ReplaceOpt) {
550563
$(
551-
if !self.$field.is_some() {
552-
self.$field = other.$field;
564+
match replace {
565+
ReplaceOpt::IgnoreDuplicate => {
566+
if self.$field.is_none() {
567+
self.$field = other.$field;
568+
}
569+
},
570+
ReplaceOpt::Override => {
571+
if other.$field.is_some() {
572+
self.$field = other.$field;
573+
}
574+
}
575+
ReplaceOpt::ErrorOnDuplicate => {
576+
if other.$field.is_some() {
577+
if self.$field.is_some() {
578+
if cfg!(test) {
579+
panic!("overriding existing option")
580+
} else {
581+
eprintln!("overriding existing option: `{}`", stringify!($field));
582+
crate::detail_exit(2);
583+
}
584+
} else {
585+
self.$field = other.$field;
586+
}
587+
}
588+
}
553589
}
554590
)*
555591
}
@@ -622,6 +658,37 @@ macro_rules! define_config {
622658
}
623659
}
624660

661+
impl<T> Merge for Option<T> {
662+
fn merge(&mut self, other: Self, replace: ReplaceOpt) {
663+
match replace {
664+
ReplaceOpt::IgnoreDuplicate => {
665+
if self.is_none() {
666+
*self = other;
667+
}
668+
}
669+
ReplaceOpt::Override => {
670+
if other.is_some() {
671+
*self = other;
672+
}
673+
}
674+
ReplaceOpt::ErrorOnDuplicate => {
675+
if other.is_some() {
676+
if self.is_some() {
677+
if cfg!(test) {
678+
panic!("overriding existing option")
679+
} else {
680+
eprintln!("overriding existing option");
681+
crate::detail_exit(2);
682+
}
683+
} else {
684+
*self = other;
685+
}
686+
}
687+
}
688+
}
689+
}
690+
}
691+
625692
define_config! {
626693
/// TOML representation of various global build decisions.
627694
#[derive(Default)]
@@ -863,28 +930,27 @@ impl Config {
863930

864931
pub fn parse(args: &[String]) -> Config {
865932
#[cfg(test)]
866-
let get_toml = |_: &_| TomlConfig::default();
933+
fn get_toml(_: &Path) -> TomlConfig {
934+
TomlConfig::default()
935+
}
936+
867937
#[cfg(not(test))]
868-
let get_toml = |file: &Path| {
938+
fn get_toml(file: &Path) -> TomlConfig {
869939
let contents =
870940
t!(fs::read_to_string(file), format!("config file {} not found", file.display()));
871941
// Deserialize to Value and then TomlConfig to prevent the Deserialize impl of
872942
// TomlConfig and sub types to be monomorphized 5x by toml.
873-
match toml::from_str(&contents)
943+
toml::from_str(&contents)
874944
.and_then(|table: toml::Value| TomlConfig::deserialize(table))
875-
{
876-
Ok(table) => table,
877-
Err(err) => {
878-
eprintln!("failed to parse TOML configuration '{}': {}", file.display(), err);
945+
.unwrap_or_else(|err| {
946+
eprintln!("failed to parse TOML configuration '{}': {err}", file.display());
879947
crate::detail_exit(2);
880-
}
881-
}
882-
};
883-
948+
})
949+
}
884950
Self::parse_inner(args, get_toml)
885951
}
886952

887-
fn parse_inner<'a>(args: &[String], get_toml: impl 'a + Fn(&Path) -> TomlConfig) -> Config {
953+
fn parse_inner(args: &[String], get_toml: impl Fn(&Path) -> TomlConfig) -> Config {
888954
let mut flags = Flags::parse(&args);
889955
let mut config = Config::default_opts();
890956

@@ -997,8 +1063,40 @@ impl Config {
9971063
include_path.push("defaults");
9981064
include_path.push(format!("config.{}.toml", include));
9991065
let included_toml = get_toml(&include_path);
1000-
toml.merge(included_toml);
1066+
toml.merge(included_toml, ReplaceOpt::IgnoreDuplicate);
1067+
}
1068+
1069+
let mut override_toml = TomlConfig::default();
1070+
for option in flags.set.iter() {
1071+
fn get_table(option: &str) -> Result<TomlConfig, toml::de::Error> {
1072+
toml::from_str(&option)
1073+
.and_then(|table: toml::Value| TomlConfig::deserialize(table))
1074+
}
1075+
1076+
let mut err = match get_table(option) {
1077+
Ok(v) => {
1078+
override_toml.merge(v, ReplaceOpt::ErrorOnDuplicate);
1079+
continue;
1080+
}
1081+
Err(e) => e,
1082+
};
1083+
// We want to be able to set string values without quotes,
1084+
// like in `configure.py`. Try adding quotes around the right hand side
1085+
if let Some((key, value)) = option.split_once("=") {
1086+
if !value.contains('"') {
1087+
match get_table(&format!(r#"{key}="{value}""#)) {
1088+
Ok(v) => {
1089+
override_toml.merge(v, ReplaceOpt::ErrorOnDuplicate);
1090+
continue;
1091+
}
1092+
Err(e) => err = e,
1093+
}
1094+
}
1095+
}
1096+
eprintln!("failed to parse override `{option}`: `{err}");
1097+
crate::detail_exit(2)
10011098
}
1099+
toml.merge(override_toml, ReplaceOpt::Override);
10021100

10031101
config.changelog_seen = toml.changelog_seen;
10041102

src/bootstrap/config/tests.rs

+71-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
use super::{Config, Flags, TomlConfig};
1+
use super::{Config, Flags};
22
use clap::CommandFactory;
33
use std::{env, path::Path};
44

5-
fn toml(config: &str) -> impl '_ + Fn(&Path) -> TomlConfig {
6-
|&_| toml::from_str(config).unwrap()
7-
}
8-
95
fn parse(config: &str) -> Config {
10-
Config::parse_inner(&["check".to_owned(), "--config=/does/not/exist".to_owned()], toml(config))
6+
Config::parse_inner(&["check".to_owned(), "--config=/does/not/exist".to_owned()], |&_| {
7+
toml::from_str(config).unwrap()
8+
})
119
}
1210

1311
#[test]
@@ -94,3 +92,70 @@ fn detect_src_and_out() {
9492
fn clap_verify() {
9593
Flags::command().debug_assert();
9694
}
95+
96+
#[test]
97+
fn override_toml() {
98+
let config = Config::parse_inner(
99+
&[
100+
"check".to_owned(),
101+
"--config=/does/not/exist".to_owned(),
102+
"--set=changelog-seen=1".to_owned(),
103+
"--set=rust.lto=fat".to_owned(),
104+
"--set=rust.deny-warnings=false".to_owned(),
105+
"--set=build.gdb=\"bar\"".to_owned(),
106+
"--set=build.tools=[\"cargo\"]".to_owned(),
107+
"--set=llvm.build-config={\"foo\" = \"bar\"}".to_owned(),
108+
],
109+
|&_| {
110+
toml::from_str(
111+
r#"
112+
changelog-seen = 0
113+
[rust]
114+
lto = "off"
115+
deny-warnings = true
116+
117+
[build]
118+
gdb = "foo"
119+
tools = []
120+
121+
[llvm]
122+
download-ci-llvm = false
123+
build-config = {}
124+
"#,
125+
)
126+
.unwrap()
127+
},
128+
);
129+
assert_eq!(config.changelog_seen, Some(1), "setting top-level value");
130+
assert_eq!(
131+
config.rust_lto,
132+
crate::config::RustcLto::Fat,
133+
"setting string value without quotes"
134+
);
135+
assert_eq!(config.gdb, Some("bar".into()), "setting string value with quotes");
136+
assert_eq!(config.deny_warnings, false, "setting boolean value");
137+
assert_eq!(
138+
config.tools,
139+
Some(["cargo".to_string()].into_iter().collect()),
140+
"setting list value"
141+
);
142+
assert_eq!(
143+
config.llvm_build_config,
144+
[("foo".to_string(), "bar".to_string())].into_iter().collect(),
145+
"setting dictionary value"
146+
);
147+
}
148+
149+
#[test]
150+
#[should_panic]
151+
fn override_toml_duplicate() {
152+
Config::parse_inner(
153+
&[
154+
"check".to_owned(),
155+
"--config=/does/not/exist".to_owned(),
156+
"--set=changelog-seen=1".to_owned(),
157+
"--set=changelog-seen=2".to_owned(),
158+
],
159+
|&_| toml::from_str("changelog-seen = 0").unwrap(),
160+
);
161+
}

src/bootstrap/flags.rs

+3
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ pub struct Flags {
158158
#[arg(global(true))]
159159
/// paths for the subcommand
160160
pub paths: Vec<PathBuf>,
161+
/// override options in config.toml
162+
#[arg(global(true), value_hint = clap::ValueHint::Other, long, value_name = "section.option=value")]
163+
pub set: Vec<String>,
161164
/// arguments passed to subcommands
162165
#[arg(global(true), last(true), value_name = "ARGS")]
163166
pub free_args: Vec<String>,

0 commit comments

Comments
 (0)