diff --git a/doc/book/src/reference/unstable.md b/doc/book/src/reference/unstable.md index 90af45a83a2..93210c9e398 100644 --- a/doc/book/src/reference/unstable.md +++ b/doc/book/src/reference/unstable.md @@ -1585,6 +1585,44 @@ Paths to all other source files will not be affected. This will not affect any hard-coded paths in the source code, such as in strings. +##### Unremap files + +When the `object` scope is active and debuginfo is enabled, +Cargo writes an unremap file beside each final artifact. +The file is aimed at helping debuggers substitute sanitized paths back to local ones, +e.g., via GDB's `set substitute-path` or LLDB's `target.source-map`. + +The unremap file name ends with `.trim-paths.jsonl`. +For example, +your `my-app` executable would come with an unremap file named +`my-app.trim-paths.jsonl` beside it. + +The unremap file is in JSONL format: + +* The first record carries the format version. +* The second record is file-level metadata, + such as the toolchain version and the workspace root. +* Each following record maps a sanitized path prefix in the artifact + (`from`) back to the local path it replaced (`to`), + ordered by the `from` prefix. + Note that this follows the debugger substitution direction, + which is the inverse of `--remap-path-prefix`. + +An example of the unremap file: + +```json +{"v":1} +{"rust_version":"1.96.0-nightly","workspace_root":"/home/me/app"} +{"from":"/cargo/build-dir","to":"/home/me/app/target"} +{"from":"/cargo/registry/6f17d22d3f0a95d1","to":"/home/me/.cargo/registry/src/index.crates.io-6f17d22d3f0a95d1"} +{"from":"/rustc/abc123","to":"/home/me/.rustup/toolchains/nightly/lib/rustlib/src/rust"} +``` + +Since it is meant to be a debugging aid, +it includes absolute paths of your system, +so there is no artifact privacy guarantee. +You might want to exclude `*.trim-paths.jsonl` files when distributing artifacts. + #### Environment variable *as a new entry of ["Environment variables Cargo sets for build scripts"](./environment-variables.md#environment-variables-cargo-sets-for-crates)* diff --git a/src/compiler/build_context/target_info.rs b/src/compiler/build_context/target_info.rs index fc9268e88df..de4b197a07f 100644 --- a/src/compiler/build_context/target_info.rs +++ b/src/compiler/build_context/target_info.rs @@ -78,6 +78,8 @@ pub enum FileFlavor { DebugInfo, /// SBOM (Software Bill of Materials pre-cursor) file (e.g. cargo-sbon.json). Sbom, + /// Unremap file for `-Ztrim-paths` (e.g. `foo.trim-paths.jsonl`). + Unremap, /// Cross-crate info JSON files generated by rustdoc. DocParts, } diff --git a/src/compiler/build_runner/compilation_files.rs b/src/compiler/build_runner/compilation_files.rs index e2de7212543..ccce7b4e072 100644 --- a/src/compiler/build_runner/compilation_files.rs +++ b/src/compiler/build_runner/compilation_files.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use tracing::debug; use super::{BuildContext, BuildRunner, CompileKind, FileFlavor, Layout}; +use crate::compiler::trim_paths; use crate::compiler::{CompileMode, CompileTarget, CrateType, FileType, Unit}; use crate::util::{self, CargoResult, OnceExt, StableHasher}; use crate::workspace::{Target, TargetKind, Workspace}; @@ -612,6 +613,27 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> { .collect(); outputs.extend(sbom_files.into_iter()); } + + // Only generates unremap files for root units. + if bcx.roots.contains(unit) && trim_paths::should_emit_unremap_file(unit) { + let unremap_files: Vec<_> = outputs + .iter() + .filter(|o| matches!(o.flavor, FileFlavor::Normal | FileFlavor::Linkable)) + .map(|output| OutputFile { + path: trim_paths::append_unremap_suffix(&output.path), + hardlink: output + .hardlink + .as_ref() + .map(trim_paths::append_unremap_suffix), + export_path: output + .export_path + .as_ref() + .map(trim_paths::append_unremap_suffix), + flavor: FileFlavor::Unremap, + }) + .collect(); + outputs.extend(unremap_files.into_iter()); + } outputs } }; diff --git a/src/compiler/build_runner/mod.rs b/src/compiler/build_runner/mod.rs index 2caec54c43c..4bf69f6585e 100644 --- a/src/compiler/build_runner/mod.rs +++ b/src/compiler/build_runner/mod.rs @@ -318,7 +318,10 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { for output in self.outputs(unit)?.iter() { if matches!( output.flavor, - FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom + FileFlavor::DebugInfo + | FileFlavor::Auxiliary + | FileFlavor::Sbom + | FileFlavor::Unremap ) { continue; } @@ -573,6 +576,16 @@ impl<'a, 'gctx> BuildRunner<'a, 'gctx> { .collect()) } + /// Returns the list of unremap file paths for a given [`Unit`]. + pub fn unremap_output_files(&self, unit: &Unit) -> CargoResult> { + Ok(self + .outputs(unit)? + .iter() + .filter(|o| o.flavor == FileFlavor::Unremap) + .map(|o| o.path.clone()) + .collect()) + } + pub fn is_primary_package(&self, unit: &Unit) -> bool { self.primary_packages.contains(&unit.pkg.package_id()) } diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index ba3f9fd181f..7c9c7c7a9dd 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -48,7 +48,7 @@ mod output_sbom; pub mod rustdoc; pub mod standard_lib; pub mod timings; -mod trim_paths; +pub(crate) mod trim_paths; mod unit; pub mod unit_dependencies; pub mod unit_graph; @@ -342,6 +342,15 @@ fn rustc( let sbom_files = build_runner.sbom_output_files(unit)?; let sbom = build_sbom(build_runner, unit)?; + let unremap_files = build_runner.unremap_output_files(unit)?; + let unremap_content = if unremap_files.is_empty() { + None + } else { + let mut buf = Vec::new(); + trim_paths::write_unremap_file(&mut buf, build_runner, unit)?; + Some(buf) + }; + let hide_diagnostics_for_scrape_unit = build_runner.bcx.unit_can_fail_for_docscraping(unit) && !matches!( build_runner.bcx.gctx.shell().verbosity(), @@ -430,6 +439,13 @@ fn rustc( serde_json::to_writer(outfile, &sbom)?; } + if let Some(content) = &unremap_content { + for file in &unremap_files { + tracing::debug!("writing unremap file to {}", file.display()); + paths::write_atomic(file, content)?; + } + } + let result = exec .exec( &rustc, diff --git a/src/compiler/output_depinfo.rs b/src/compiler/output_depinfo.rs index 1e976f141a2..48ac0b11390 100644 --- a/src/compiler/output_depinfo.rs +++ b/src/compiler/output_depinfo.rs @@ -156,7 +156,7 @@ pub fn output_depinfo(build_runner: &mut BuildRunner<'_, '_>, unit: &Unit) -> Ca for output in build_runner.outputs(unit)?.iter().filter(|o| { !matches!( o.flavor, - FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom + FileFlavor::DebugInfo | FileFlavor::Auxiliary | FileFlavor::Sbom | FileFlavor::Unremap ) }) { if let Some(ref link_dst) = output.hardlink { diff --git a/src/compiler/trim_paths.rs b/src/compiler/trim_paths.rs index 0864ba39ffe..74bb19c828d 100644 --- a/src/compiler/trim_paths.rs +++ b/src/compiler/trim_paths.rs @@ -2,18 +2,31 @@ //! //! [RFC 3127]: https://rust-lang.github.io/rfcs/3127-trim-paths.html +use std::collections::BTreeMap; +use std::collections::btree_map::Entry; use std::ffi::OsString; +use std::io::Write; use std::path::Path; +use std::path::PathBuf; use cargo_util::ProcessBuilder; use cargo_util_schemas::manifest::TomlTrimPaths; use cargo_util_schemas::manifest::TomlTrimPathsValue; +use serde::Serialize; +use tracing::debug; use super::BuildRunner; use super::Unit; +use crate::util::data_structures::HashSet; use crate::util::errors::CargoResult; use crate::util::hex; +/// The current version of the unremap file. +const CURRENT_UNREMAP_VERSION: u8 = 1; + +/// Filename suffix of the unremap file. +pub(crate) const UNREMAP_SUFFIX: &str = ".trim-paths.jsonl"; + /// Like [`trim_paths_args`] but for rustdoc invocations. pub(crate) fn trim_paths_args_rustdoc( cmd: &mut ProcessBuilder, @@ -83,78 +96,74 @@ pub(crate) fn trim_paths_args( /// [RFC 3127]: https://rust-lang.github.io/rfcs/3127-trim-paths.html pub(crate) fn trim_paths_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> [OsString; 3] { [ - package_remap(build_runner, unit), - build_dir_remap(build_runner), - sysroot_remap(build_runner), + join_remap(package_remap(build_runner, unit)), + join_remap(build_dir_remap(build_runner)), + join_remap(sysroot_remap(build_runner)), ] } +fn join_remap((from, to): (PathBuf, String)) -> OsString { + let mut remap = OsString::with_capacity(from.as_os_str().len() + 1 + to.len()); + remap.push(from); + remap.push("="); + remap.push(to); + remap +} + /// Path prefix remap rules for sysroot. /// /// This remap logic aligns with rustc: /// -fn sysroot_remap(build_runner: &BuildRunner<'_, '_>) -> OsString { - let mut remap = OsString::new(); - remap.push({ - build_runner - .bcx - .get_sysroot() - .join("lib") - .join("rustlib") - .join("src") - .join("rust") - }); - remap.push("="); - remap.push("/rustc/"); - if let Some(commit_hash) = build_runner.bcx.rustc().commit_hash.as_ref() { - remap.push(commit_hash); - } else { - remap.push(build_runner.bcx.rustc().version.to_string()); - } - remap +fn sysroot_remap(build_runner: &BuildRunner<'_, '_>) -> (PathBuf, String) { + // See also `detect_sysroot_src_path()`. + let sysroot = build_runner + .bcx + .get_sysroot() + .join("lib") + .join("rustlib") + .join("src") + .join("rust"); + + let rustc = build_runner.bcx.rustc(); + let to = match rustc.commit_hash.as_ref() { + Some(commit_hash) => format!("/rustc/{commit_hash}"), + None => format!("/rustc/{}", rustc.version), + }; + (sysroot, to) } /// Path prefix remap rules for dependencies. -fn package_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> OsString { +fn package_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> (PathBuf, String) { let pkg_root = unit.pkg.root(); let ws_root = build_runner.bcx.ws.root(); - let mut remap = OsString::new(); let source_id = unit.pkg.package_id().source_id(); if source_id.is_git() { if let Some((from, rev)) = git_checkout(build_runner, pkg_root) { const GIT_OID_LEN: usize = 7; // This matches MIN_ABBREV_LEN in git source - remap.push(from); - remap.push("=/cargo/git/"); - remap.push(hex::short_hash(source_id.canonical_url())); - remap.push("/"); - remap.push(&rev[..rev.len().min(GIT_OID_LEN)]); - return remap; + let repo = hex::short_hash(source_id.canonical_url()); + let rev = &rev[..rev.len().min(GIT_OID_LEN)]; + return (from.to_path_buf(), format!("/cargo/git/{repo}/{rev}")); } } else if source_id.is_registry() { let registry_src = build_runner.bcx.gctx.registry_source_path(); let registry_src = registry_src.as_path_unlocked(); let from = pkg_root.parent().unwrap(); if from.starts_with(registry_src) { - remap.push(from); - remap.push("=/cargo/registry/"); - remap.push(hex::short_hash(&source_id)); - return remap; + let registry = hex::short_hash(&source_id); + return (from.to_path_buf(), format!("/cargo/registry/{registry}")); } } // Handle path local dependencies and abnormal reg/git deps source location. if pkg_root.strip_prefix(ws_root).is_ok() { - remap.push(ws_root); - remap.push("=."); // remap to relative rustc work dir explicitly + // remap to relative rustc work dir explicitly + (ws_root.to_path_buf(), ".".to_owned()) } else { - remap.push(pkg_root); - remap.push("=/cargo/path/"); - remap.push(unit.pkg.name()); - remap.push("-"); - remap.push(unit.pkg.version().to_string()); + let from = pkg_root.to_path_buf(); + let to = format!("/cargo/path/{}-{}", unit.pkg.name(), unit.pkg.version()); + (from, to) } - remap } /// Finds the checkout root and revision directory name of a git dependency. @@ -189,10 +198,117 @@ fn git_checkout<'a>( /// [`file!`] macro in-place via the `OUT_DIR` environment. /// * On Linux, `DW_AT_GNU_dwo_name` that contains paths to split debuginfo /// files (dwp and dwo). -fn build_dir_remap(build_runner: &BuildRunner<'_, '_>) -> OsString { - let build_dir = build_runner.bcx.ws.build_dir(); - let mut remap = OsString::new(); - remap.push(build_dir.as_path_unlocked()); - remap.push("=/cargo/build-dir"); - remap +fn build_dir_remap(build_runner: &BuildRunner<'_, '_>) -> (PathBuf, String) { + let from = build_runner.bcx.ws.build_dir().into_path_unlocked(); + let to = "/cargo/build-dir".to_owned(); + (from, to) +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +struct UnremapVersion { + v: u8, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +struct UnremapMetadata<'a> { + rust_version: &'a str, + workspace_root: &'a Path, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +struct Remap<'a> { + from: &'a str, + to: &'a Path, +} + +/// Whether an unremap file is worth emitting beside its artifacts. +pub(crate) fn should_emit_unremap_file(unit: &Unit) -> bool { + // The unremap file is a debug companion like a dSYM or PDB. + // Without debuginfo there is nothing worth unmapping. + if !unit.profile.debuginfo.is_turned_on() { + return false; + } + + match unit.profile.trim_paths.as_ref() { + None => false, + Some(TomlTrimPaths::All) => true, + Some(TomlTrimPaths::Values(values)) => values.contains(&TomlTrimPathsValue::Object), + } +} + +/// Writes the unremap file for a unit's final artifacts. +pub(crate) fn write_unremap_file( + mut out: impl Write, + build_runner: &BuildRunner<'_, '_>, + unit: &Unit, +) -> CargoResult<()> { + let mut remaps = BTreeMap::new(); + + let mut insert = |(from, to): (PathBuf, String)| match remaps.entry(to) { + Entry::Vacant(entry) => { + entry.insert(from); + } + Entry::Occupied(entry) if *entry.get() != from => { + debug!( + "conflicting unremap records for `{}`: `{}` and `{}`", + entry.key(), + entry.get().display(), + from.display(), + ); + } + Entry::Occupied(_) => {} + }; + + insert(sysroot_remap(build_runner)); + insert(build_dir_remap(build_runner)); + + let mut seen = HashSet::default(); + let mut stack = vec![unit.clone()]; + while let Some(unit) = stack.pop() { + if !seen.insert(unit.clone()) { + continue; + } + for dep in build_runner.unit_deps(&unit) { + stack.push(dep.unit.clone()); + } + let (from, to) = package_remap(build_runner, &unit); + + // Skipping workspace remap because debugger substitutions are global in a session. + // If two unremap files with different workspace roots are loaded. + // substitutions of `.` would override each others. + if to != "." { + insert((from, to)); + } + } + + serde_json::to_writer( + &mut out, + &UnremapVersion { + v: CURRENT_UNREMAP_VERSION, + }, + )?; + out.write_all(b"\n")?; + let rust_version = build_runner.bcx.rustc().version.to_string(); + let metadata = UnremapMetadata { + rust_version: &rust_version, + workspace_root: build_runner.bcx.ws.root(), + }; + serde_json::to_writer(&mut out, &metadata)?; + out.write_all(b"\n")?; + for (from, to) in &remaps { + serde_json::to_writer(&mut out, &Remap { from, to })?; + out.write_all(b"\n")?; + } + + Ok(()) +} + +/// Appends the unremap file suffix to an artifact path. +pub(crate) fn append_unremap_suffix(link: &PathBuf) -> PathBuf { + let mut link_buf = link.clone().into_os_string(); + link_buf.push(UNREMAP_SUFFIX); + PathBuf::from(link_buf) } diff --git a/src/ops/cargo_clean.rs b/src/ops/cargo_clean.rs index 2b76f0d1408..10f5ba8541a 100644 --- a/src/ops/cargo_clean.rs +++ b/src/ops/cargo_clean.rs @@ -1,3 +1,4 @@ +use crate::compiler::trim_paths; use crate::compiler::{CompileKind, CompileMode, Layout, RustcTargetData}; use crate::ops; use crate::util::HumanBytes; @@ -386,9 +387,13 @@ fn clean_specs( // Some files include a hash in the filename, some don't. let (prefix, suffix) = file_type.output_prefix_suffix(target); let unhashed_name = file_type.output_filename(target, None); + // Handle uplifted or unhashed output (e.g. on MSVC executables) + let unhashed_unremap = + format!("{unhashed_name}{}", trim_paths::UNREMAP_SUFFIX); dirs_to_clean.mark_utf(&dir, |filename| { (filename.starts_with(&prefix) && filename.ends_with(&suffix)) || unhashed_name == filename + || unhashed_unremap == filename }); // Remove the uplifted copy. @@ -399,6 +404,9 @@ fn clean_specs( // Dep-info generated by Cargo itself. let dep_info = uplifted_path.with_extension("d"); clean_ctx.rm_rf(&dep_info)?; + // Unremap file emitted for `-Ztrim-paths`. + let unremap = trim_paths::append_unremap_suffix(&uplifted_path); + clean_ctx.rm_rf(&unremap)?; } } let unhashed_dep_info = format!("{}.d", crate_name); @@ -409,6 +417,8 @@ fn clean_specs( // Remove dep-info file generated by rustc. It is not tracked in // file_types. It does not have a prefix. filename.ends_with(".d") + // Unremap file emitted for `-Ztrim-paths`. + || filename.ends_with(trim_paths::UNREMAP_SUFFIX) } else if filename.starts_with(&path_dot) { // Remove split-debuginfo files generated by rustc. [".o", ".dwo", ".dwp"] diff --git a/tests/testsuite/profile_trim_paths.rs b/tests/testsuite/profile_trim_paths.rs index e6a523dd222..9bd401f7e82 100644 --- a/tests/testsuite/profile_trim_paths.rs +++ b/tests/testsuite/profile_trim_paths.rs @@ -2,6 +2,7 @@ use crate::prelude::*; use cargo_test_support::basic_manifest; +use cargo_test_support::compare::assert_e2e; use cargo_test_support::git; use cargo_test_support::paths; use cargo_test_support::project; @@ -244,6 +245,38 @@ fn registry_dependency() { "#]]) .run(); + + // Unremap files for both original exe and uplifted exe. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert_e2e().eq( + &std::fs::read_to_string(&unremap_file).unwrap(), + str![[r#" +[ + { + "v": 1 + }, + { + "rust_version": "[..]", + "workspace_root": "[ROOT]/foo" + }, + { + "from": "/cargo/build-dir", + "to": "[ROOT]/foo/target" + }, + { + "from": "/cargo/registry/[..]", + "to": "[ROOT]/home/.cargo/registry/src/-[HASH]" + }, + { + "from": "/rustc/[..]", + "to": "[..]/lib/rustlib/src/rust" + } +] +"#]] + .is_json() + .against_jsonlines(), + ); } #[cargo_test] @@ -316,6 +349,11 @@ fn registry_dependency_with_build_script_codegen() { "#]]) .run(); + + // Unremap files for both original exe and uplifted exe. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(unremap_file.exists()); } #[cargo_test] @@ -366,6 +404,38 @@ fn git_dependency() { "#]]) .run(); + + // Unremap files for both original exe and uplifted exe. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert_e2e().eq( + &std::fs::read_to_string(&unremap_file).unwrap(), + str![[r#" +[ + { + "v": 1 + }, + { + "rust_version": "[..]", + "workspace_root": "[ROOT]/foo" + }, + { + "from": "/cargo/build-dir", + "to": "[ROOT]/foo/target" + }, + { + "from": "/cargo/git/[..]", + "to": "[ROOT]/home/.cargo/git/checkouts/bar-[..]" + }, + { + "from": "/rustc/[..]", + "to": "[..]/lib/rustlib/src/rust" + } +] +"#]] + .is_json() + .against_jsonlines(), + ); } #[cargo_test] @@ -411,6 +481,34 @@ cocktail-bar/src/lib.rs "#]]) .run(); + + // Unremap files for both original exe and uplifted exe. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert_e2e().eq( + &std::fs::read_to_string(&unremap_file).unwrap(), + str![[r#" +[ + { + "v": 1 + }, + { + "rust_version": "[..]", + "workspace_root": "[ROOT]/foo" + }, + { + "from": "/cargo/build-dir", + "to": "[ROOT]/foo/target" + }, + { + "from": "/rustc/[..]", + "to": "[..]/lib/rustlib/src/rust" + } +] +"#]] + .is_json() + .against_jsonlines(), + ); } #[cargo_test] @@ -457,6 +555,38 @@ fn path_dependency_outside_workspace() { "#]]) .run(); + + // Unremap files for both original exe and uplifted exe. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert_e2e().eq( + &std::fs::read_to_string(&unremap_file).unwrap(), + str![[r#" +[ + { + "v": 1 + }, + { + "rust_version": "[..]", + "workspace_root": "[ROOT]/foo" + }, + { + "from": "/cargo/build-dir", + "to": "[ROOT]/foo/target" + }, + { + "from": "/cargo/path/bar-0.0.1", + "to": "[ROOT]/bar" + }, + { + "from": "/rustc/[..]", + "to": "[..]/lib/rustlib/src/rust" + } +] +"#]] + .is_json() + .against_jsonlines(), + ); } #[cargo_test] @@ -537,6 +667,34 @@ fn vendored_dependencies() { "#]]) .run(); + + // Unremap files for both original exe and uplifted exe. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert_e2e().eq( + &std::fs::read_to_string(&unremap_file).unwrap(), + str![[r#" +[ + { + "v": 1 + }, + { + "rust_version": "[..]", + "workspace_root": "[ROOT]/foo" + }, + { + "from": "/cargo/build-dir", + "to": "[ROOT]/foo/target" + }, + { + "from": "/rustc/[..]", + "to": "[..]/lib/rustlib/src/rust" + } +] +"#]] + .is_json() + .against_jsonlines(), + ); } #[cargo_test] @@ -618,6 +776,42 @@ fn vendored_dependencies_outside_workspace() { "#]]) .run(); + + // Unremap files for both original exe and uplifted exe. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert_e2e().eq( + &std::fs::read_to_string(&unremap_file).unwrap(), + str![[r#" +[ + { + "v": 1 + }, + { + "rust_version": "[..]", + "workspace_root": "[ROOT]/foo" + }, + { + "from": "/cargo/build-dir", + "to": "[ROOT]/foo/target" + }, + { + "from": "/cargo/path/bar-0.0.1", + "to": "[ROOT]/shared-vendor/bar" + }, + { + "from": "/cargo/path/baz-0.0.1", + "to": "[ROOT]/shared-vendor/baz" + }, + { + "from": "/rustc/[..]", + "to": "[..]/lib/rustlib/src/rust" + } +] +"#]] + .is_json() + .against_jsonlines(), + ); } #[cargo_test] @@ -676,6 +870,11 @@ fn local_package_with_build_script_codegen() { "#]]) .run(); + + // Unremap files for both original exe and uplifted exe. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(unremap_file.exists()); } #[cargo_test] @@ -721,6 +920,9 @@ fn diagnostics_works() { ... "#]]) .run(); + + // Non `object` scope never emits unremap files. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); } #[cfg(target_os = "macos")] @@ -1332,3 +1534,292 @@ fn command_output(command: &mut std::process::Command, name: &str) -> std::proce ); output } + +#[cargo_test] +fn unremap_file_rebuild() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [profile.dev] + trim-paths = "object" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build -Ztrim-paths") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .run(); + assert!(p.bin("foo").is_file()); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(unremap_file.exists()); + + // Deleting the uplifted copy won't cause rebuild. + std::fs::remove_file(&unremap_file).unwrap(); + p.cargo("build --verbose -Ztrim-paths") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .with_stderr_data(str![[r#" +[FRESH] foo v0.0.1 ([ROOT]/foo) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + assert!(unremap_file.exists()); + + // Deleting the original one will cause rebuild. + // The non-uplifted copy is the one that is not `unremap_file`, + // as its file name layout varies across platforms. + let deps_file = p + .glob("target/**/*.trim-paths.jsonl") + .map(|f| f.unwrap()) + .find(|f| *f != unremap_file) + .unwrap(); + std::fs::remove_file(&deps_file).unwrap(); + p.cargo("build --verbose -Ztrim-paths") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .with_stderr_data(str![[r#" +[DIRTY] foo v0.0.1 ([ROOT]/foo): couldn't read metadata for file `target/debug/[..]/foo[..].trim-paths.jsonl` +[COMPILING] foo v0.0.1 ([ROOT]/foo) +[RUNNING] `rustc [..]` +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); + assert!(deps_file.exists()); + assert!(unremap_file.exists()); +} + +#[cargo_test] +fn unremap_file_without_debuginfo() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [profile.dev] + trim-paths = "object" + debug = 0 + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build -Ztrim-paths") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .run(); + + // No debuginfo. No unremap file. + assert!(p.bin("foo").is_file()); + assert!(!unremap_file_path(&p.bin("foo")).exists()); +} + +#[cargo_test] +fn unremap_file_with_cargo_clean() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [profile.dev] + trim-paths = "object" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build -Ztrim-paths") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .run(); + + assert!(unremap_file_path(&p.bin("foo")).exists()); + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 2); + + p.cargo("clean -p foo -Ztrim-paths") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .run(); + + assert!(!unremap_file_path(&p.bin("foo")).exists()); + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); +} + +// MSVC always emits a PDB when debuginfo is on (which the unremap file requires), +// It adds a third `filenames` entry in JSON message. +// Skip to make snapshot's life easier. +#[cfg(not(target_env = "msvc"))] +#[cargo_test] +fn unremap_file_in_json_messages() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [profile.dev] + trim-paths = "object" + # Suppress the platform-default dSYM on macOS so that `filenames` + # in JSON message is identical on all non-MSVC platforms. + split-debuginfo = "off" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build -Ztrim-paths --message-format=json") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .with_stdout_data( + str![[r#" +[ + { + "executable": "[ROOT]/foo/target/debug/foo[EXE]", + "features": [], + "filenames": [ + "[ROOT]/foo/target/debug/foo[EXE]", + "[ROOT]/foo/target/debug/foo[EXE].trim-paths.jsonl" + ], + "fresh": false, + "manifest_path": "[ROOT]/foo/Cargo.toml", + "package_id": "path+[ROOTURL]/foo#0.0.1", + "profile": "{...}", + "reason": "compiler-artifact", + "target": "{...}" + }, + { + "reason": "build-finished", + "success": true + } +] +"#]] + .is_json() + .against_jsonlines(), + ) + .run(); +} + +#[cargo_test] +fn unremap_file_with_artifact_dir() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [profile.dev] + trim-paths = "object" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("build -Ztrim-paths -Zunstable-options --artifact-dir out") + .masquerade_as_nightly_cargo(&["-Ztrim-paths", "unstable-options"]) + .run(); + + let exported = p + .root() + .join("out") + .join(format!("foo{}", std::env::consts::EXE_SUFFIX)); + assert!(exported.is_file()); + assert!(unremap_file_path(&exported).exists()); +} + +#[cargo_test] +fn unremap_file_for_all_bin_types() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [profile.dev] + trim-paths = "object" + "#, + ) + .file("src/lib.rs", "#[test] fn t() {}") + .file("tests/it.rs", "#[test] fn t() {}") + .file("examples/ex.rs", "fn main() {}") + .build(); + + p.cargo("test --no-run -Ztrim-paths") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .run(); + + // Unit test, integration test, and example binaries are all root units + // and receive unremap files. + assert_eq!(p.glob("target/**/foo-*.trim-paths.jsonl").count(), 1); + assert_eq!(p.glob("target/**/it-*.trim-paths.jsonl").count(), 1); + // MSVC executables don't get a hashed filename + // The PDB path is embedded in the executable. + let expected = if cfg!(target_env = "msvc") { 1 } else { 2 }; + assert_eq!( + p.glob("target/debug/examples/*.trim-paths.jsonl").count(), + expected + ); +} + +#[cargo_test] +fn unremap_file_with_multiple_crate_types() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + + [lib] + crate-type = ["cdylib", "staticlib"] + + [profile.dev] + trim-paths = "object" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("build -Ztrim-paths") + .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) + .run(); + + // Unremap files for both original cdylib/staticlib and uplifted ones. + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 4); + let uplifted: Vec<_> = p + .glob("target/debug/*.trim-paths.jsonl") + .map(|f| f.unwrap()) + .collect(); + assert_eq!(uplifted.len(), 2); + let contents: Vec<_> = uplifted + .iter() + .map(|f| std::fs::read_to_string(f).unwrap()) + .collect(); + assert_eq!(contents[0], contents[1]); +} + +fn unremap_file_path(artifact: &std::path::Path) -> std::path::PathBuf { + let mut path = artifact.as_os_str().to_owned(); + path.push(".trim-paths.jsonl"); + path.into() +}