From 0d429de262ac1f471678ec40f5a42ddfe8c56da3 Mon Sep 17 00:00:00 2001 From: Enrico Bolzonello Date: Sun, 8 Mar 2026 18:56:25 +0100 Subject: [PATCH] feat: add frame-pointers profile option Add `frame-pointers` as a profile setting that maps to `-C force-frame-pointers`. Uses string values (for now `"force-on"`, `"force-off"` and `"default"`) to allow future variants to be added. This allows to turn on frame pointers for a particular profile in `Cargo.toml`, addressing issue #15333. --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/cargo-util-schemas/Cargo.toml | 2 +- .../cargo-util-schemas/manifest.schema.json | 7 + crates/cargo-util-schemas/src/manifest/mod.rs | 5 + src/cargo/core/compiler/mod.rs | 11 +- src/cargo/core/profiles.rs | 40 +++++- src/cargo/util/toml/mod.rs | 12 ++ src/doc/src/reference/profiles.md | 20 +++ tests/testsuite/config.rs | 1 + tests/testsuite/profiles.rs | 133 ++++++++++++++++++ 11 files changed, 230 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6b6480de1d..01617e08607 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,7 +574,7 @@ dependencies = [ [[package]] name = "cargo-util-schemas" -version = "0.13.1" +version = "0.14.0" dependencies = [ "jiff", "schemars", diff --git a/Cargo.toml b/Cargo.toml index e408e8e39bd..22443decaf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ cargo-platform = { path = "crates/cargo-platform", version = "0.3.3" } cargo-test-macro = { version = "0.4.10", path = "crates/cargo-test-macro" } cargo-test-support = { version = "0.11.0", path = "crates/cargo-test-support" } cargo-util = { version = "0.2.28", path = "crates/cargo-util" } -cargo-util-schemas = { version = "0.13.0", path = "crates/cargo-util-schemas" } +cargo-util-schemas = { version = "0.14.0", path = "crates/cargo-util-schemas" } cargo_metadata = "0.23.1" clap = "4.5.60" clap_complete = { version = "4.5.66", features = ["unstable-dynamic"] } diff --git a/crates/cargo-util-schemas/Cargo.toml b/crates/cargo-util-schemas/Cargo.toml index a205864220c..94003480a6e 100644 --- a/crates/cargo-util-schemas/Cargo.toml +++ b/crates/cargo-util-schemas/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-util-schemas" -version = "0.13.1" +version = "0.14.0" rust-version = "1.94" # MSRV:1 edition.workspace = true license.workspace = true diff --git a/crates/cargo-util-schemas/manifest.schema.json b/crates/cargo-util-schemas/manifest.schema.json index ec63f68ee0c..ae5f01c23c2 100644 --- a/crates/cargo-util-schemas/manifest.schema.json +++ b/crates/cargo-util-schemas/manifest.schema.json @@ -1437,6 +1437,13 @@ "null" ], "default": null + }, + "frame-pointers": { + "type": [ + "string", + "null" + ], + "default": null } } }, diff --git a/crates/cargo-util-schemas/src/manifest/mod.rs b/crates/cargo-util-schemas/src/manifest/mod.rs index c08ea423d62..2e329820303 100644 --- a/crates/cargo-util-schemas/src/manifest/mod.rs +++ b/crates/cargo-util-schemas/src/manifest/mod.rs @@ -941,6 +941,7 @@ pub struct TomlProfile { pub trim_paths: Option, /// Unstable feature `hint-mostly-unused` pub hint_mostly_unused: Option, + pub frame_pointers: Option, } impl TomlProfile { @@ -1036,6 +1037,10 @@ impl TomlProfile { if let Some(v) = profile.hint_mostly_unused { self.hint_mostly_unused = Some(v); } + + if let Some(v) = &profile.frame_pointers { + self.frame_pointers = Some(v.clone()); + } } } diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index ea3cc7f9c63..4a583c645bc 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -101,7 +101,7 @@ pub use crate::core::compiler::unit::Unit; pub use crate::core::compiler::unit::UnitIndex; pub use crate::core::compiler::unit::UnitInterner; use crate::core::manifest::TargetSourcePath; -use crate::core::profiles::{PanicStrategy, Profile, StripInner}; +use crate::core::profiles::{FramePointers, PanicStrategy, Profile, StripInner}; use crate::core::{Feature, PackageId, Target, Verbosity}; use crate::lints::get_key_value; use crate::util::OnceExt; @@ -1228,6 +1228,7 @@ fn build_base_args( rustflags: profile_rustflags, trim_paths, hint_mostly_unused: profile_hint_mostly_unused, + frame_pointers, .. } = unit.profile.clone(); let hints = unit.pkg.hints().cloned().unwrap_or_default(); @@ -1450,6 +1451,14 @@ fn build_base_args( cmd.arg("-C").arg(format!("strip={}", strip)); } + if let Some(frame_pointers) = frame_pointers { + let val = match frame_pointers { + FramePointers::ForceOn => "on", + FramePointers::ForceOff => "off", + }; + cmd.arg("-C").arg(format!("force-frame-pointers={}", val)); + } + if unit.is_std { // -Zforce-unstable-if-unmarked prevents the accidental use of // unstable crates within the sysroot (such as "extern crate libc" or diff --git a/src/cargo/core/profiles.rs b/src/cargo/core/profiles.rs index c98188d33d4..515a75a3b2f 100644 --- a/src/cargo/core/profiles.rs +++ b/src/cargo/core/profiles.rs @@ -582,6 +582,15 @@ fn merge_profile(profile: &mut Profile, toml: &TomlProfile) { if let Some(hint_mostly_unused) = toml.hint_mostly_unused { profile.hint_mostly_unused = Some(hint_mostly_unused); } + if let Some(ref frame_pointers) = toml.frame_pointers { + profile.frame_pointers = match frame_pointers.as_str() { + "force-on" => Some(FramePointers::ForceOn), + "force-off" => Some(FramePointers::ForceOff), + "default" => None, + // This should be validated in TomlProfile::validate + _ => panic!("invalid frame-pointers value `{}`", frame_pointers), + }; + } profile.strip = match toml.strip { Some(StringOrBool::Bool(true)) => Strip::Resolved(StripInner::Named("symbols".into())), Some(StringOrBool::Bool(false)) => Strip::Resolved(StripInner::None), @@ -633,6 +642,8 @@ pub struct Profile { pub trim_paths: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hint_mostly_unused: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub frame_pointers: Option, } impl Default for Profile { @@ -655,6 +666,7 @@ impl Default for Profile { rustflags: vec![], trim_paths: None, hint_mostly_unused: None, + frame_pointers: None, } } } @@ -685,6 +697,7 @@ compact_debug! { rustflags trim_paths hint_mostly_unused + frame_pointers )] } } @@ -751,7 +764,12 @@ impl Profile { self.debug_assertions, self.overflow_checks, self.rpath, - (self.incremental, self.panic, self.strip), + ( + self.incremental, + self.panic, + self.strip, + self.frame_pointers, + ), &self.rustflags, &self.trim_paths, ) @@ -973,6 +991,26 @@ impl Ord for Strip { } } +/// The setting for controlling frame pointers in generated code. +#[derive( + Clone, Copy, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] +#[serde(rename_all = "kebab-case")] +pub enum FramePointers { + ForceOn, + ForceOff, +} + +impl fmt::Display for FramePointers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + FramePointers::ForceOn => "force-on", + FramePointers::ForceOff => "force-off", + } + .fmt(f) + } +} + /// Flags used in creating `Unit`s to indicate the purpose for the target, and /// to ensure the target's dependencies have the correct settings. /// diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index a1be7b0aa46..e26280ed892 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -2595,6 +2595,18 @@ pub fn validate_profile( } } + if let Some(frame_pointers) = &root.frame_pointers { + if frame_pointers != "force-on" + && frame_pointers != "force-off" + && frame_pointers != "default" + { + bail!( + "`frame-pointers` setting of `{frame_pointers}` is not a valid setting, \ + must be `\"force-on\"`, `\"force-off\"`, or `\"default\"`.", + ); + } + } + Ok(()) } diff --git a/src/doc/src/reference/profiles.md b/src/doc/src/reference/profiles.md index 28dd841fd16..6996c7f8f39 100644 --- a/src/doc/src/reference/profiles.md +++ b/src/doc/src/reference/profiles.md @@ -258,6 +258,26 @@ whether or not [`rpath`] is enabled. [`-C rpath` flag]: ../../rustc/codegen-options/index.html#rpath [`rpath`]: https://en.wikipedia.org/wiki/Rpath +### frame-pointers + +The `frame-pointers` setting controls the [`-C force-frame-pointers` flag] +which controls whether frame pointers are forced in generated code. Frame +pointers are useful for profiling as they enable reliable stack unwinding. + +```toml +[profile.release] +frame-pointers = "force-on" +``` + +The valid options are: +- `"force-on"`: Force frame pointers to be enabled. +- `"force-off"`: Don't force frame pointers (the compiler may still emit them based on target defaults). +- `"default"`: Use the compiler's target-specific default. + +When not specified, the compiler's target-specific default is used. + +[`-C force-frame-pointers` flag]: ../../rustc/codegen-options/index.html#force-frame-pointers + ## Default profiles ### dev diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index 648958f3326..1314b853eea 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -1675,6 +1675,7 @@ fn all_profile_options() { rustflags: None, trim_paths: None, hint_mostly_unused: None, + frame_pointers: Some("force-on".to_string()), }; let mut overrides = BTreeMap::new(); let key = cargo_toml::ProfilePackageSpec::Spec(PackageIdSpec::parse("foo").unwrap()); diff --git a/tests/testsuite/profiles.rs b/tests/testsuite/profiles.rs index 61cc8d2187a..d8ac1dcaf09 100644 --- a/tests/testsuite/profiles.rs +++ b/tests/testsuite/profiles.rs @@ -956,3 +956,136 @@ fn profile_hint_mostly_unused_nightly() { ) .run(); } + +#[cargo_test] +fn frame_pointers_force_on() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [profile.release] + frame-pointers = "force-on" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build --release -v") + .with_stderr_data(str![[r#" +[COMPILING] foo v0.1.0 ([ROOT]/foo) +[RUNNING] `rustc [..] -C force-frame-pointers=on [..]` +[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn frame_pointers_force_off() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [profile.release] + frame-pointers = "force-off" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build --release -v") + .with_stderr_data(str![[r#" +[COMPILING] foo v0.1.0 ([ROOT]/foo) +[RUNNING] `rustc [..] -C force-frame-pointers=off [..]` +[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s + +"#]]) + .run(); +} + +#[cargo_test] +fn frame_pointers_unspecified() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build -v") + .with_stderr_does_not_contain("[RUNNING] `rustc [..] -C force-frame-pointers[..]`") + .run(); +} + +#[cargo_test] +fn frame_pointers_default_overrides_parent() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [profile.release] + frame-pointers = "force-on" + + [profile.myprofile] + inherits = "release" + frame-pointers = "default" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build --profile myprofile -v") + .with_stderr_does_not_contain("[RUNNING] `rustc [..] -C force-frame-pointers[..]`") + .run(); +} + +#[cargo_test] +fn frame_pointers_invalid_value() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [profile.release] + frame-pointers = "invalid" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build --release") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml` + +Caused by: + `frame-pointers` setting of `invalid` is not a valid setting, must be `"force-on"`, `"force-off"`, or `"default"`. + +"#]]) + .run(); +}