diff --git a/crates/cargo-util-schemas/manifest.schema.json b/crates/cargo-util-schemas/manifest.schema.json index 60eb5434b76..2a5499c3fc0 100644 --- a/crates/cargo-util-schemas/manifest.schema.json +++ b/crates/cargo-util-schemas/manifest.schema.json @@ -1059,6 +1059,16 @@ "Hints": { "type": "object", "properties": { + "min-opt-level": { + "anyOf": [ + { + "$ref": "#/$defs/TomlValue" + }, + { + "type": "null" + } + ] + }, "mostly-unused": { "anyOf": [ { diff --git a/crates/cargo-util-schemas/src/manifest/mod.rs b/crates/cargo-util-schemas/src/manifest/mod.rs index a51ac3ef7fc..f222bcee7a0 100644 --- a/crates/cargo-util-schemas/src/manifest/mod.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -1673,6 +1673,11 @@ pub enum TomlLintLevel { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))] pub struct Hints { + #[cfg_attr( + feature = "unstable-schema", + schemars(with = "Option") + )] + pub min_opt_level: Option, #[cfg_attr( feature = "unstable-schema", schemars(with = "Option") diff --git a/doc/book/src/reference/manifest.md b/doc/book/src/reference/manifest.md index d3f9c4b65c6..64e40b80b93 100644 --- a/doc/book/src/reference/manifest.md +++ b/doc/book/src/reference/manifest.md @@ -595,9 +595,9 @@ Individual hints may have an associated unstable feature gate that you need to pass in order to apply the configuration they specify, but if you don't specify that unstable feature gate, you will again get only a warning, not an error. -There are no stable hints at this time. See the [hint-mostly-unused -documentation](unstable.md#profile-hint-mostly-unused-option) for information -on an unstable hint. +There are no stable hints at this time. See the documentation for the unstable +[`mostly-unused`](unstable.md#profile-hint-mostly-unused-option) and +[`min-opt-level`](unstable.md#package-min-opt-level-hint) hints. > **MSRV:** Respected as of 1.90. diff --git a/doc/book/src/reference/unstable.md b/doc/book/src/reference/unstable.md index e9989e951d7..65a4dc4c338 100644 --- a/doc/book/src/reference/unstable.md +++ b/doc/book/src/reference/unstable.md @@ -109,6 +109,7 @@ Each new feature described below should explain how to use it. * `Cargo.toml` extensions * [Profile `rustflags` option](#profile-rustflags-option) --- Passed directly to rustc. * [Profile `hint-mostly-unused` option](#profile-hint-mostly-unused-option) --- Hint that a dependency is mostly unused, to optimize compilation time. + * [Package `min-opt-level` hint](#package-min-opt-level-hint) --- Request a numeric optimization floor for one package. * [codegen-backend](#codegen-backend) --- Select the codegen backend used by rustc. * [per-package-target](#per-package-target) --- Sets the `--target` to use for each individual package. * [artifact dependencies](#artifact-dependencies) --- Allow build artifacts to be included into other build artifacts and build them for different targets. @@ -924,6 +925,80 @@ This will cause the crate to default to hint-mostly-unused, unless overridden via `profile`, which takes precedence, and which can only be specified in the top-level crate being built. +## Package `min-opt-level` hint +* Tracking Issue: [#17334](https://github.com/rust-lang/cargo/issues/17334) +* RFC: [#3924](https://github.com/rust-lang/rfcs/pull/3924) + +This feature adds the `min-opt-level` key to the `[hints]` table. It lets a +package set the lowest optimization level that Cargo will use to build it: + +```toml +[hints] +min-opt-level = 2 +``` + +To enable this feature, pass `-Zhint-min-opt-level`. Without the flag, Cargo +warns and ignores the hint. Versions of Cargo prior to the introduction of this +feature will give an "unused manifest key" warning, but will otherwise function +without erroring. This means using the hint does not change the package's MSRV. + +### Documentation updates + +#### `min-opt-level` + +*as a new subsection of ["The `[hints]` section"](./manifest.html#the-hints-section), +which would gain one subsection per hint* + +The `min-opt-level` hint sets the lowest [`opt-level`](profiles.md#opt-level) +that Cargo will use to build this package. Some packages are very slow without +optimization, or take longer to build without it. Such a package can ask for a +minimum optimization level: + +```toml +# In example-dependency's Cargo.toml +[hints] +min-opt-level = 2 +``` + +The valid values are `0`, `1`, `2`, and `3`. Cargo warns about and ignores any +other value. The `"s"` and `"z"` levels are not valid, because they cannot be +compared with the numeric levels, and the top-level package is in a better +position to choose them. + +When the selected [profile](profiles.md) has a lower `opt-level` than the hint, +Cargo builds the package at the hinted level instead. When the profile has a +higher `opt-level`, Cargo keeps the higher level. This is true whether the +profile's `opt-level` is the default or is set by the user. It also applies to +the `opt-level = 0` default for +[build dependencies](profiles.md#build-dependencies). + +The top-level package can override a hint with a profile +[override](profiles.md#overrides). Any `opt-level` set in a `package` table, in +the `"*"` package, or in the `build-override` table takes precedence over the +hint. Setting `opt-level = "s"` or `"z"` in the profile also takes precedence, +because a package that optimizes for size usually wants its dependencies to do +the same. + +```toml +# Does not lower the `opt-level` below a dependency's hint. +[profile.dev] +opt-level = 0 + +# Overrides the hint for the `example-dependency` package. +[profile.dev.package.example-dependency] +opt-level = 0 + +# Overrides the hint for all dependencies. +[profile.dev.package."*"] +opt-level = 1 +``` + +A hint only applies to the package that sets it, not to its dependencies. If +the slow code is in a dependency, that dependency needs to set its own hint. + +Only use this hint when optimizing the package makes a full build faster, or +when the package is many times slower without optimization. + ## rustdoc-map * Tracking Issue: [#8296](https://github.com/rust-lang/cargo/issues/8296) diff --git a/src/compiler/standard_lib.rs b/src/compiler/standard_lib.rs index ba5e1bc12f9..7b9ccb8f62a 100644 --- a/src/compiler/standard_lib.rs +++ b/src/compiler/standard_lib.rs @@ -127,6 +127,7 @@ pub fn generate_std_roots( interner: &UnitInterner, profiles: &Profiles, target_data: &RustcTargetData<'_>, + hint_min_opt_level: bool, ) -> CargoResult>> { // Generate a map of Units for each kind requested. let mut ret = HashMap::default(); @@ -149,6 +150,7 @@ pub fn generate_std_roots( interner, profiles, target_data, + hint_min_opt_level, )?; } @@ -167,6 +169,7 @@ fn generate_roots( interner: &UnitInterner, profiles: &Profiles, target_data: &RustcTargetData<'_>, + hint_min_opt_level: bool, ) -> CargoResult<()> { let std_ids = std_crates(crates, default, units) .iter() @@ -191,6 +194,8 @@ fn generate_roots( let unit_for = UnitFor::new_normal(kind); let profile = profiles.get_profile( pkg.package_id(), + pkg.hints(), + hint_min_opt_level, /*is_member*/ false, /*is_local*/ false, unit_for, diff --git a/src/compiler/unit_dependencies.rs b/src/compiler/unit_dependencies.rs index ad442269734..89c0ed283da 100644 --- a/src/compiler/unit_dependencies.rs +++ b/src/compiler/unit_dependencies.rs @@ -885,6 +885,8 @@ fn new_unit_dep( let is_local = pkg.package_id().source_id().is_path() && !state.is_std; let profile = state.profiles.get_profile( pkg.package_id(), + pkg.hints(), + state.gctx.cli_unstable().hint_min_opt_level, state.ws.is_member(pkg), is_local, unit_for, diff --git a/src/diagnostics/mod.rs b/src/diagnostics/mod.rs index aed35eff8ba..97d8ef4df39 100644 --- a/src/diagnostics/mod.rs +++ b/src/diagnostics/mod.rs @@ -20,7 +20,7 @@ //! - TOML syntax or manifest schema: [`passes::emit_parse_diagnostics`], [`rules::PARSE_PASS_RULES`] //! - Lockfile //! - May be overly broad for what dependencies are checked -//! - Pre-build unit graph +//! - Pre-build unit graph: [`rules::min_opt_level_hint::diagnose`] //! - Tailored to a specific configuration (features, targets) but requires users to enumerate every configuration //! - Post-build unit graph: [`rules::unused_dependencies::lint_build_results`] //! - Slow feedback cycle since a build needs to happen @@ -69,7 +69,9 @@ pub mod passes; pub mod rules; pub use lint::{Lint, LintGroup, LintLevel, LintLevelProduct, LintLevelSource}; -pub use report::{AsIndex, cwd_rel_path, get_key_value, get_key_value_span, workspace_rel_path}; +pub use report::{ + AsIndex, TomlSpan, cwd_rel_path, get_key_value, get_key_value_span, workspace_rel_path, +}; pub use rules::{LINT_GROUPS, LINTS}; pub struct PassOutput { diff --git a/src/diagnostics/rules/min_opt_level_hint.rs b/src/diagnostics/rules/min_opt_level_hint.rs new file mode 100644 index 00000000000..6ec5fcfd833 --- /dev/null +++ b/src/diagnostics/rules/min_opt_level_hint.rs @@ -0,0 +1,88 @@ +use cargo_util_terminal::report::AnnotationKind; +use cargo_util_terminal::report::Group; +use cargo_util_terminal::report::Level; +use cargo_util_terminal::report::Origin; +use cargo_util_terminal::report::Snippet; + +use crate::CargoResult; +use crate::compiler::BuildContext; +use crate::diagnostics::TomlSpan; +use crate::diagnostics::get_key_value_span; +use crate::diagnostics::workspace_rel_path; +use crate::workspace::Package; +use crate::workspace::profiles::{MinOptLevelHintError, parse_min_opt_level_hint}; + +/// Emits diagnostics for `hints.min-opt-level` once per package selected for compilation. +#[tracing::instrument(skip_all)] +pub(crate) fn diagnose(bcx: &BuildContext<'_, '_>) -> CargoResult<()> { + let gctx = bcx.gctx; + let mut packages = bcx + .unit_graph + .keys() + .filter(|unit| !unit.skip_non_compile_time_dep && unit.show_warnings(gctx)) + .map(|unit| unit.pkg.clone()) + .collect::>(); + packages.sort_by_key(|pkg| pkg.package_id()); + packages.dedup_by_key(|pkg| pkg.package_id()); + + for pkg in packages { + let manifest_path = workspace_rel_path(bcx.ws, pkg.manifest_path()); + let min_opt_level = match parse_min_opt_level_hint( + pkg.hints().and_then(|hints| hints.min_opt_level.as_ref()), + ) { + Ok(level) => level, + Err(err) => { + let (title, label) = match err { + MinOptLevelHintError::OutOfRange(level) => ( + format!("ignoring unsupported value ({level}) for `hints.min-opt-level`"), + "expected an integer from 0 to 3", + ), + MinOptLevelHintError::WrongType(value_type) => ( + format!( + "ignoring unsupported value type ({value_type}) for `hints.min-opt-level`" + ), + "expected an integer", + ), + }; + let group = Group::with_title(Level::WARNING.primary_title(title)); + let group = match hint_span(&pkg) { + Some((contents, span)) => group.element( + Snippet::source(contents) + .path(&manifest_path) + .annotation(AnnotationKind::Primary.span(span.value).label(label)), + ), + None => group.element(Origin::path(&manifest_path)), + }; + gctx.shell().print_report(&[group], false)?; + None + } + }; + + if matches!(min_opt_level, Some(1..=3)) && !gctx.cli_unstable().hint_min_opt_level { + let group = + Group::with_title(Level::WARNING.primary_title("ignoring `hints.min-opt-level`")); + let group = match hint_span(&pkg) { + Some((contents, span)) => group.element( + Snippet::source(contents) + .path(&manifest_path) + .annotation(AnnotationKind::Primary.span(span.key.start..span.value.end)), + ), + None => group.element(Origin::path(&manifest_path)), + }; + let group = + group.element(Level::HELP.message("pass `-Zhint-min-opt-level` to enable it")); + gctx.shell().print_report(&[group], false)?; + } + } + + Ok(()) +} + +/// Locates `hints.min-opt-level` in the package's original manifest, if its source is available. +fn hint_span(pkg: &Package) -> Option<(&str, TomlSpan)> { + let manifest = pkg.manifest(); + let contents = manifest.contents()?; + let document = manifest.document()?; + let span = get_key_value_span(document, &["hints", "min-opt-level"])?; + Some((contents, span)) +} diff --git a/src/diagnostics/rules/mod.rs b/src/diagnostics/rules/mod.rs index ba02003d75f..a2c3d29bd8a 100644 --- a/src/diagnostics/rules/mod.rs +++ b/src/diagnostics/rules/mod.rs @@ -2,6 +2,7 @@ mod blanket_hint_mostly_unused; mod deferred_parse_diagnostics; mod im_a_teapot; mod manual_readme; +pub mod min_opt_level_hint; mod missing_lints_features; mod missing_lints_inheritance; mod non_kebab_case_bins; diff --git a/src/ops/cargo_compile/mod.rs b/src/ops/cargo_compile/mod.rs index 084804d371e..28d5b3d421b 100644 --- a/src/ops/cargo_compile/mod.rs +++ b/src/ops/cargo_compile/mod.rs @@ -196,6 +196,7 @@ fn compile_ws<'a>( } let bcx = create_bcx(ws, options, &interner, logger.as_ref())?; + crate::diagnostics::rules::min_opt_level_hint::diagnose(&bcx)?; if options.build_config.unit_graph { unit_graph::emit_serialized_unit_graph(&bcx.roots, &bcx.unit_graph, ws.gctx())?; @@ -497,6 +498,7 @@ pub fn create_bcx<'a, 'gctx>( interner, &profiles, &target_data, + gctx.cli_unstable().hint_min_opt_level, )? } else { Default::default() diff --git a/src/ops/cargo_compile/unit_generator.rs b/src/ops/cargo_compile/unit_generator.rs index bd59631d5da..d998ab736ee 100644 --- a/src/ops/cargo_compile/unit_generator.rs +++ b/src/ops/cargo_compile/unit_generator.rs @@ -148,6 +148,8 @@ impl<'a> UnitGenerator<'a, '_> { }; let profile = self.profiles.get_profile( pkg.package_id(), + pkg.hints(), + self.ws.gctx().cli_unstable().hint_min_opt_level, self.ws.is_member(pkg), is_local, unit_for, diff --git a/src/workspace/features.rs b/src/workspace/features.rs index a1ac9155e55..ad45b88356f 100644 --- a/src/workspace/features.rs +++ b/src/workspace/features.rs @@ -902,6 +902,7 @@ unstable_cli_options!( git: Option = ("Enable support for shallow git fetch operations"), #[serde(deserialize_with = "deserialize_gitoxide_features")] gitoxide: Option = ("Use gitoxide for the given git interactions, or all of them if no argument is given"), + hint_min_opt_level: bool = ("Enable the `hints.min-opt-level` manifest key"), hint_msrv: bool = ("Enable passing `package.rust-version` to rustc for lints"), host_config: bool = ("Enable the `[host]` section in the .cargo/config.toml file"), json_target_spec: bool = ("Enable `.json` target spec files"), @@ -1440,6 +1441,7 @@ impl CliUnstable { |v| parse_gitoxide(v.split(',')), )? } + "hint-min-opt-level" => self.hint_min_opt_level = parse_empty(k, v)?, "host-config" => self.host_config = parse_empty(k, v)?, "json-target-spec" => self.json_target_spec = parse_empty(k, v)?, "hint-msrv" => self.hint_msrv = parse_empty(k, v)?, diff --git a/src/workspace/profiles.rs b/src/workspace/profiles.rs index e7ac89416a9..0d0c2e3f81b 100644 --- a/src/workspace/profiles.rs +++ b/src/workspace/profiles.rs @@ -33,7 +33,7 @@ use crate::workspace::parser::validate_profile; use crate::workspace::{PackageId, PackageIdSpec, PackageIdSpecQuery, Target, Workspace}; use anyhow::{Context as _, bail}; use cargo_util::is_ci; -use cargo_util_schemas::manifest::TomlTrimPaths; +use cargo_util_schemas::manifest::{Hints, TomlTrimPaths}; use cargo_util_schemas::manifest::{ ProfilePackageSpec, StringOrBool, TomlDebugInfo, TomlProfile, TomlProfiles, }; @@ -272,16 +272,33 @@ impl Profiles { /// Retrieves the profile for a target. /// `is_member` is whether or not this package is a member of the /// workspace. + /// `hint_min_opt_level` is whether `-Zhint-min-opt-level` was passed, + /// enabling the `hints.min-opt-level` manifest key. pub fn get_profile( &self, pkg_id: PackageId, + pkg_hints: Option<&Hints>, + hint_min_opt_level: bool, is_member: bool, is_local: bool, unit_for: UnitFor, kind: CompileKind, ) -> Profile { let maker = self.get_profile_maker(&self.requested_profile).unwrap(); - let mut profile = maker.get_profile(Some(pkg_id), is_member, unit_for.is_for_host()); + let min_opt_level = if hint_min_opt_level { + parse_min_opt_level_hint(pkg_hints.and_then(|hints| hints.min_opt_level.as_ref())) + .ok() + .flatten() + } else { + None + }; + + let mut profile = maker.get_profile( + Some(pkg_id), + is_member, + unit_for.is_for_host(), + min_opt_level, + ); // Dealing with `panic=abort` and `panic=unwind` requires some special // treatment. Be sure to process all the various options here. @@ -345,7 +362,9 @@ impl Profiles { pub fn base_profile(&self) -> Profile { let profile_name = self.requested_profile; let maker = self.get_profile_maker(&profile_name).unwrap(); - maker.get_profile(None, /*is_member*/ true, /*is_for_host*/ false) + maker.get_profile( + None, /*is_member*/ true, /*is_for_host*/ false, None, + ) } /// Gets the directory name for a profile, like `debug` or `release`. @@ -403,6 +422,27 @@ impl Profiles { } } +pub(crate) enum MinOptLevelHintError { + OutOfRange(i64), + WrongType(&'static str), +} + +pub(crate) fn parse_min_opt_level_hint( + value: Option<&toml::Value>, +) -> Result, MinOptLevelHintError> { + let Some(value) = value else { + return Ok(None); + }; + let Some(level) = value.as_integer() else { + return Err(MinOptLevelHintError::WrongType(value.type_str())); + }; + if (0..=3).contains(&level) { + Ok(Some(level as u32)) + } else { + Err(MinOptLevelHintError::OutOfRange(level)) + } +} + /// An object used for handling the profile hierarchy. /// /// The precedence of profiles are (first one wins): @@ -440,6 +480,7 @@ impl ProfileMaker { pkg_id: Option, is_member: bool, is_for_host: bool, + min_opt_level: Option, ) -> Profile { let mut profile = self.default.clone(); @@ -475,6 +516,13 @@ impl ProfileMaker { // the unit's debuginfo. profile.debuginfo = DebugInfo::Deferred(profile.debuginfo.into_inner()); } + if let (Some(min_opt_level), Ok(opt_level)) = + (min_opt_level, profile.opt_level.as_str().parse::()) + { + if opt_level < min_opt_level { + profile.opt_level = min_opt_level.to_string().into(); + } + } // ... and next comes any other sorts of overrides specified in // profiles, such as `[profile.release.build-override]` or // `[profile.release.package.foo]` diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index 80f9e2b4b9c..be641821db3 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -1,4 +1,4 @@ - +