diff --git a/crates/cargo-util-schemas/manifest.schema.json b/crates/cargo-util-schemas/manifest.schema.json index 2024fea8411..5d6d45b05a4 100644 --- a/crates/cargo-util-schemas/manifest.schema.json +++ b/crates/cargo-util-schemas/manifest.schema.json @@ -167,6 +167,16 @@ } ] }, + "hints": { + "anyOf": [ + { + "$ref": "#/$defs/Hints" + }, + { + "type": "null" + } + ] + }, "workspace": { "anyOf": [ { @@ -998,6 +1008,21 @@ "$ref": "#/$defs/TomlValue" } }, + "Hints": { + "type": "object", + "properties": { + "mostly-unused": { + "anyOf": [ + { + "$ref": "#/$defs/TomlValue" + }, + { + "type": "null" + } + ] + } + } + }, "TomlWorkspace": { "type": "object", "properties": { diff --git a/crates/cargo-util-schemas/src/manifest/mod.rs b/crates/cargo-util-schemas/src/manifest/mod.rs index b28aa38fb60..b73f8f46fe6 100644 --- a/crates/cargo-util-schemas/src/manifest/mod.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -56,6 +56,7 @@ pub struct TomlManifest { pub build_dependencies2: Option>, pub target: Option>, pub lints: Option, + pub hints: Option, pub workspace: Option, pub profile: Option, @@ -85,6 +86,7 @@ impl TomlManifest { .map(|_| "build-dependencies"), self.target.as_ref().map(|_| "target"), self.lints.as_ref().map(|_| "lints"), + self.hints.as_ref().map(|_| "hints"), ] .into_iter() .flatten() @@ -1641,6 +1643,17 @@ pub enum TomlLintLevel { Allow, } +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[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 mostly_unused: Option, +} + #[derive(Copy, Clone, Debug)] pub struct InvalidCargoFeatures {} diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 2775e1d22ce..3189d198415 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -1135,11 +1135,27 @@ fn build_base_args( strip, rustflags: profile_rustflags, trim_paths, - hint_mostly_unused, + hint_mostly_unused: profile_hint_mostly_unused, .. } = unit.profile.clone(); + let hints = unit.pkg.hints().cloned().unwrap_or_default(); let test = unit.mode.is_any_test(); + let warn = |msg: &str| { + bcx.gctx.shell().warn(format!( + "{}@{}: {msg}", + unit.pkg.package_id().name(), + unit.pkg.package_id().version() + )) + }; + let unit_capped_warn = |msg: &str| { + if unit.show_warnings(bcx.gctx) { + warn(msg) + } else { + Ok(()) + } + }; + cmd.arg("--crate-name").arg(&unit.target.crate_name()); let edition = unit.target.edition(); @@ -1326,13 +1342,30 @@ fn build_base_args( opt(cmd, "-C", "incremental=", Some(dir)); } - if hint_mostly_unused { + let pkg_hint_mostly_unused = match hints.mostly_unused { + None => None, + Some(toml::Value::Boolean(b)) => Some(b), + Some(v) => { + unit_capped_warn(&format!( + "ignoring unsupported value type ({}) for 'hints.mostly-unused', which expects a boolean", + v.type_str() + ))?; + None + } + }; + if profile_hint_mostly_unused + .or(pkg_hint_mostly_unused) + .unwrap_or(false) + { if bcx.gctx.cli_unstable().profile_hint_mostly_unused { cmd.arg("-Zhint-mostly-unused"); } else { - bcx.gctx - .shell() - .warn("ignoring 'hint-mostly-unused' profile option, pass `-Zprofile-hint-mostly-unused` to enable it")?; + if profile_hint_mostly_unused.is_some() { + // Profiles come from the top-level unit, so we don't use `unit_capped_warn` here. + warn("ignoring 'hint-mostly-unused' profile option, pass `-Zprofile-hint-mostly-unused` to enable it")?; + } else if pkg_hint_mostly_unused.is_some() { + unit_capped_warn("ignoring 'hints.mostly-unused', pass `-Zprofile-hint-mostly-unused` to enable it")?; + } } } diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index 18bd6210215..54bebf5ff8f 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use anyhow::Context as _; use cargo_util_schemas::manifest::RustVersion; -use cargo_util_schemas::manifest::{TomlManifest, TomlProfiles}; +use cargo_util_schemas::manifest::{Hints, TomlManifest, TomlProfiles}; use semver::Version; use serde::ser; use serde::Serialize; @@ -90,6 +90,7 @@ pub struct Manifest { metabuild: Option>, resolve_behavior: Option, lint_rustflags: Vec, + hints: Option, embedded: bool, } @@ -521,6 +522,7 @@ impl Manifest { metabuild: Option>, resolve_behavior: Option, lint_rustflags: Vec, + hints: Option, embedded: bool, ) -> Manifest { Manifest { @@ -551,6 +553,7 @@ impl Manifest { metabuild, resolve_behavior, lint_rustflags, + hints, embedded, } } @@ -668,6 +671,10 @@ impl Manifest { self.lint_rustflags.as_slice() } + pub fn hints(&self) -> Option<&Hints> { + self.hints.as_ref() + } + pub fn map_source(self, to_replace: SourceId, replace_with: SourceId) -> Manifest { Manifest { summary: self.summary.map_source(to_replace, replace_with), diff --git a/src/cargo/core/package.rs b/src/cargo/core/package.rs index ebb10aca108..49996054a3e 100644 --- a/src/cargo/core/package.rs +++ b/src/cargo/core/package.rs @@ -9,7 +9,7 @@ use std::rc::Rc; use std::time::{Duration, Instant}; use anyhow::Context as _; -use cargo_util_schemas::manifest::RustVersion; +use cargo_util_schemas::manifest::{Hints, RustVersion}; use curl::easy::Easy; use curl::multi::{EasyHandle, Multi}; use lazycell::LazyCell; @@ -95,6 +95,8 @@ pub struct SerializedPackage { metabuild: Option>, default_run: Option, rust_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + hints: Option, } impl Package { @@ -172,6 +174,11 @@ impl Package { self.manifest().rust_version() } + /// Gets the package's hints. + pub fn hints(&self) -> Option<&Hints> { + self.manifest().hints() + } + /// Returns `true` if the package uses a custom build script for any target. pub fn has_custom_build(&self) -> bool { self.targets().iter().any(|t| t.is_custom_build()) @@ -241,6 +248,7 @@ impl Package { publish: self.publish().as_ref().cloned(), default_run: self.manifest().default_run().map(|s| s.to_owned()), rust_version: self.rust_version().cloned(), + hints: self.hints().cloned(), } } } diff --git a/src/cargo/core/profiles.rs b/src/cargo/core/profiles.rs index 0989475a7d5..7fcaeee3d7f 100644 --- a/src/cargo/core/profiles.rs +++ b/src/cargo/core/profiles.rs @@ -578,7 +578,7 @@ fn merge_profile(profile: &mut Profile, toml: &TomlProfile) { profile.trim_paths = Some(trim_paths.clone()); } if let Some(hint_mostly_unused) = toml.hint_mostly_unused { - profile.hint_mostly_unused = hint_mostly_unused; + profile.hint_mostly_unused = Some(hint_mostly_unused); } profile.strip = match toml.strip { Some(StringOrBool::Bool(true)) => Strip::Resolved(StripInner::Named("symbols".into())), @@ -629,8 +629,8 @@ pub struct Profile { // remove when `-Ztrim-paths` is stablized #[serde(skip_serializing_if = "Option::is_none")] pub trim_paths: Option, - #[serde(skip_serializing_if = "std::ops::Not::not")] - pub hint_mostly_unused: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub hint_mostly_unused: Option, } impl Default for Profile { @@ -652,7 +652,7 @@ impl Default for Profile { strip: Strip::Deferred(StripInner::None), rustflags: vec![], trim_paths: None, - hint_mostly_unused: false, + hint_mostly_unused: None, } } } diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 83d10678368..8e89a71b9f3 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -316,6 +316,7 @@ fn normalize_toml( build_dependencies2: None, target: None, lints: None, + hints: None, workspace: original_toml.workspace.clone().or_else(|| { // Prevent looking for a workspace by `read_manifest_from_str` is_embedded.then(manifest::TomlWorkspace::default) @@ -559,6 +560,8 @@ fn normalize_toml( lints, }); + normalized_toml.hints = original_toml.hints.clone(); + normalized_toml.badges = original_toml.badges.clone(); } else { if let Some(field) = original_toml.requires_package().next() { @@ -1608,6 +1611,8 @@ pub fn to_real_manifest( .unwrap_or(&default), )?; + let hints = normalized_toml.hints.clone(); + let metadata = ManifestMetadata { description: normalized_package .normalized_description() @@ -1799,6 +1804,7 @@ pub fn to_real_manifest( metabuild, resolve_behavior, rustflags, + hints, is_embedded, ); if manifest @@ -3050,6 +3056,7 @@ fn prepare_toml_for_publish( None => None, }, lints: me.lints.clone(), + hints: me.hints.clone(), workspace: None, profile: me.profile.clone(), patch: None, diff --git a/src/doc/src/reference/manifest.md b/src/doc/src/reference/manifest.md index 3d0a6f1b1ba..306295869f9 100644 --- a/src/doc/src/reference/manifest.md +++ b/src/doc/src/reference/manifest.md @@ -50,6 +50,7 @@ Every manifest file consists of the following sections: * [`[badges]`](#the-badges-section) --- Badges to display on a registry. * [`[features]`](features.md) --- Conditional compilation features. * [`[lints]`](#the-lints-section) --- Configure linters for this package. +* [`[hints]`](#the-hints-section) --- Provide hints for compiling this package. * [`[patch]`](overriding-dependencies.md#the-patch-section) --- Override dependencies. * [`[replace]`](overriding-dependencies.md#the-replace-section) --- Override dependencies (deprecated). * [`[profile]`](profiles.md) --- Compiler settings and optimizations. @@ -565,6 +566,25 @@ As for dependents, Cargo suppresses lints from non-path dependencies with featur > **MSRV:** Respected as of 1.74 +## The `[hints]` section + +The `[hints]` section allows specifying hints for compiling this crate, which +crates depending on this one will use by default but may override. Hints are, +by design, always safe for Cargo to ignore; if Cargo encounters a hint it +doesn't understand, or a hint it understands but with a value it doesn't +understand, it will warn, but not error. + +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. + +> **MSRV:** Specifying hints does not impact MSRV. Older versions of Cargo will +> ignore hints with a warning, but will not error. + ## The `[badges]` section The `[badges]` section is for specifying status badges that can be displayed diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index d9022d761d2..60b96110071 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -944,6 +944,18 @@ introduction of this feature will give an "unused manifest key" warning, but will otherwise function without erroring. This allows using the hint in a crate's `Cargo.toml` without mandating the use of a newer Cargo to build it. +A crate can also provide this hint automatically for crates that depend on it, +using the `[hints]` table (which will likewise be ignored by older Cargo): + +```toml +[hints] +mostly-unused = true +``` + +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. + ## rustdoc-map * Tracking Issue: [#8296](https://github.com/rust-lang/cargo/issues/8296) diff --git a/tests/testsuite/hints.rs b/tests/testsuite/hints.rs new file mode 100644 index 00000000000..39ca81b7d48 --- /dev/null +++ b/tests/testsuite/hints.rs @@ -0,0 +1,316 @@ +//! Tests for hints. + +use cargo_test_support::prelude::*; +use cargo_test_support::registry::Package; +use cargo_test_support::{project, str}; + +#[cargo_test] +fn empty_hints_no_warn() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [hints] + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + p.cargo("check -v") + .with_stderr_data(str![[r#" +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustc --crate-name foo [..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn unknown_hints_warn() { + Package::new("bar", "1.0.0") + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "1.0.0" + edition = "2015" + + [hints] + this-is-an-unknown-hint = true + "#, + ) + .file("src/lib.rs", "") + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [hints] + this-is-an-unknown-hint = true + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + p.cargo("check -v") + .with_stderr_data(str![[r#" +[WARNING] unused manifest key: hints.this-is-an-unknown-hint +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustc --crate-name foo [..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn hint_unknown_type_warn() { + Package::new("bar", "1.0.0") + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "1.0.0" + edition = "2015" + + [hints] + mostly-unused = 1 + "#, + ) + .file("src/lib.rs", "") + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [dependencies] + bar = "1.0" + + [hints] + mostly-unused = "string" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + p.cargo("check -v") + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] bar v1.0.0 (registry `dummy-registry`) +[WARNING] foo@0.0.1: ignoring unsupported value type (string) for 'hints.mostly-unused', which expects a boolean +[CHECKING] bar v1.0.0 +[RUNNING] `rustc --crate-name bar [..]` +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustc --crate-name foo [..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .with_stderr_does_not_contain("-Zhint-mostly-unused") + .run(); +} + +#[cargo_test] +fn hints_mostly_unused_warn_without_gate() { + Package::new("bar", "1.0.0") + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "1.0.0" + edition = "2015" + + [hints] + mostly-unused = true + "#, + ) + .file("src/lib.rs", "") + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [dependencies] + bar = "1.0" + + [hints] + mostly-unused = true + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + p.cargo("check -v") + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] bar v1.0.0 (registry `dummy-registry`) +[WARNING] foo@0.0.1: ignoring 'hints.mostly-unused', pass `-Zprofile-hint-mostly-unused` to enable it +[CHECKING] bar v1.0.0 +[RUNNING] `rustc --crate-name bar [..]` +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustc --crate-name foo [..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .with_stderr_does_not_contain("-Zhint-mostly-unused") + .run(); +} + +#[cargo_test(nightly, reason = "-Zhint-mostly-unused is unstable")] +fn hints_mostly_unused_nightly() { + Package::new("bar", "1.0.0") + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "1.0.0" + edition = "2015" + + [hints] + mostly-unused = true + "#, + ) + .file("src/lib.rs", "") + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [dependencies] + bar = "1.0" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + p.cargo("check -Zprofile-hint-mostly-unused -v") + .masquerade_as_nightly_cargo(&["profile-hint-mostly-unused"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] bar v1.0.0 (registry `dummy-registry`) +[CHECKING] bar v1.0.0 +[RUNNING] `rustc --crate-name bar [..] -Zhint-mostly-unused [..]` +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustc --crate-name foo [..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .with_stderr_does_not_contain( + "[RUNNING] `rustc --crate-name foo [..] -Zhint-mostly-unused [..]", + ) + .run(); +} + +#[cargo_test(nightly, reason = "-Zhint-mostly-unused is unstable")] +fn mostly_unused_profile_overrides_hints_nightly() { + Package::new("bar", "1.0.0") + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "1.0.0" + edition = "2015" + + [hints] + mostly-unused = true + "#, + ) + .file("src/lib.rs", "") + .publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [dependencies] + bar = "1.0" + + [profile.dev.package.bar] + hint-mostly-unused = false + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + p.cargo("check -Zprofile-hint-mostly-unused -v") + .masquerade_as_nightly_cargo(&["profile-hint-mostly-unused"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[DOWNLOADING] crates ... +[DOWNLOADED] bar v1.0.0 (registry `dummy-registry`) +[CHECKING] bar v1.0.0 +[RUNNING] `rustc --crate-name bar [..]` +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustc --crate-name foo [..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .with_stderr_does_not_contain("-Zhint-mostly-unused") + .run(); +} + +#[cargo_test(nightly, reason = "-Zhint-mostly-unused is unstable")] +fn mostly_unused_profile_overrides_hints_on_self_nightly() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [hints] + mostly-unused = true + + [profile.dev] + hint-mostly-unused = false + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + p.cargo("check -v") + .with_stderr_data(str![[r#" +[CHECKING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustc --crate-name foo [..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .with_stderr_does_not_contain("-Zhint-mostly-unused") + .run(); +} diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 038953dc425..46533a0c03c 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -105,6 +105,7 @@ mod git_shallow; mod glob_targets; mod global_cache_tracker; mod help; +mod hints; mod https; mod inheritable_workspace_fields; mod install; diff --git a/tests/testsuite/profiles.rs b/tests/testsuite/profiles.rs index 73a140395b9..0352d36953a 100644 --- a/tests/testsuite/profiles.rs +++ b/tests/testsuite/profiles.rs @@ -904,7 +904,7 @@ fn profile_hint_mostly_unused_warn_without_gate() { [LOCKING] 1 package to latest compatible version [DOWNLOADING] crates ... [DOWNLOADED] bar v1.0.0 (registry `dummy-registry`) -[WARNING] ignoring 'hint-mostly-unused' profile option, pass `-Zprofile-hint-mostly-unused` to enable it +[WARNING] bar@1.0.0: ignoring 'hint-mostly-unused' profile option, pass `-Zprofile-hint-mostly-unused` to enable it [CHECKING] bar v1.0.0 [RUNNING] `rustc --crate-name bar [..]` [CHECKING] foo v0.0.1 ([ROOT]/foo)