Skip to content

detect incompatible CI rustc options more precisely #129052

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

Merged
merged 10 commits into from
Aug 16, 2024
Merged
187 changes: 105 additions & 82 deletions src/bootstrap/src/core/config/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ impl Display for DebuginfoLevel {
/// 2) MSVC
/// - Self-contained: `-Clinker=<path to rust-lld>`
/// - External: `-Clinker=lld`
#[derive(Default, Copy, Clone)]
#[derive(Copy, Clone, Default, PartialEq)]
pub enum LldMode {
/// Do not use LLD
#[default]
Expand Down Expand Up @@ -1191,37 +1191,39 @@ impl Config {
}
}

pub fn parse(flags: Flags) -> Config {
#[cfg(test)]
fn get_toml(_: &Path) -> TomlConfig {
TomlConfig::default()
}
#[cfg(test)]
fn get_toml(_: &Path) -> TomlConfig {
TomlConfig::default()
}

#[cfg(not(test))]
fn get_toml(file: &Path) -> TomlConfig {
let contents =
t!(fs::read_to_string(file), format!("config file {} not found", file.display()));
// Deserialize to Value and then TomlConfig to prevent the Deserialize impl of
// TomlConfig and sub types to be monomorphized 5x by toml.
toml::from_str(&contents)
.and_then(|table: toml::Value| TomlConfig::deserialize(table))
.unwrap_or_else(|err| {
if let Ok(Some(changes)) = toml::from_str(&contents)
.and_then(|table: toml::Value| ChangeIdWrapper::deserialize(table)).map(|change_id| change_id.inner.map(crate::find_recent_config_change_ids))
{
if !changes.is_empty() {
println!(
"WARNING: There have been changes to x.py since you last updated:\n{}",
crate::human_readable_changes(&changes)
);
}
#[cfg(not(test))]
fn get_toml(file: &Path) -> TomlConfig {
let contents =
t!(fs::read_to_string(file), format!("config file {} not found", file.display()));
// Deserialize to Value and then TomlConfig to prevent the Deserialize impl of
// TomlConfig and sub types to be monomorphized 5x by toml.
toml::from_str(&contents)
.and_then(|table: toml::Value| TomlConfig::deserialize(table))
.unwrap_or_else(|err| {
if let Ok(Some(changes)) = toml::from_str(&contents)
.and_then(|table: toml::Value| ChangeIdWrapper::deserialize(table))
.map(|change_id| change_id.inner.map(crate::find_recent_config_change_ids))
{
if !changes.is_empty() {
println!(
"WARNING: There have been changes to x.py since you last updated:\n{}",
crate::human_readable_changes(&changes)
);
}
}

eprintln!("failed to parse TOML configuration '{}': {err}", file.display());
exit!(2);
})
}
Self::parse_inner(flags, get_toml)
eprintln!("failed to parse TOML configuration '{}': {err}", file.display());
exit!(2);
})
}

pub fn parse(flags: Flags) -> Config {
Self::parse_inner(flags, Self::get_toml)
}

pub(crate) fn parse_inner(mut flags: Flags, get_toml: impl Fn(&Path) -> TomlConfig) -> Config {
Expand Down Expand Up @@ -1579,24 +1581,6 @@ impl Config {
let mut is_user_configured_rust_channel = false;

if let Some(rust) = toml.rust {
if let Some(commit) = config.download_ci_rustc_commit(rust.download_rustc.clone()) {
// Primarily used by CI runners to avoid handling download-rustc incompatible
// options one by one on shell scripts.
let disable_ci_rustc_if_incompatible =
env::var_os("DISABLE_CI_RUSTC_IF_INCOMPATIBLE")
.is_some_and(|s| s == "1" || s == "true");

if let Err(e) = check_incompatible_options_for_ci_rustc(&rust) {
if disable_ci_rustc_if_incompatible {
config.download_rustc_commit = None;
} else {
panic!("{}", e);
}
} else {
config.download_rustc_commit = Some(commit);
}
}

let Rust {
optimize: optimize_toml,
debug: debug_toml,
Expand Down Expand Up @@ -1644,7 +1628,7 @@ impl Config {
new_symbol_mangling,
profile_generate,
profile_use,
download_rustc: _,
download_rustc,
lto,
validate_mir_opts,
frame_pointers,
Expand All @@ -1656,6 +1640,8 @@ impl Config {
is_user_configured_rust_channel = channel.is_some();
set(&mut config.channel, channel.clone());

config.download_rustc_commit = config.download_ci_rustc_commit(download_rustc);

debug = debug_toml;
debug_assertions = debug_assertions_toml;
debug_assertions_std = debug_assertions_std_toml;
Expand Down Expand Up @@ -2333,6 +2319,29 @@ impl Config {
None => None,
Some(commit) => {
self.download_ci_rustc(commit);

let builder_config_path =
self.out.join(self.build.triple).join("ci-rustc/builder-config");
let ci_config_toml = Self::get_toml(&builder_config_path);
let current_config_toml = Self::get_toml(self.config.as_ref().unwrap());

// Check the config compatibility
let res = check_incompatible_options_for_ci_rustc(
current_config_toml,
ci_config_toml,
);

let disable_ci_rustc_if_incompatible =
env::var_os("DISABLE_CI_RUSTC_IF_INCOMPATIBLE")
.is_some_and(|s| s == "1" || s == "true");

if disable_ci_rustc_if_incompatible && res.is_err() {
println!("WARNING: download-rustc is disabled with `DISABLE_CI_RUSTC_IF_INCOMPATIBLE` env.");
return None;
}

res.unwrap();

Some(commit.clone())
}
})
Expand Down Expand Up @@ -2650,31 +2659,44 @@ impl Config {
}
}

/// Checks the CI rustc incompatible options by destructuring the `Rust` instance
/// and makes sure that no rust options from config.toml are missed.
fn check_incompatible_options_for_ci_rustc(rust: &Rust) -> Result<(), String> {
/// Compares the current Rust options against those in the CI rustc builder and detects any incompatible options.
/// It does this by destructuring the `Rust` instance to make sure every `Rust` field is covered and not missing.
fn check_incompatible_options_for_ci_rustc(
current_config_toml: TomlConfig,
ci_config_toml: TomlConfig,
) -> Result<(), String> {
macro_rules! err {
($name:expr) => {
if $name.is_some() {
return Err(format!(
"ERROR: Setting `rust.{}` is incompatible with `rust.download-rustc`.",
stringify!($name).replace("_", "-")
));
}
($current:expr, $expected:expr) => {
if let Some(current) = $current {
if Some(current) != $expected {
return Err(format!(
"ERROR: Setting `rust.{}` is incompatible with `rust.download-rustc`.",
stringify!($expected).replace("_", "-")
));
};
};
};
}

macro_rules! warn {
($name:expr) => {
if $name.is_some() {
println!(
"WARNING: `rust.{}` has no effect with `rust.download-rustc`.",
stringify!($name).replace("_", "-")
);
}
($current:expr, $expected:expr) => {
if let Some(current) = $current {
if Some(current) != $expected {
println!(
"WARNING: `rust.{}` has no effect with `rust.download-rustc`.",
stringify!($expected).replace("_", "-")
);
};
};
};
}

let (Some(current_rust_config), Some(ci_rust_config)) =
(current_config_toml.rust, ci_config_toml.rust)
else {
return Ok(());
};

let Rust {
// Following options are the CI rustc incompatible ones.
optimize,
Expand Down Expand Up @@ -2732,30 +2754,31 @@ fn check_incompatible_options_for_ci_rustc(rust: &Rust) -> Result<(), String> {
download_rustc: _,
validate_mir_opts: _,
frame_pointers: _,
} = rust;
} = ci_rust_config;

// There are two kinds of checks for CI rustc incompatible options:
// 1. Checking an option that may change the compiler behaviour/output.
// 2. Checking an option that have no effect on the compiler behaviour/output.
//
// If the option belongs to the first category, we call `err` macro for a hard error;
// otherwise, we just print a warning with `warn` macro.
err!(optimize);
err!(debug_logging);
err!(debuginfo_level_rustc);
err!(default_linker);
err!(rpath);
err!(strip);
err!(stack_protector);
err!(lld_mode);
err!(llvm_tools);
err!(llvm_bitcode_linker);
err!(jemalloc);
err!(lto);

warn!(channel);
warn!(description);
warn!(incremental);

err!(current_rust_config.optimize, optimize);
err!(current_rust_config.debug_logging, debug_logging);
err!(current_rust_config.debuginfo_level_rustc, debuginfo_level_rustc);
err!(current_rust_config.rpath, rpath);
err!(current_rust_config.strip, strip);
err!(current_rust_config.lld_mode, lld_mode);
err!(current_rust_config.llvm_tools, llvm_tools);
err!(current_rust_config.llvm_bitcode_linker, llvm_bitcode_linker);
err!(current_rust_config.jemalloc, jemalloc);
err!(current_rust_config.default_linker, default_linker);
err!(current_rust_config.stack_protector, stack_protector);
err!(current_rust_config.lto, lto);

warn!(current_rust_config.channel, channel);
warn!(current_rust_config.description, description);
warn!(current_rust_config.incremental, incremental);

Ok(())
}
Expand Down
11 changes: 7 additions & 4 deletions src/bootstrap/src/core/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,12 @@ impl Config {

let mut tar = tar::Archive::new(decompressor);

let is_ci_rustc = dst.ends_with("ci-rustc");

// `compile::Sysroot` needs to know the contents of the `rustc-dev` tarball to avoid adding
// it to the sysroot unless it was explicitly requested. But parsing the 100 MB tarball is slow.
// Cache the entries when we extract it so we only have to read it once.
let mut recorded_entries =
if dst.ends_with("ci-rustc") { recorded_entries(dst, pattern) } else { None };
let mut recorded_entries = if is_ci_rustc { recorded_entries(dst, pattern) } else { None };

for member in t!(tar.entries()) {
let mut member = t!(member);
Expand All @@ -287,10 +288,12 @@ impl Config {
continue;
}
let mut short_path = t!(original_path.strip_prefix(directory_prefix));
if !short_path.starts_with(pattern) {
let is_builder_config = short_path.to_str() == Some("builder-config");

if !short_path.starts_with(pattern) && (is_ci_rustc && !is_builder_config) {
continue;
}
short_path = t!(short_path.strip_prefix(pattern));
short_path = short_path.strip_prefix(pattern).unwrap_or(short_path);
let dst_path = dst.join(short_path);
self.verbose(|| {
println!("extracting {} to {}", original_path.display(), dst.display())
Expand Down
Loading