Skip to content
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ cargo-platform = { path = "crates/cargo-platform", version = "0.3.3" }
cargo-test-macro = { version = "0.4.15", path = "crates/cargo-test-macro" }
cargo-test-support = { version = "0.12.0", path = "crates/cargo-test-support" }
cargo-util = { version = "0.2.33", path = "crates/cargo-util" }
cargo-util-schemas = { version = "0.14.4", path = "crates/cargo-util-schemas" }
cargo-util-schemas = { version = "0.15.0", path = "crates/cargo-util-schemas" }
cargo-util-terminal = { version = "0.1.3", path = "crates/cargo-util-terminal" }
cargo_metadata = "0.23.1"
clap = "4.6.0"
Expand Down
2 changes: 1 addition & 1 deletion crates/cargo-util-schemas/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cargo-util-schemas"
version = "0.14.4"
version = "0.15.0"
Comment thread
0xPoe marked this conversation as resolved.
rust-version = "1.98" # MSRV:1
edition.workspace = true
license.workspace = true
Expand Down
16 changes: 15 additions & 1 deletion crates/cargo-util-schemas/manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,16 @@
"Hints": {
"type": "object",
"properties": {
"min-opt-level": {
"anyOf": [
{
"$ref": "#/$defs/TomlValue"
},
{
"type": "null"
}
]
},
"mostly-unused": {
"anyOf": [
{
Expand Down Expand Up @@ -1444,7 +1454,11 @@
"type": "string"
},
"TomlDebugInfo": {
"type": ["string", "integer", "boolean"],
"type": [
Comment thread
0xPoe marked this conversation as resolved.
"string",
"integer",
"boolean"
],
"enum": [
"none",
"line-directives-only",
Expand Down
5 changes: 5 additions & 0 deletions crates/cargo-util-schemas/src/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,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<TomlValueWrapper>")
)]
pub min_opt_level: Option<toml::Value>,
#[cfg_attr(
feature = "unstable-schema",
schemars(with = "Option<TomlValueWrapper>")
Expand Down
6 changes: 3 additions & 3 deletions doc/book/src/reference/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,9 +590,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.

Expand Down
49 changes: 49 additions & 0 deletions doc/book/src/reference/unstable.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -923,6 +924,54 @@ 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

@epage epage Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you have a subsection that is written as-if it was the end-user documentation?

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Could you give an example of the kind of end-user documentation you expect? What should it include? I thought the current document already covered the basic usage and examples.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not just looking for how well it is explain but find it helpful to literally have the docs that will go into any guide or reference section written out in the Unstable chapter with asides saying where they will go. I generally ask for this to be written in the RFC but seemed to have overlooked it in this case.

For example, will the docs live under https://doc.rust-lang.org/cargo/reference/manifest.html?highlight=hint#the-hints-section ? What will the heading be? How do we want to organize these?

* Tracking Issue: [#17334](https://github.com/rust-lang/cargo/issues/17334)
* RFC: [#3924](https://github.com/rust-lang/rfcs/pull/3924)

The `min-opt-level` hint lets a package request a numeric optimization floor
for itself:

```toml
# In example-dependency's Cargo.toml
[hints]
min-opt-level = 2
```

To enable this feature, pass `-Zhint-min-opt-level`. Without the flag, Cargo
warns and ignores a positive hint. Older versions of Cargo may instead report
an unused manifest key, but specifying the hint does not change the package's
MSRV.

The hint accepts the numeric optimization levels 0, 1, 2, and 3. Other values
produce a warning and are ignored. If the selected [profile](profiles.md) has a
Comment thread
epage marked this conversation as resolved.
lower numeric [`opt-level`](profiles.md#opt-level), Cargo raises it to the
hinted minimum for that package. A higher numeric level remains unchanged. The
hint also applies to Cargo's built-in `opt-level = 0` default for build
dependencies. The size optimization levels `"s"` and `"z"` override a numeric
hint.

Explicit [package overrides](profiles.md#overrides) from the top-level package,
including `[profile.dev.package."*"]`, take precedence over a hint, as do
explicit build overrides. A lower numeric `opt-level` in the general profile
cannot reduce the hinted floor. For example:

```toml
# In the top-level package's Cargo.toml

# Does not affect example-dependency: the hint keeps it at opt-level 2.
[profile.dev]
opt-level = 0

# Takes precedence over the hint, forcing example-dependency back to no
# optimization when debugging it.
[profile.dev.package.example-dependency]
opt-level = 0
```

A hint only affects the package that declares it, not its dependencies. Use
this hint when optimization either makes a typical full build faster or when
the package is many times slower without it.

## rustdoc-map
* Tracking Issue: [#8296](https://github.com/rust-lang/cargo/issues/8296)

Expand Down
5 changes: 5 additions & 0 deletions src/compiler/standard_lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ pub fn generate_std_roots(
interner: &UnitInterner,
profiles: &Profiles,
target_data: &RustcTargetData<'_>,
hint_min_opt_level: bool,
) -> CargoResult<HashMap<CompileKind, Vec<Unit>>> {
// Generate a map of Units for each kind requested.
let mut ret = HashMap::default();
Expand All @@ -149,6 +150,7 @@ pub fn generate_std_roots(
interner,
profiles,
target_data,
hint_min_opt_level,
)?;
}

Expand All @@ -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()
Expand All @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/unit_dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions src/diagnostics/rules/min_opt_level_hint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use crate::CargoResult;
use crate::compiler::BuildContext;
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::<Vec<_>>();
packages.sort_by_key(|pkg| pkg.package_id());
packages.dedup_by_key(|pkg| pkg.package_id());

for pkg in packages {
let warn = |message: &str| {
gctx.shell()
.warn(format!("{}@{}: {message}", pkg.name(), pkg.version()))
};
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(MinOptLevelHintError::OutOfRange(level)) => {
warn(&format!(
"ignoring unsupported value ({level}) for 'hints.min-opt-level', which only supports integers from 0 to 3"
))?;
None
}
Err(MinOptLevelHintError::WrongType(value_type)) => {
warn(&format!(
"ignoring unsupported value type ({value_type}) for 'hints.min-opt-level', which expects an integer"
))?;
None
}
};
Comment on lines +19 to +39

@epage epage Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we're keeping these long term, let's render them "correctly" using annotate-snippets

View changes since the review


if matches!(min_opt_level, Some(1..=3)) && !gctx.cli_unstable().hint_min_opt_level {
warn("ignoring 'hints.min-opt-level', pass `-Zhint-min-opt-level` to enable it")?;
}
}

Ok(())
}
1 change: 1 addition & 0 deletions src/diagnostics/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/ops/cargo_compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;
Expand Down Expand Up @@ -497,6 +498,7 @@ pub fn create_bcx<'a, 'gctx>(
interner,
&profiles,
&target_data,
gctx.cli_unstable().hint_min_opt_level,
)?
} else {
Default::default()
Expand Down
2 changes: 2 additions & 0 deletions src/ops/cargo_compile/unit_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/workspace/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,7 @@ unstable_cli_options!(
git: Option<GitFeatures> = ("Enable support for shallow git fetch operations"),
#[serde(deserialize_with = "deserialize_gitoxide_features")]
gitoxide: Option<GitoxideFeatures> = ("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"),
Expand Down Expand Up @@ -1437,6 +1438,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)?,
Expand Down
54 changes: 51 additions & 3 deletions src/workspace/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ 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::TomlTrimPathsValue;
use cargo_util_schemas::manifest::{Hints, TomlTrimPaths};
use cargo_util_schemas::manifest::{
ProfilePackageSpec, StringOrBool, TomlDebugInfo, TomlProfile, TomlProfiles,
};
Expand Down Expand Up @@ -280,16 +280,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,
Comment thread
0xPoe marked this conversation as resolved.
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,
);
Comment thread
epage marked this conversation as resolved.

// Dealing with `panic=abort` and `panic=unwind` requires some special
// treatment. Be sure to process all the various options here.
Expand Down Expand Up @@ -353,7 +370,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`.
Expand Down Expand Up @@ -411,6 +430,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<Option<u32>, 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):
Expand Down Expand Up @@ -448,6 +488,7 @@ impl ProfileMaker {
pkg_id: Option<PackageId>,
is_member: bool,
is_for_host: bool,
min_opt_level: Option<u32>,
) -> Profile {
let mut profile = self.default.clone();

Expand Down Expand Up @@ -483,6 +524,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::<u32>())
{
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]`
Expand Down
Loading