diff --git a/src/cargo/core/compiler/fingerprint.rs b/src/cargo/core/compiler/fingerprint.rs index e3aaf9c76ec..f3703727467 100644 --- a/src/cargo/core/compiler/fingerprint.rs +++ b/src/cargo/core/compiler/fingerprint.rs @@ -1331,6 +1331,9 @@ fn calculate_normal(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult, unit: &Unit) -> CargoResult { Ok(output_dir.join(format!("{}.examples", unit.buildkey()))) }; + let mut unstable_opts = false; if unit.mode.is_doc_scrape() { debug_assert!(cx.bcx.scrape_units.contains(unit)); - rustdoc.arg("-Zunstable-options"); + unstable_opts = true; rustdoc .arg("--scrape-examples-output-path") @@ -680,7 +690,7 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult { // We only pass scraped examples to packages in the workspace // since examples are only coming from reverse-dependencies of workspace packages - rustdoc.arg("-Zunstable-options"); + unstable_opts = true; for scrape_unit in &cx.bcx.scrape_units { rustdoc @@ -689,7 +699,14 @@ fn rustdoc(cx: &mut Context<'_, '_>, unit: &Unit) -> CargoResult { } } - build_deps_args(&mut rustdoc, cx, unit)?; + build_deps_args(&mut rustdoc, cx, unit, &mut unstable_opts)?; + + // This will only be set if we're already using a feature + // requiring nightly rust + if unstable_opts { + rustdoc.arg("-Z").arg("unstable-options"); + } + rustdoc::add_root_urls(cx, unit, &mut rustdoc)?; rustdoc.args(bcx.rustdocflags_args(unit)); @@ -1074,10 +1091,46 @@ fn lto_args(cx: &Context<'_, '_>, unit: &Unit) -> Vec { result } +fn build_natvis( + cmd: &mut ProcessBuilder, + cx: &mut Context<'_, '_>, + unit: &Unit, + unstable_opts: &mut bool, +) -> CargoResult<()> { + if let Some(natvis) = &unit.pkg.manifest().natvis() { + // Only add natvis for the primary unit of each package. + // This will ensure units that depend on the primary + // unit, i.e. bins, integration tests or examples, will not + // have a duplicate set of natvis files. + let mut add_natvis = true; + let deps = cx.unit_deps(unit); + for dep in deps { + if cx.is_primary_package(&dep.unit) { + add_natvis = false; + break; + } + } + + if add_natvis { + *unstable_opts = true; + for file in natvis { + cmd.arg("-C").arg(&{ + let mut arg = OsString::from("natvis="); + arg.push(file); + arg + }); + } + } + } + + Ok(()) +} + fn build_deps_args( cmd: &mut ProcessBuilder, cx: &mut Context<'_, '_>, unit: &Unit, + unstable_opts: &mut bool, ) -> CargoResult<()> { let bcx = cx.bcx; cmd.arg("-L").arg(&{ @@ -1121,24 +1174,16 @@ fn build_deps_args( } } - let mut unstable_opts = false; - for dep in deps { if dep.unit.mode.is_run_custom_build() { cmd.env("OUT_DIR", &cx.files().build_script_out_dir(&dep.unit)); } } - for arg in extern_args(cx, unit, &mut unstable_opts)? { + for arg in extern_args(cx, unit, unstable_opts)? { cmd.arg(arg); } - // This will only be set if we're already using a feature - // requiring nightly rust - if unstable_opts { - cmd.arg("-Z").arg("unstable-options"); - } - Ok(()) } diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 61452542a51..dd821b6bd83 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -412,6 +412,9 @@ features! { // Allow specifying rustflags directly in a profile (unstable, profile_rustflags, "", "reference/unstable.html#profile-rustflags-option"), + + // Allow specifying natvis files to embed in the PDB when using the MSVC Linker. + (unstable, natvis, "", "reference/unstable.html#natvis"), } pub struct Feature { diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index 4bf015619d3..2a79a075215 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt; use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; @@ -54,6 +54,7 @@ pub struct Manifest { default_run: Option, metabuild: Option>, resolve_behavior: Option, + natvis: Option>, } /// When parsing `Cargo.toml`, some warnings should silenced @@ -393,6 +394,7 @@ impl Manifest { original: Rc, metabuild: Option>, resolve_behavior: Option, + natvis: Option>, ) -> Manifest { Manifest { summary, @@ -418,6 +420,7 @@ impl Manifest { default_run, metabuild, resolve_behavior, + natvis, } } @@ -486,6 +489,10 @@ impl Manifest { self.links.as_deref() } + pub fn natvis(&self) -> &Option> { + &self.natvis + } + pub fn workspace_config(&self) -> &WorkspaceConfig { &self.workspace } diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 217f7ce114e..dff4ae4f7b8 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt; +use std::fs; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::rc::Rc; @@ -326,6 +327,7 @@ pub struct TomlManifest { replace: Option>, patch: Option>>, workspace: Option, + debugger_visualizations: Option, badges: Option>>, } @@ -934,6 +936,16 @@ pub struct TomlWorkspace { metadata: Option, } +/// Represents the `debugger-visualizations` section of a `Cargo.toml`. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TomlDebuggerVisualizations { + natvis: Option>, + + // Note that this field must come last due to the way toml serialization + // works which requires tables to be emitted after all values. + metadata: Option, +} + impl TomlProject { pub fn to_package_id(&self, source_id: SourceId) -> CargoResult { PackageId::new(self.name, self.version.clone(), source_id) @@ -1049,6 +1061,7 @@ impl TomlManifest { replace: None, patch: None, workspace: None, + debugger_visualizations: self.debugger_visualizations.clone(), badges: self.badges.clone(), cargo_features: self.cargo_features.clone(), }); @@ -1400,6 +1413,65 @@ impl TomlManifest { .transpose()? .map(CompileKind::Target); + let mut natvis = BTreeSet::new(); + let mut natvis_manifest_override = false; + let natvis_ext = "natvis"; + + // Collect the Natvis files specified via the toml file, if any exist, and normalize the absolute paths. + if let Some(debugger_visualizations) = me.debugger_visualizations.clone() { + features.require(Feature::natvis())?; + if let Some(natvis_files) = debugger_visualizations.natvis { + natvis_manifest_override = true; + for file in &natvis_files { + let path = paths::normalize_path(&package_root.join(file.clone().0)); + if !path.exists() { + warnings.push(format!( + "Natvis file `{}` does not exist. Skipping file.", + path.display().to_string() + )); + } else { + if path.extension() != Some(natvis_ext.as_ref()) { + warnings.push(format!( + "Natvis file `{}` does not have the expected `.natvis` extension.", + path.display().to_string() + )); + } + natvis.insert(path); + } + } + } + } + + // If the `natvis` manifest key was not specified, append all of the Natvis files in the pre-determined + // directory under the package root, `dbgvis/natvis/**/`. + fn find_natvis_files(path: &PathBuf, natvis: &mut BTreeSet) { + if let Ok(path) = fs::read_dir(&path) { + for entry in path { + match entry { + Ok(entry) => { + let path = entry.path(); + if path.is_dir() { + find_natvis_files(&path, natvis); + } else if path.is_file() && path.extension() == Some("natvis".as_ref()) { + natvis.insert(path); + } + } + Err(_) => { } + } + } + } + } + + if !natvis_manifest_override && features.require(Feature::natvis()).is_ok() { + let natvis_dir_path = package_root.join("dbgvis").join(natvis_ext); + find_natvis_files(&natvis_dir_path, &mut natvis); + } + + let natvis = if natvis.is_empty() { + None + } else { + Some(natvis) + }; let custom_metadata = project.metadata.clone(); let mut manifest = Manifest::new( summary, @@ -1424,6 +1496,7 @@ impl TomlManifest { Rc::clone(me), project.metabuild.clone().map(|sov| sov.0), resolve_behavior, + natvis, ); if project.license_file.is_some() && project.license.is_some() { manifest.warnings_mut().add_warning( diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index c23d09773ae..d1c9dd57d4f 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -89,6 +89,7 @@ Each new feature described below should explain how to use it. * [Profile `strip` option](#profile-strip-option) — Forces the removal of debug information and symbols from executables. * [Profile `rustflags` option](#profile-rustflags-option) — Passed directly to rustc. * [per-package-target](#per-package-target) — Sets the `--target` to use for each individual package. + * [natvis](#natvis) — Allows the specification of Natvis`(.natvis)` files to be embedded in a PDB when linking via the MSVC Linker. * Information and metadata * [Build-plan](#build-plan) — Emits JSON information on which commands will be run. * [timings](#timings) — Generates a report on how long individual dependencies took to run. @@ -876,6 +877,36 @@ In this example, the crate is always built for as a plugin for a main program that runs on the host (or provided on the command line) target. +### natvis +* Tracking Issue: [#0000](https://github.com/rust-lang/cargo/pull/0000) +* Original Pull Request: [#0000](https://github.com/rust-lang/cargo/pull/0000) +* Original Issue: [#0000](https://github.com/rust-lang/cargo/pull/0000) + +The `natvis` feature adds a new section and key to the manifest: +`[debugger-visualizations]` and `debugger-visualizations.natvis` which +takes a list of `.natvis` files. The `natvis` feature also allows Cargo +to automatically find Natvis files that live in a predetermined +directory relative to the root of the package, `dbgvis/natvis`, including +its subdirectories. This will be the default for including Natvis files in +a package and will be overridden if the manifest key is specified. The Natvis +files will then be linked into the PDB generated if using the MSVC Linker. + +Example: + +```toml +cargo-features = ["natvis"] + +[package] +name = "foo" +version = "0.0.1" + +[debugger-visualizations] +natvis = ["foo.natvis", "bar.natvis"] +``` + +In this example, the crate `foo` will embed `foo.natvis` and `bar.natvis` +into the PDB being generated when using the MSVC Linker. + ### credential-process * Tracking Issue: [#8933](https://github.com/rust-lang/cargo/issues/8933) * RFC: [#2730](https://github.com/rust-lang/rfcs/pull/2730) diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 8c30bf929a8..ddd3ecf02fe 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -77,6 +77,7 @@ mod metabuild; mod metadata; mod minimal_versions; mod multitarget; +mod natvis; mod net_config; mod new; mod offline; diff --git a/tests/testsuite/natvis.rs b/tests/testsuite/natvis.rs new file mode 100644 index 00000000000..e543a842ab8 --- /dev/null +++ b/tests/testsuite/natvis.rs @@ -0,0 +1,629 @@ +//! Tests for natvis Cargo.toml syntax + +use cargo_test_support::project; +use cargo_test_support::registry::Package; + +#[cargo_test] +fn gated() { + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["foo.natvis"] + "#, + ) + .file("src/main.rs", "fn main() { assert!(true) }") + .build(); + + // Run cargo build. + p.cargo("build") + .masquerade_as_nightly_cargo() + .with_status(101) + .with_stderr_contains("[..]feature `natvis` is required") + .run(); +} + +#[cargo_test] +fn file_does_not_exist() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "foo" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["foo.natvis"] + "#, + ) + .file("src/main.rs", "fn main() { assert!(true) }") + .build(); + + let foo = p.root().join("foo.natvis"); + // Run cargo build. + p.cargo("build") + .masquerade_as_nightly_cargo() + .with_stderr_contains(format!("[..]Natvis file `{}` does not exist. Skipping file.[..]", foo.display().to_string())) + .run(); +} + +#[cargo_test] +fn invalid_natvis_extension() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "foo" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["src/main.rs"] + "#, + ) + .file("src/main.rs", "fn main() { assert!(true) }") + .build(); + + let natvis_path = p.root().join("src").join("main.rs").display().to_string(); + let expected_msg = format!( + "warning: Natvis file `{}` does not have the expected `.natvis` extension.", + natvis_path + ); + + // Run cargo build. + p.cargo("build") + .masquerade_as_nightly_cargo() + .with_stderr_contains(expected_msg) + .run(); +} + +#[cargo_test] +fn simple_test() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "foo" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["foo.natvis"] + "#, + ) + .file( + "src/main.rs", + r#" + fn main() { assert!(true) } + + pub struct Foo { + pub x: i32, + pub y: i32, + pub z: i32 + } + "#, + ) + .file( + "foo.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .build(); + + // Run cargo build. + p.cargo("build").masquerade_as_nightly_cargo().run(); +} + +#[cargo_test] +fn direct_dependency_with_natvis() { + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.0.1" + + [dependencies] + test_dependency = { path = "src/test_dependency" } + "#, + ) + .file( + "src/main.rs", + r#" + fn main() { assert!(true) } + "#, + ) + .file( + "src/test_dependency/src/lib.rs", + r#" + pub struct Foo { + pub x: i32, + pub y: i32, + pub z: i32 + } + "#, + ) + .file( + "src/test_dependency/Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "test_dependency" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["test_dependency.natvis"] + "#, + ) + .file( + "src/test_dependency/test_dependency.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .build(); + + let natvis_path = p + .root() + .join("src/test_dependency/test_dependency.natvis") + .display() + .to_string(); + + // Run cargo build. + p.cargo("build -v") + .masquerade_as_nightly_cargo() + .with_status(0) + .with_stderr_contains(format!("[..]-C natvis={}[..]", natvis_path)) + .run(); +} + +#[cargo_test] +fn multiple_natvis_files() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "foo" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["bar.natvis", "foo.natvis"] + "#, + ) + .file( + "src/main.rs", + r#" + fn main() { assert!(true) } + + pub struct Foo { + pub x: i32, + pub y: i32, + pub z: i32 + } + + pub struct Bar { + pub x: i32, + pub y: i32, + pub z: i32 + } + "#, + ) + .file( + "foo.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .file( + "bar.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .build(); + + let bar_natvis_path = p.root().join("bar.natvis").display().to_string(); + let foo_natvis_path = p.root().join("foo.natvis").display().to_string(); + + // Run cargo build. + p.cargo("build -v") + .masquerade_as_nightly_cargo() + .with_status(0) + .with_stderr_contains(format!("[..]-C natvis={}[..]", bar_natvis_path)) + .with_stderr_contains(format!("[..]-C natvis={}[..]", foo_natvis_path)) + .run(); +} + +#[cargo_test] +fn natvis_from_dbgvis_directory() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "foo" + version = "0.0.1" + "#, + ) + .file( + "src/main.rs", + r#" + fn main() { assert!(true) } + + pub struct Foo { + pub x: i32, + pub y: i32, + pub z: i32 + } + "#, + ) + .file( + "dbgvis/natvis/foo.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .file( + "dbgvis/natvis/foo/bar.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .build(); + + let foo_natvis_path = p + .root() + .join("dbgvis/natvis/foo.natvis") + .display() + .to_string(); + + let bar_natvis_path = p + .root() + .join("dbgvis/natvis/natvis/bar.natvis") + .display() + .to_string(); + + // Run cargo build. + p.cargo("build -v") + .masquerade_as_nightly_cargo() + .with_stderr_contains(format!("[..]-C natvis={}[..]", foo_natvis_path)) + .with_stderr_contains(format!("[..]-C natvis={}[..]", bar_natvis_path)) + .run(); +} + +#[cargo_test] +fn natvis_toml_and_dbgvis_directory() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "foo" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["src/bar.natvis"] + "#, + ) + .file( + "src/main.rs", + r#" + fn main() { assert!(true) } + + pub struct Foo { + pub x: i32, + pub y: i32, + pub z: i32 + } + "#, + ) + .file( + "src/bar.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .file( + "dbgvis/natvis/foo.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .build(); + + let foo_natvis_path = p + .root() + .join("dbgvis/natvis/foo.natvis") + .display() + .to_string(); + + let bar_natvis_path = p + .root() + .join("src/bar.natvis") + .display() + .to_string(); + + // Run cargo build. + p.cargo("build -v") + .masquerade_as_nightly_cargo() + .with_stderr_contains(format!("[..]-C natvis={}[..]", bar_natvis_path)) + .with_stderr_does_not_contain(format!("[..]-C natvis={}[..]", foo_natvis_path)) + .run(); +} + +#[cargo_test] +fn indirect_dependency_with_natvis() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "foo" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["foo.natvis"] + + [dependencies] + test_dependency = { path = "src/test_dependency" } + "#, + ) + .file( + "src/main.rs", + r#" + fn main() { assert!(true) } + + pub struct Foo { + pub x: i32, + pub y: i32, + pub z: i32 + } + "#, + ) + .file( + "foo.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .file( + "src/test_dependency/Cargo.toml", + r#" + [project] + name = "test_dependency" + version = "0.0.1" + + [dependencies] + nested_dependency = { path = "src/nested_dependency" } + "#, + ) + .file("src/test_dependency/src/lib.rs", r#""#) + .file( + "src/test_dependency/src/nested_dependency/Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "nested_dependency" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["nested_dependency.natvis"] + "#, + ) + .file( + "src/test_dependency/src/nested_dependency/src/lib.rs", + r#" + pub struct Bar { + pub x: i32, + pub y: i32, + pub z: i32 + } + "#, + ) + .file( + "src/test_dependency/src/nested_dependency/nested_dependency.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .build(); + + let foo_natvis_path = p.root().join("foo.natvis").display().to_string(); + let nested_dependency_natvis_path = p + .root() + .join("src/test_dependency/src/nested_dependency/nested_dependency.natvis") + .display() + .to_string(); + + // Run cargo build. + p.cargo("build -v") + .masquerade_as_nightly_cargo() + .with_status(0) + .with_stderr_contains(format!("[..]-C natvis={}[..]", foo_natvis_path)) + .with_stderr_contains(format!( + "[..]-C natvis={}[..]", + nested_dependency_natvis_path + )) + .run(); +} + +#[cargo_test] +fn registry_dependency_with_natvis() { + Package::new("bar", "0.0.1") + .file( + "Cargo.toml", + r#" + cargo-features = ["natvis"] + + [project] + name = "bar" + version = "0.0.1" + + [debugger-visualizations] + natvis = ["bar.natvis"] + "#, + ) + .file( + "bar.natvis", + r#" + + + + x:{x}, y:{y}, z:{z} + + x + y + z + + + + "#, + ) + .file("src/lib.rs", "") + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.0.1" + + [dependencies] + bar = "0.0.1" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build -v") + .masquerade_as_nightly_cargo() + .with_stderr_contains(format!("[..]-C natvis=[..]/bar.natvis[..]")) + .run(); +}