diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl index 2f695cbbb9..32ce62256b 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -753,6 +753,16 @@ _common_attrs = { cfg = "exec", providers = [[CrateInfo], [CrateGroupInfo]], ), + "require_explicit_unstable_features": attr.int( + doc = ( + "Whether to require all unstable features to be explicitly opted in to using " + + "`-Zallow-features=...`. Possible values: [-1, 0, 1]. -1 means delegate to the " + + "toolchain.require_explicit_unstable_features boolean build setting; 0 means False; " + + "1 means True." + ), + values = [-1, 0, 1], + default = -1, + ), "rustc_env": attr.string_dict( doc = dedent("""\ Dictionary of additional `"key": "value"` environment variables to set for rustc. diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 48be035043..787d8ea081 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -835,6 +835,7 @@ def construct_arguments( build_metadata = False, force_depend_on_objects = False, skip_expanding_rustc_env = False, + require_explicit_unstable_features = False, error_format = None): """Builds an Args object containing common rustc flags @@ -867,6 +868,7 @@ def construct_arguments( build_metadata (bool): Generate CLI arguments for building *only* .rmeta files. This requires use_json_output. force_depend_on_objects (bool): Force using `.rlib` object files instead of metadata (`.rmeta`) files even if they are available. skip_expanding_rustc_env (bool): Whether to skip expanding CrateInfo.rustc_env_attr + require_explicit_unstable_features (bool): Whether to require all unstable features to be explicitly opted in to using `-Zallow-features=...`. error_format (str, optional): Error format to pass to the `--error-format` command line argument. If set to None, uses the "_error_format" entry in `attr`. Returns: @@ -895,6 +897,9 @@ def construct_arguments( process_wrapper_flags.add_all(build_flags_files, before_each = "--arg-file") + if require_explicit_unstable_features: + process_wrapper_flags.add("--require-explicit-unstable-features", "true") + # Certain rust build processes expect to find files from the environment # variable `$CARGO_MANIFEST_DIR`. Examples of this include pest, tera, # asakuma. @@ -1289,6 +1294,16 @@ def rustc_compile_action( if experimental_use_cc_common_link: emit = ["obj"] + # Determine whether to pass `--require-explicit-unstable-features true` to the process wrapper: + require_explicit_unstable_features = False + if hasattr(ctx.attr, "require_explicit_unstable_features"): + if ctx.attr.require_explicit_unstable_features == 0: + require_explicit_unstable_features = False + elif ctx.attr.require_explicit_unstable_features == 1: + require_explicit_unstable_features = True + elif ctx.attr.require_explicit_unstable_features == -1: + require_explicit_unstable_features = toolchain.require_explicit_unstable_features + args, env_from_args = construct_arguments( ctx = ctx, attr = attr, @@ -1311,6 +1326,7 @@ def rustc_compile_action( stamp = stamp, use_json_output = bool(build_metadata) or bool(rustc_output) or bool(rustc_rmeta_output), skip_expanding_rustc_env = skip_expanding_rustc_env, + require_explicit_unstable_features = require_explicit_unstable_features, ) args_metadata = None @@ -1337,6 +1353,7 @@ def rustc_compile_action( stamp = stamp, use_json_output = True, build_metadata = True, + require_explicit_unstable_features = require_explicit_unstable_features, ) env = dict(ctx.configuration.default_shell_env) diff --git a/rust/settings/BUILD.bazel b/rust/settings/BUILD.bazel index a93b91cc64..129131feb8 100644 --- a/rust/settings/BUILD.bazel +++ b/rust/settings/BUILD.bazel @@ -31,6 +31,7 @@ load( "no_std", "pipelined_compilation", "rename_first_party_crates", + "require_explicit_unstable_features", "rustc_output_diagnostics", "rustfmt_toml", "third_party_dir", @@ -119,6 +120,8 @@ pipelined_compilation() rename_first_party_crates() +require_explicit_unstable_features() + rustc_output_diagnostics() rustfmt_toml() diff --git a/rust/settings/settings.bzl b/rust/settings/settings.bzl index abedffa4c0..e1886923df 100644 --- a/rust/settings/settings.bzl +++ b/rust/settings/settings.bzl @@ -78,6 +78,19 @@ def rename_first_party_crates(): build_setting_default = False, ) +def require_explicit_unstable_features(): + """A flag controlling whether unstable features should be disallowed by default + + If true, an empty `-Zallow-features=` will be added to the rustc command line whenever no other + `-Zallow-features=` is present in the rustc flags. The effect is to disallow all unstable + features by default, with the possibility to explicitly re-enable them selectively using + `-Zallow-features=...`. + """ + bool_flag( + name = "require_explicit_unstable_features", + build_setting_default = False, + ) + def third_party_dir(): """A flag specifying the location of vendored third-party rust crates within this \ repository that must not be renamed when `rename_first_party_crates` is enabled. diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl index f3ef2f2987..4d9de9925a 100644 --- a/rust/toolchain.bzl +++ b/rust/toolchain.bzl @@ -538,6 +538,9 @@ def _generate_sysroot( def _experimental_use_cc_common_link(ctx): return ctx.attr.experimental_use_cc_common_link[BuildSettingInfo].value +def _require_explicit_unstable_features(ctx): + return ctx.attr.require_explicit_unstable_features[BuildSettingInfo].value + def _expand_flags(ctx, attr_name, targets, make_variables): targets = deduplicate(targets) expanded_flags = [] @@ -753,6 +756,7 @@ def _rust_toolchain_impl(ctx): target_json = target_json, target_os = target_os, target_triple = target_triple, + require_explicit_unstable_features = _require_explicit_unstable_features(ctx), # Experimental and incompatible flags _rename_first_party_crates = rename_first_party_crates, @@ -904,6 +908,13 @@ rust_toolchain = rule( "per_crate_rustc_flags": attr.string_list( doc = "Extra flags to pass to rustc in non-exec configuration", ), + "require_explicit_unstable_features": attr.label( + default = Label( + "//rust/settings:require_explicit_unstable_features", + ), + doc = ("Label to a boolean build setting that controls whether all uses of unstable " + + "Rust features must be explicitly opted in to using `-Zallow-features=...`."), + ), "rust_doc": attr.label( doc = "The location of the `rustdoc` binary. Can be a direct source or a filegroup containing one item.", allow_single_file = True, diff --git a/util/process_wrapper/options.rs b/util/process_wrapper/options.rs index c0f4add011..34e9b11d2e 100644 --- a/util/process_wrapper/options.rs +++ b/util/process_wrapper/options.rs @@ -67,6 +67,7 @@ pub(crate) fn options() -> Result { let mut rustc_quit_on_rmeta_raw = None; let mut rustc_output_format_raw = None; let mut flags = Flags::new(); + let mut require_explicit_unstable_features = None; flags.define_repeated_flag("--subst", "", &mut subst_mapping_raw); flags.define_flag("--stable-status-file", "", &mut stable_status_file_raw); flags.define_flag("--volatile-status-file", "", &mut volatile_status_file_raw); @@ -114,6 +115,12 @@ pub(crate) fn options() -> Result { Default: `rendered`", &mut rustc_output_format_raw, ); + flags.define_flag( + "--require-explicit-unstable-features", + "If set, an empty -Zallow-features= will be added to the rustc command line whenever no \ + other -Zallow-features= is present in the rustc flags.", + &mut require_explicit_unstable_features, + ); let mut child_args = match flags .parse(env::args().collect()) @@ -191,9 +198,13 @@ pub(crate) fn options() -> Result { &volatile_stamp_mappings, &subst_mappings, ); + + let require_explicit_unstable_features = + require_explicit_unstable_features.is_some_and(|s| s == "true"); + // Append all the arguments fetched from files to those provided via command line. child_args.append(&mut file_arguments); - let child_args = prepare_args(child_args, &subst_mappings)?; + let child_args = prepare_args(child_args, &subst_mappings, require_explicit_unstable_features)?; // Split the executable path from the rest of the arguments. let (exec_path, args) = child_args.split_first().ok_or_else(|| { OptionError::Generic( @@ -243,6 +254,10 @@ fn env_from_files(paths: Vec) -> Result, OptionE Ok(env_vars) } +fn is_allow_features_flag(arg: &str) -> bool { + arg.starts_with("-Zallow-features=") || arg.starts_with("allow-features=") +} + fn prepare_arg(mut arg: String, subst_mappings: &[(String, String)]) -> String { for (f, replace_with) in subst_mappings { let from = format!("${{{f}}}"); @@ -251,11 +266,12 @@ fn prepare_arg(mut arg: String, subst_mappings: &[(String, String)]) -> String { arg } -/// Apply substitutions to the given param file. Returns the new filename. +/// Apply substitutions to the given param file. Returns the new filename and whether any +/// allow-features flags were found. fn prepare_param_file( filename: &str, subst_mappings: &[(String, String)], -) -> Result { +) -> Result<(String, bool), OptionError> { let expanded_file = format!("{filename}.expanded"); let format_err = |err: io::Error| { OptionError::Generic(format!( @@ -271,38 +287,48 @@ fn prepare_param_file( out: &mut io::BufWriter, subst_mappings: &[(String, String)], format_err: &impl Fn(io::Error) -> OptionError, - ) -> Result<(), OptionError> { + ) -> Result { + let mut has_allow_features_flag = false; for arg in read_file_to_array(filename).map_err(OptionError::Generic)? { let arg = prepare_arg(arg, subst_mappings); + has_allow_features_flag |= is_allow_features_flag(&arg); if let Some(arg_file) = arg.strip_prefix('@') { - process_file(arg_file, out, subst_mappings, format_err)?; + has_allow_features_flag |= process_file(arg_file, out, subst_mappings, format_err)?; } else { writeln!(out, "{arg}").map_err(format_err)?; } } - Ok(()) + Ok(has_allow_features_flag) } - process_file(filename, &mut out, subst_mappings, &format_err)?; - Ok(expanded_file) + let has_allow_features_flag = process_file(filename, &mut out, subst_mappings, &format_err)?; + Ok((expanded_file, has_allow_features_flag)) } /// Apply substitutions to the provided arguments, recursing into param files. fn prepare_args( args: Vec, subst_mappings: &[(String, String)], + require_explicit_unstable_features: bool, ) -> Result, OptionError> { - args.into_iter() - .map(|arg| { - let arg = prepare_arg(arg, subst_mappings); - if let Some(param_file) = arg.strip_prefix('@') { - // Note that substitutions may also apply to the param file path! - prepare_param_file(param_file, subst_mappings) - .map(|filename| format!("@{filename}")) - } else { - Ok(arg) - } - }) - .collect() + let mut allowed_features = false; + let mut processed_args = Vec::::new(); + for arg in args.into_iter() { + let arg = prepare_arg(arg, subst_mappings); + if let Some(param_file) = arg.strip_prefix('@') { + // Note that substitutions may also apply to the param file path! + let (file, allowed) = prepare_param_file(param_file, subst_mappings) + .map(|(filename, af)| (format!("@{filename}"), af))?; + allowed_features |= allowed; + processed_args.push(file); + } else { + allowed_features |= is_allow_features_flag(&arg); + processed_args.push(arg); + } + } + if !allowed_features && require_explicit_unstable_features { + processed_args.push("-Zallow-features=".to_string()); + } + Ok(processed_args) } fn environment_block( @@ -334,3 +360,33 @@ fn environment_block( } environment_variables } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_enforce_allow_features_flag_user_didnt_say() { + let args = vec!["rustc".to_string()]; + let subst_mappings: Vec<(String, String)> = vec![]; + let args = prepare_args(args, &subst_mappings).unwrap(); + assert_eq!(args, vec!["rustc".to_string(), "-Zallow-features=".to_string(),]); + } + + #[test] + fn test_enforce_allow_features_flag_user_requested_something() { + let args = vec![ + "rustc".to_string(), + "-Zallow-features=whitespace_instead_of_curly_braces".to_string(), + ]; + let subst_mappings: Vec<(String, String)> = vec![]; + let args = prepare_args(args, &subst_mappings).unwrap(); + assert_eq!( + args, + vec![ + "rustc".to_string(), + "-Zallow-features=whitespace_instead_of_curly_braces".to_string(), + ] + ); + } +}