Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support config extensions #138934

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion bootstrap.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
# Note that this has no default value (x.py uses the defaults in `bootstrap.example.toml`).
#profile = <none>

# Inherits configuration values from different configuration files (a.k.a. config extensions).
# Supports absolute paths, and uses the current directory (where the bootstrap was invoked)
# as the base if the given path is not absolute.
#
# The overriding logic follows a right-to-left order. For example, in `include = ["a.toml", "b.toml"]`,
# extension `b.toml` overrides `a.toml`. Also, parent extensions always overrides the inner ones.
#include = []

# Keeps track of major changes made to this configuration.
#
# This value also represents ID of the PR that caused major changes. Meaning,
Expand All @@ -28,7 +36,7 @@
# - A new option
# - A change in the default values
#
# If the change-id does not match the version currently in use, x.py will
# If the change-id does not match the version currently in use, x.py will
# display the changes made to the bootstrap.
# To suppress these warnings, you can set change-id = "ignore".
#change-id = <latest change id in src/bootstrap/src/utils/change_tracker.rs>
Expand Down
117 changes: 104 additions & 13 deletions src/bootstrap/src/core/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use std::cell::{Cell, RefCell};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fmt::{self, Display};
use std::hash::Hash;
use std::io::IsTerminal;
use std::path::{Path, PathBuf, absolute};
use std::process::Command;
Expand Down Expand Up @@ -698,6 +699,7 @@ pub(crate) struct TomlConfig {
target: Option<HashMap<String, TomlTarget>>,
dist: Option<Dist>,
profile: Option<String>,
include: Option<Vec<PathBuf>>,
}

/// This enum is used for deserializing change IDs from TOML, allowing both numeric values and the string `"ignore"`.
Expand Down Expand Up @@ -744,27 +746,70 @@ enum ReplaceOpt {
}

trait Merge {
fn merge(&mut self, other: Self, replace: ReplaceOpt);
fn merge(
&mut self,
parent_config_path: Option<PathBuf>,
included_extensions: &mut HashSet<PathBuf>,
other: Self,
replace: ReplaceOpt,
);
}

impl Merge for TomlConfig {
fn merge(
&mut self,
TomlConfig { build, install, llvm, gcc, rust, dist, target, profile, change_id }: Self,
parent_config_path: Option<PathBuf>,
included_extensions: &mut HashSet<PathBuf>,
TomlConfig { build, install, llvm, gcc, rust, dist, target, profile, change_id, include }: Self,
replace: ReplaceOpt,
) {
fn do_merge<T: Merge>(x: &mut Option<T>, y: Option<T>, replace: ReplaceOpt) {
if let Some(new) = y {
if let Some(original) = x {
original.merge(new, replace);
original.merge(None, &mut Default::default(), new, replace);
} else {
*x = Some(new);
}
}
}

self.change_id.inner.merge(change_id.inner, replace);
self.profile.merge(profile, replace);
let parent_dir = parent_config_path
.as_ref()
.and_then(|p| p.parent().map(ToOwned::to_owned))
.unwrap_or_default();

for include_path in include.clone().unwrap_or_default().iter().rev() {
let include_path = parent_dir.join(include_path);
let include_path = include_path.canonicalize().unwrap_or_else(|e| {
eprintln!("ERROR: Failed to canonicalize '{}' path: {e}", include_path.display());
exit!(2);
});

let included_toml = Config::get_toml(&include_path).unwrap_or_else(|e| {
eprintln!("ERROR: Failed to parse '{}': {e}", include_path.display());
exit!(2);
});

assert!(
included_extensions.insert(include_path.clone()),
"Cyclic inclusion detected: '{}' is being included again before its previous inclusion was fully processed.",
include_path.display()
);

self.merge(
Some(include_path.clone()),
included_extensions,
included_toml,
// Ensures that parent configuration always takes precedence
// over child configurations.
ReplaceOpt::IgnoreDuplicate,
);

included_extensions.remove(&include_path);
}

self.change_id.inner.merge(None, &mut Default::default(), change_id.inner, replace);
self.profile.merge(None, &mut Default::default(), profile, replace);

do_merge(&mut self.build, build, replace);
do_merge(&mut self.install, install, replace);
Expand All @@ -779,7 +824,7 @@ impl Merge for TomlConfig {
(Some(original_target), Some(new_target)) => {
for (triple, new) in new_target {
if let Some(original) = original_target.get_mut(&triple) {
original.merge(new, replace);
original.merge(None, &mut Default::default(), new, replace);
} else {
original_target.insert(triple, new);
}
Expand All @@ -800,7 +845,13 @@ macro_rules! define_config {
}

impl Merge for $name {
fn merge(&mut self, other: Self, replace: ReplaceOpt) {
fn merge(
&mut self,
_parent_config_path: Option<PathBuf>,
_included_extensions: &mut HashSet<PathBuf>,
other: Self,
replace: ReplaceOpt
) {
$(
match replace {
ReplaceOpt::IgnoreDuplicate => {
Expand Down Expand Up @@ -900,7 +951,13 @@ macro_rules! define_config {
}

impl<T> Merge for Option<T> {
fn merge(&mut self, other: Self, replace: ReplaceOpt) {
fn merge(
&mut self,
_parent_config_path: Option<PathBuf>,
_included_extensions: &mut HashSet<PathBuf>,
other: Self,
replace: ReplaceOpt,
) {
match replace {
ReplaceOpt::IgnoreDuplicate => {
if self.is_none() {
Expand Down Expand Up @@ -1547,7 +1604,8 @@ impl Config {
// but not if `bootstrap.toml` hasn't been created.
let mut toml = if !using_default_path || toml_path.exists() {
config.config = Some(if cfg!(not(test)) {
toml_path.canonicalize().unwrap()
toml_path = toml_path.canonicalize().unwrap();
toml_path.clone()
} else {
toml_path.clone()
});
Expand Down Expand Up @@ -1575,6 +1633,24 @@ impl Config {
toml.profile = Some("dist".into());
}

// Reverse the list to ensure the last added config extension remains the most dominant.
// For example, given ["a.toml", "b.toml"], "b.toml" should take precedence over "a.toml".
//
// This must be handled before applying the `profile` since `include`s should always take
// precedence over `profile`s.
for include_path in toml.include.clone().unwrap_or_default().iter().rev() {
let included_toml = get_toml(include_path).unwrap_or_else(|e| {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let included_toml = get_toml(include_path).unwrap_or_else(|e| {
let included_toml = get_toml(toml_path.parent().unwrap().join(include_path)).unwrap_or_else(|e| {

Otherwise the file isn't resolved correctly. (Better do something like let include_path = toml_path.parent().unwrap().join(include_path); at the beginning of the loop).

eprintln!("ERROR: Failed to parse '{}': {e}", include_path.display());
exit!(2);
});
toml.merge(
Some(toml_path.join(include_path)),
&mut Default::default(),
included_toml,
ReplaceOpt::IgnoreDuplicate,
);
}

if let Some(include) = &toml.profile {
// Allows creating alias for profile names, allowing
// profiles to be renamed while maintaining back compatibility
Expand All @@ -1596,7 +1672,12 @@ impl Config {
);
exit!(2);
});
toml.merge(included_toml, ReplaceOpt::IgnoreDuplicate);
toml.merge(
Some(include_path),
&mut Default::default(),
included_toml,
ReplaceOpt::IgnoreDuplicate,
);
}

let mut override_toml = TomlConfig::default();
Expand All @@ -1607,7 +1688,12 @@ impl Config {

let mut err = match get_table(option) {
Ok(v) => {
override_toml.merge(v, ReplaceOpt::ErrorOnDuplicate);
override_toml.merge(
None,
&mut Default::default(),
v,
ReplaceOpt::ErrorOnDuplicate,
);
continue;
}
Err(e) => e,
Expand All @@ -1618,7 +1704,12 @@ impl Config {
if !value.contains('"') {
match get_table(&format!(r#"{key}="{value}""#)) {
Ok(v) => {
override_toml.merge(v, ReplaceOpt::ErrorOnDuplicate);
override_toml.merge(
None,
&mut Default::default(),
v,
ReplaceOpt::ErrorOnDuplicate,
);
continue;
}
Err(e) => err = e,
Expand All @@ -1628,7 +1719,7 @@ impl Config {
eprintln!("failed to parse override `{option}`: `{err}");
exit!(2)
}
toml.merge(override_toml, ReplaceOpt::Override);
toml.merge(None, &mut Default::default(), override_toml, ReplaceOpt::Override);

config.change_id = toml.change_id.inner;

Expand Down
5 changes: 5 additions & 0 deletions src/bootstrap/src/utils/change_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,4 +391,9 @@ pub const CONFIG_CHANGE_HISTORY: &[ChangeInfo] = &[
severity: ChangeSeverity::Info,
summary: "You can now use `change-id = \"ignore\"` to suppress `change-id ` warnings in the console.",
},
ChangeInfo {
change_id: 138934,
severity: ChangeSeverity::Info,
summary: "Added new option `include` to create config extensions.",
},
];
37 changes: 37 additions & 0 deletions src/doc/rustc-dev-guide/src/building/suggested.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,43 @@ your `.git/hooks` folder as `pre-push` (without the `.sh` extension!).

You can also install the hook as a step of running `./x setup`!

## Config extensions

When working on different tasks, you might need to switch between different bootstrap configurations.
Sometimes you may want to keep an old configuration for future use. But saving raw config values in
random files and manually copying and pasting them can quickly become messy, especially if you have a
long history of different configurations.

To simplify managing multiple configurations, you can create config extensions.

For example, you can create a simple config file named `cross.toml`:

```toml
[build]
build = "x86_64-unknown-linux-gnu"
host = ["i686-unknown-linux-gnu"]
target = ["i686-unknown-linux-gnu"]


[llvm]
download-ci-llvm = false

[target.x86_64-unknown-linux-gnu]
llvm-config = "/path/to/llvm-19/bin/llvm-config"
```

Then, include this in your `bootstrap.toml`:

```toml
include = ["cross.toml"]
```

You can also include extensions within extensions recursively.

**Note:** In the `include` field, the overriding logic follows a right-to-left order. For example,
in `include = ["a.toml", "b.toml"]`, extension `b.toml` overrides `a.toml`. Also, parent extensions
always overrides the inner ones.

## Configuring `rust-analyzer` for `rustc`

### Project-local rust-analyzer setup
Expand Down
Loading