From f97d6b1a24a7da2c6b3b8798969a1b408581f8bf Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Jul 2026 23:55:57 -0400 Subject: [PATCH 1/4] test(trim-paths): cover unremap file scenarios --- tests/testsuite/profile_trim_paths.rs | 286 ++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) diff --git a/tests/testsuite/profile_trim_paths.rs b/tests/testsuite/profile_trim_paths.rs index e6a523dd222..980c4793376 100644 --- a/tests/testsuite/profile_trim_paths.rs +++ b/tests/testsuite/profile_trim_paths.rs @@ -244,6 +244,10 @@ fn registry_dependency() { "#]]) .run(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(!unremap_file.exists()); } #[cargo_test] @@ -316,6 +320,10 @@ fn registry_dependency_with_build_script_codegen() { "#]]) .run(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(!unremap_file.exists()); } #[cargo_test] @@ -366,6 +374,10 @@ fn git_dependency() { "#]]) .run(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(!unremap_file.exists()); } #[cargo_test] @@ -411,6 +423,10 @@ cocktail-bar/src/lib.rs "#]]) .run(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(!unremap_file.exists()); } #[cargo_test] @@ -457,6 +473,10 @@ fn path_dependency_outside_workspace() { "#]]) .run(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(!unremap_file.exists()); } #[cargo_test] @@ -537,6 +557,10 @@ fn vendored_dependencies() { "#]]) .run(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(!unremap_file.exists()); } #[cargo_test] @@ -618,6 +642,10 @@ fn vendored_dependencies_outside_workspace() { "#]]) .run(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(!unremap_file.exists()); } #[cargo_test] @@ -676,6 +704,10 @@ fn local_package_with_build_script_codegen() { "#]]) .run(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + let unremap_file = unremap_file_path(&p.bin("foo")); + assert!(!unremap_file.exists()); } #[cargo_test] @@ -721,6 +753,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 +1367,254 @@ 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()); + assert!(!unremap_file_path(&p.bin("foo")).exists()); + + 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_path(&p.bin("foo")).exists()); + + 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_path(&p.bin("foo")).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(), 0); + + 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]" + ], + "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(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); +} + +#[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(); + + assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); +} + +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() +} From a14b3031538b9c020af0e0bbe246e7835f5df4d0 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Jul 2026 23:09:30 -0400 Subject: [PATCH 2/4] refactor(trim-paths): return remap pairs from rules Split remap rule logic into its `(, )` pair as return values. This is meant to be used in unremap file generation. --- src/compiler/trim_paths.rs | 91 ++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/src/compiler/trim_paths.rs b/src/compiler/trim_paths.rs index 0864ba39ffe..5fbf7558d39 100644 --- a/src/compiler/trim_paths.rs +++ b/src/compiler/trim_paths.rs @@ -4,6 +4,7 @@ use std::ffi::OsString; use std::path::Path; +use std::path::PathBuf; use cargo_util::ProcessBuilder; use cargo_util_schemas::manifest::TomlTrimPaths; @@ -83,78 +84,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 +186,8 @@ 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) } From 19cb6ce35fedc11bb7186b6c3f8428b82659c851 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Jul 2026 23:12:19 -0400 Subject: [PATCH 3/4] feat(trim-paths): emit unremap files for final artifacts This implements the unremap file discussed in 2026 All-Hands. The logic basically follows the unstable SBOM file generation. See for the concept of the unremap file. See the doc update commit for more about the file format. 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"} ``` --- src/compiler/build_context/target_info.rs | 2 + .../build_runner/compilation_files.rs | 22 ++ src/compiler/build_runner/mod.rs | 15 +- src/compiler/mod.rs | 18 +- src/compiler/output_depinfo.rs | 2 +- src/compiler/trim_paths.rs | 121 ++++++++ src/ops/cargo_clean.rs | 10 + tests/testsuite/profile_trim_paths.rs | 259 ++++++++++++++++-- 8 files changed, 419 insertions(+), 30 deletions(-) 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 5fbf7558d39..74bb19c828d 100644 --- a/src/compiler/trim_paths.rs +++ b/src/compiler/trim_paths.rs @@ -2,19 +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, @@ -191,3 +203,112 @@ fn build_dir_remap(build_runner: &BuildRunner<'_, '_>) -> (PathBuf, String) { 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 980c4793376..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; @@ -245,9 +246,37 @@ fn registry_dependency() { "#]]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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()); + 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] @@ -321,9 +350,10 @@ fn registry_dependency_with_build_script_codegen() { "#]]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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()); + assert!(unremap_file.exists()); } #[cargo_test] @@ -375,9 +405,37 @@ fn git_dependency() { "#]]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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()); + 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] @@ -424,9 +482,33 @@ cocktail-bar/src/lib.rs "#]]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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()); + 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] @@ -474,9 +556,37 @@ fn path_dependency_outside_workspace() { "#]]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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()); + 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] @@ -558,9 +668,33 @@ fn vendored_dependencies() { "#]]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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()); + 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] @@ -643,9 +777,41 @@ fn vendored_dependencies_outside_workspace() { "#]]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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()); + 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] @@ -705,9 +871,10 @@ fn local_package_with_build_script_codegen() { "#]]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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()); + assert!(unremap_file.exists()); } #[cargo_test] @@ -1390,8 +1557,11 @@ fn unremap_file_rebuild() { .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) .run(); assert!(p.bin("foo").is_file()); - assert!(!unremap_file_path(&p.bin("foo")).exists()); + 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#" @@ -1400,17 +1570,29 @@ fn unremap_file_rebuild() { "#]]) .run(); - assert!(!unremap_file_path(&p.bin("foo")).exists()); - + 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#" -[FRESH] foo v0.0.1 ([ROOT]/foo) +[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!(!unremap_file_path(&p.bin("foo")).exists()); + assert!(deps_file.exists()); + assert!(unremap_file.exists()); } #[cargo_test] @@ -1462,8 +1644,9 @@ fn unremap_file_with_cargo_clean() { 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(), 0); + + 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"]) @@ -1507,7 +1690,8 @@ fn unremap_file_in_json_messages() { "executable": "[ROOT]/foo/target/debug/foo[EXE]", "features": [], "filenames": [ - "[ROOT]/foo/target/debug/foo[EXE]" + "[ROOT]/foo/target/debug/foo[EXE]", + "[ROOT]/foo/target/debug/foo[EXE].trim-paths.jsonl" ], "fresh": false, "manifest_path": "[ROOT]/foo/Cargo.toml", @@ -1555,7 +1739,7 @@ fn unremap_file_with_artifact_dir() { .join("out") .join(format!("foo{}", std::env::consts::EXE_SUFFIX)); assert!(exported.is_file()); - assert!(!unremap_file_path(&exported).exists()); + assert!(unremap_file_path(&exported).exists()); } #[cargo_test] @@ -1582,7 +1766,17 @@ fn unremap_file_for_all_bin_types() { .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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] @@ -1610,7 +1804,18 @@ fn unremap_file_with_multiple_crate_types() { .masquerade_as_nightly_cargo(&["-Ztrim-paths"]) .run(); - assert_eq!(p.glob("target/**/*.trim-paths.jsonl").count(), 0); + // 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 { From 1bfa33c676d1be3f9cd23836bb028543d039539c Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Jul 2026 23:15:44 -0400 Subject: [PATCH 4/4] docs(trim-paths): emit unremap files for final artifacts --- doc/book/src/reference/unstable.md | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) 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)*