From dee3c02cbef4d0440076b07f4c149284bea838b4 Mon Sep 17 00:00:00 2001 From: Arlie Davis Date: Wed, 16 Dec 2020 12:11:20 -0800 Subject: [PATCH] NatVis support in Cargo This allows developers to write NatVis descriptions for their crates. If a Cargo package contains NatVis files, then they will be written into the PDB file. Debuggers that support NatVis will automatically locate these files and use them. Currently, Visual Studio 2019, VS Code, and WinDbg support NatVis. (NatVis is a Windows-only feature.) The easiest way to add a NatVis file to a Cargo package is to create a `natvis` subdirectory and place the file in that directory. The file must have a `.natvis` extension. Any (reasonable) number of NatVis files can be provided. Optionally, you can give explicit paths by setting the `natvis-files` key in `Cargo.toml`. For example: ``` [package] name = ... natvis-files = ["my_types.natvis", "more_types.natvis"] ``` This key can also be set to `[]` in order to prevent Cargo from searching the `natvis` directory. --- src/cargo/core/compiler/mod.rs | 9 ++ src/cargo/core/manifest.rs | 7 + src/cargo/core/package.rs | 7 + src/cargo/util/config/target.rs | 2 + src/cargo/util/toml/mod.rs | 68 ++++++++- tests/testsuite/main.rs | 1 + tests/testsuite/natvis.rs | 243 ++++++++++++++++++++++++++++++++ 7 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 tests/testsuite/natvis.rs diff --git a/src/cargo/core/compiler/mod.rs b/src/cargo/core/compiler/mod.rs index 7ac3842f4df..1a2d686fef1 100644 --- a/src/cargo/core/compiler/mod.rs +++ b/src/cargo/core/compiler/mod.rs @@ -943,6 +943,15 @@ fn build_base_args( .env("RUSTC_BOOTSTRAP", "1"); } + // If the target is using the MSVC toolchain, then pass any NatVis files to it. + let target_config = bcx.target_data.target_config(unit.kind); + let is_msvc = target_config.triple.ends_with("-msvc"); + if is_msvc { + for natvis_file in unit.pkg.manifest().natvis_files().iter() { + cmd.arg(format!("-Clink-arg=/natvis:{}", natvis_file)); + } + } + // Add `CARGO_BIN_` environment variables for building tests. if unit.target.is_test() || unit.target.is_bench() { for bin_target in unit diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index f6a7323d25f..c05761744d6 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -50,6 +50,7 @@ pub struct Manifest { default_run: Option, metabuild: Option>, resolve_behavior: Option, + natvis_files: Vec, } /// When parsing `Cargo.toml`, some warnings should silenced @@ -384,6 +385,7 @@ impl Manifest { original: Rc, metabuild: Option>, resolve_behavior: Option, + natvis_files: Vec, ) -> Manifest { Manifest { summary, @@ -407,6 +409,7 @@ impl Manifest { publish_lockfile, metabuild, resolve_behavior, + natvis_files, } } @@ -539,6 +542,10 @@ impl Manifest { .join(".metabuild") .join(format!("metabuild-{}-{}.rs", self.name(), hash)) } + + pub fn natvis_files(&self) -> &[String] { + &self.natvis_files + } } impl VirtualManifest { diff --git a/src/cargo/core/package.rs b/src/cargo/core/package.rs index 88bc43c7311..2416aa0bf60 100644 --- a/src/cargo/core/package.rs +++ b/src/cargo/core/package.rs @@ -102,6 +102,8 @@ pub struct SerializedPackage { links: Option, #[serde(skip_serializing_if = "Option::is_none")] metabuild: Option>, + #[serde(skip_serializing_if = "Vec::is_empty")] + natvis_files: Vec, } impl Package { @@ -259,8 +261,13 @@ impl Package { links: self.manifest().links().map(|s| s.to_owned()), metabuild: self.manifest().metabuild().cloned(), publish: self.publish().as_ref().cloned(), + natvis_files: self.natvis_files().to_vec(), } } + + pub fn natvis_files(&self) -> &[String] { + self.inner.manifest.natvis_files() + } } impl fmt::Display for Package { diff --git a/src/cargo/util/config/target.rs b/src/cargo/util/config/target.rs index 13b77e262d0..f6064e15830 100644 --- a/src/cargo/util/config/target.rs +++ b/src/cargo/util/config/target.rs @@ -34,6 +34,7 @@ pub struct TargetConfig { /// running its build script and instead use the given output from the /// config file. pub links_overrides: BTreeMap, + pub triple: String, } /// Loads all of the `target.'cfg()'` tables. @@ -85,6 +86,7 @@ pub(super) fn load_target_triple(config: &Config, triple: &str) -> CargoResult, metadata: Option, resolver: Option, + #[serde(rename = "natvis-files")] + natvis_files: Option>, } #[derive(Debug, Deserialize, Serialize)] @@ -867,7 +869,7 @@ struct Context<'a, 'b> { } impl TomlManifest { - /// Prepares the manfiest for publishing. + /// Prepares the manifest for publishing. // - Path and git components of dependency specifications are removed. // - License path is updated to point within the package. pub fn prepare_for_publish( @@ -1297,6 +1299,15 @@ impl TomlManifest { } } + // Handle NatVis files. If the TomlManifest specifies a set of NatVis files, then we use + // exactly that set and we do not inspect the filesystem. (We do require that the paths + // specified in the manifest be relative paths.) If the manifest does not specify any + // NatVis files, then we look for `natvis/*.natvis` files. + let natvis_files = resolve_natvis_files( + project.natvis_files.as_ref().map(|s| s.as_slice()), + package_root, + )?; + let custom_metadata = project.metadata.clone(); let mut manifest = Manifest::new( summary, @@ -1319,6 +1330,7 @@ impl TomlManifest { Rc::clone(me), project.metabuild.clone().map(|sov| sov.0), resolve_behavior, + natvis_files, ); if project.license_file.is_some() && project.license.is_some() { manifest.warnings_mut().add_warning( @@ -1892,3 +1904,57 @@ impl fmt::Debug for PathValue { self.0.fmt(f) } } + +fn resolve_natvis_files( + toml_natvis_files: Option<&[String]>, + package_root: &Path, +) -> CargoResult> { + if let Some(toml_natvis_files) = toml_natvis_files { + let mut natvis_files = Vec::with_capacity(toml_natvis_files.len()); + for toml_natvis_file in toml_natvis_files.iter() { + let natvis_file_path = Path::new(toml_natvis_file); + if !natvis_file_path.is_relative() { + bail!( + "`natvis-files` contains absolute path `{}`; \ + all paths in `natvis-files` are required to be relative paths.", + toml_natvis_file + ); + } + natvis_files.push( + package_root + .join(natvis_file_path) + .to_str() + .unwrap() + .to_string(), + ); + } + Ok(natvis_files) + } else { + // The manifest file did not specify any natvis files. + // By convention, we look for `natvis\*.natvis` files. + if let Ok(nv) = find_natvis_files(package_root) { + Ok(nv) + } else { + Ok(Vec::new()) + } + } +} + +fn find_natvis_files(package_root: &Path) -> std::io::Result> { + use std::ffi::OsStr; + + let mut natvis_files = Vec::new(); + let natvis_dir = package_root.join("natvis"); + for entry in std::fs::read_dir(&natvis_dir)? { + let entry = entry?; + let filename = PathBuf::from(entry.file_name().to_str().unwrap()); + if filename.extension() == Some(OsStr::new("natvis")) { + if let Ok(md) = entry.metadata() { + if md.is_file() { + natvis_files.push(entry.path().to_str().unwrap().to_string()); + } + } + } + } + Ok(natvis_files) +} diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 8af5858b373..413bb433d11 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -75,6 +75,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..a9c51a29589 --- /dev/null +++ b/tests/testsuite/natvis.rs @@ -0,0 +1,243 @@ +//! Tests for NatVis support. +//! +//! Currently, there is no way to test for the presence (or absence) +//! of a specific item in a JSON array, so these tests verify all of +//! the arguments in the corresponding `rustc` calls. That's fragile. +//! Ideally, we would be able to test for the `-Clink-args=...` args +//! without caring about any other args. + +use cargo_test_support::{project, rustc_host}; + +const NATVIS_CONTENT: &str = r#" + + + +"#; + +fn is_natvis_supported() -> bool { + rustc_host().ends_with("-msvc") +} + +/// Tests a project that contains a single NatVis file. +/// The file is discovered by Cargo, since it is in the `/natvis` directory, +/// and does not need to be explicitly specified in the manifest. +#[cargo_test] +fn natvis_autodiscovery() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "natvis_autodiscovery" + version = "0.0.1" + edition = "2018" + "#, + ) + .file("src/main.rs", "fn main() {}") + .file("natvis/types.natvis", NATVIS_CONTENT) + .build(); + + let mut execs = p.cargo("build --build-plan -Zunstable-options"); + execs.masquerade_as_nightly_cargo(); + + if is_natvis_supported() { + execs.with_json( + r#" + { + "inputs": [ + "[..]/foo/Cargo.toml" + ], + "invocations": [ + { + "args": [ + "--crate-name", + "natvis_autodiscovery", + "--edition=2018", + "src/main.rs", + "--error-format=json", + "--json=[..]", + "--crate-type", + "bin", + "--emit=[..]", + "-C", + "embed-bitcode=[..]", + "-C", + "debuginfo=[..]", + "-C", + "metadata=[..]", + "--out-dir", + "[..]", + "-Clink-arg=/natvis:[..]/foo/natvis/types.natvis", + "-L", + "dependency=[..]" + ], + "cwd": "[..]/cit/[..]/foo", + "deps": [], + "env": "{...}", + "kind": null, + "links": "{...}", + "outputs": "{...}", + "package_name": "natvis_autodiscovery", + "package_version": "0.0.1", + "program": "rustc", + "target_kind": ["bin"], + "compile_mode": "build" + } + ] + } + "#, + ); + } + + execs.run(); +} + +/// Tests a project that contains a single NatVis file, which is explicitly +/// specified in the manifest file. Because it is explicitly specified, it +/// does not have to be in the `/natvis` subdirectory. +#[cargo_test] +fn natvis_explicit() { + if !is_natvis_supported() { + return; + } + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "natvis_explicit" + version = "0.0.1" + edition = "2018" + natvis-files = ["types.natvis"] + "#, + ) + .file("src/main.rs", "fn main() {}") + .file("types.natvis", NATVIS_CONTENT) + .build(); + + let mut execs = p.cargo("build --build-plan -Zunstable-options"); + execs.masquerade_as_nightly_cargo(); + + if is_natvis_supported() { + execs.with_json( + r#" + { + "inputs": [ + "[..]/foo/Cargo.toml" + ], + "invocations": [ + { + "args": [ + "--crate-name", + "natvis_explicit", + "--edition=2018", + "src/main.rs", + "--error-format=json", + "--json=[..]", + "--crate-type", + "bin", + "--emit=[..]", + "-C", + "embed-bitcode=[..]", + "-C", + "debuginfo=[..]", + "-C", + "metadata=[..]", + "--out-dir", + "[..]", + "-Clink-arg=/natvis:[..]/foo/types.natvis", + "-L", + "dependency=[..]" + ], + "cwd": "[..]/cit/[..]/foo", + "deps": [], + "env": "{...}", + "kind": null, + "links": "{...}", + "outputs": "{...}", + "package_name": "natvis_explicit", + "package_version": "0.0.1", + "program": "rustc", + "target_kind": ["bin"], + "compile_mode": "build" + } + ] + } + "#, + ); + } + + execs.run(); +} + +/// Tests a project that has a file in the `/natvis` directory, but which has +/// been disabled in the manifest. This is analogous to specifying `autobenches = false`. +#[cargo_test] +fn natvis_disabled() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "natvis_disabled" + version = "0.0.1" + edition = "2018" + natvis-files = [] + "#, + ) + .file("src/main.rs", "fn main() {}") + .file("natvis/types.natvis", NATVIS_CONTENT) + .build(); + + let mut execs = p.cargo("build --build-plan -Zunstable-options"); + execs.masquerade_as_nightly_cargo(); + if is_natvis_supported() { + execs.with_json( + r#" + { + "inputs": [ + "[..]/foo/Cargo.toml" + ], + "invocations": [ + { + "args": [ + "--crate-name", + "natvis_disabled", + "--edition=2018", + "src/main.rs", + "--error-format=json", + "--json=[..]", + "--crate-type", + "bin", + "--emit=[..]", + "-C", + "embed-bitcode=[..]", + "-C", + "debuginfo=[..]", + "-C", + "metadata=[..]", + "--out-dir", + "[..]", + "-L", + "dependency=[..]" + ], + "cwd": "[..]/cit/[..]/foo", + "deps": [], + "env": "{...}", + "kind": null, + "links": "{...}", + "outputs": "{...}", + "package_name": "natvis_disabled", + "package_version": "0.0.1", + "program": "rustc", + "target_kind": ["bin"], + "compile_mode": "build" + } + ] + } + "#, + ); + } + execs.run(); +}