Skip to main content

cargo/compiler/
trim_paths.rs

1//! Path prefix remapping for [RFC 3127] `trim-paths`.
2//!
3//! [RFC 3127]: https://rust-lang.github.io/rfcs/3127-trim-paths.html
4
5use std::collections::BTreeMap;
6use std::collections::btree_map::Entry;
7use std::ffi::OsString;
8use std::io::Write;
9use std::path::Path;
10use std::path::PathBuf;
11
12use cargo_util::ProcessBuilder;
13use cargo_util_schemas::manifest::TomlTrimPaths;
14use cargo_util_schemas::manifest::TomlTrimPathsValue;
15use serde::Serialize;
16use tracing::debug;
17
18use super::BuildRunner;
19use super::Unit;
20use crate::util::data_structures::HashSet;
21use crate::util::errors::CargoResult;
22use crate::util::hex;
23use crate::util::path_args;
24
25/// The current version of the unremap file.
26const CURRENT_UNREMAP_VERSION: u8 = 1;
27
28/// Filename suffix of the unremap file.
29pub(crate) const UNREMAP_SUFFIX: &str = ".trim-paths.jsonl";
30
31/// This is an internal contract with rustc bootstrap,
32/// which needs workspace sources remapped to `/rust{c,-dev}/<sha>`.
33///
34/// See <https://github.com/rust-lang/cargo/issues/17309>.
35pub(crate) const WS_REMAP_ENV: &str = "__CARGO_RUSTC_BOOTSTRAP_WS_REMAP";
36
37/// Like [`trim_paths_args`] but for rustdoc invocations.
38pub(crate) fn trim_paths_args_rustdoc(
39    cmd: &mut ProcessBuilder,
40    build_runner: &BuildRunner<'_, '_>,
41    unit: &Unit,
42    trim_paths: &TomlTrimPaths,
43) -> CargoResult<()> {
44    match trim_paths {
45        // rustdoc supports diagnostics trimming only.
46        TomlTrimPaths::Values(values) if !values.contains(&TomlTrimPathsValue::Diagnostics) => {
47            return Ok(());
48        }
49        _ => {}
50    }
51
52    for pair in trim_paths_remap(build_runner, unit) {
53        let mut arg = OsString::from("--remap-path-prefix=");
54        arg.push(pair);
55        cmd.arg(arg);
56    }
57
58    Ok(())
59}
60
61/// Generates the `--remap-path-scope` and `--remap-path-prefix` for [RFC 3127].
62/// See also unstable feature [`-Ztrim-paths`].
63///
64/// [RFC 3127]: https://rust-lang.github.io/rfcs/3127-trim-paths.html
65/// [`-Ztrim-paths`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#profile-trim-paths-option
66pub(crate) fn trim_paths_args(
67    cmd: &mut ProcessBuilder,
68    build_runner: &BuildRunner<'_, '_>,
69    unit: &Unit,
70    trim_paths: &TomlTrimPaths,
71) -> CargoResult<()> {
72    if trim_paths.is_none() {
73        return Ok(());
74    }
75
76    // feature gate was checked during manifest/config parsing.
77    cmd.arg(format!("--remap-path-scope={trim_paths}"));
78
79    for pair in trim_paths_remap(build_runner, unit) {
80        let mut arg = OsString::from("--remap-path-prefix=");
81        arg.push(pair);
82        cmd.arg(arg);
83    }
84
85    Ok(())
86}
87
88/// Computes the `<from>=<to>` path remap pairs for [RFC 3127] trim-paths.
89///
90/// Order of `--remap-path-prefix` flags is important for `-Zbuild-std`.
91/// We want to show `/rustc/<hash>/library/std` instead of `std-0.0.0`.
92///
93/// | Category     | From                                             | To                                 |
94/// |--------------|--------------------------------------------------|------------------------------------|
95/// | Sysroot      | `<sysroot>/lib/rustlib/src/rust`                 | `/rustc/<commit-hash>`             |
96/// | Registry dep | `$CARGO_HOME/registry/src/<registry-dir>`        | `/cargo/registry/<registry-id>`    |
97/// | Git dep      | `$CARGO_HOME/git/checkouts/<repo-dir>/<rev-dir>` | `/cargo/git/<git-source-id>/<rev>` |
98/// | Workspace    | `<workspace-root>`                               | `.` (workspace-relative)           |
99/// | Path dep†    | `<pkg-root>`                                     | `/cargo/deps/<name>-<version>`     |
100/// | Vendored     | `<pkg-root>` (by file location)                  | workspace or path rules above      |
101/// | Build dir    | `<build-dir>`                                    | `/cargo/build-dir`                 |
102///
103/// **†**: path dependencies outside the workspace and other uncategorized dependencies.
104///
105/// [RFC 3127]: https://rust-lang.github.io/rfcs/3127-trim-paths.html
106pub(crate) fn trim_paths_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> [OsString; 3] {
107    [
108        join_remap(package_remap(build_runner, unit)),
109        join_remap(build_dir_remap(build_runner)),
110        join_remap(sysroot_remap(build_runner)),
111    ]
112}
113
114fn join_remap((from, to): (PathBuf, String)) -> OsString {
115    let mut remap = OsString::with_capacity(from.as_os_str().len() + 1 + to.len());
116    remap.push(from);
117    remap.push("=");
118    remap.push(to);
119    remap
120}
121
122/// Path prefix remap rules for sysroot.
123///
124/// This remap logic aligns with rustc:
125/// <https://github.com/rust-lang/rust/blob/c2ef3516/src/bootstrap/src/lib.rs#L1113-L1116>
126fn sysroot_remap(build_runner: &BuildRunner<'_, '_>) -> (PathBuf, String) {
127    // See also `detect_sysroot_src_path()`.
128    let sysroot = build_runner
129        .bcx
130        .get_sysroot()
131        .join("lib")
132        .join("rustlib")
133        .join("src")
134        .join("rust");
135
136    let rustc = build_runner.bcx.rustc();
137    let to = match rustc.commit_hash.as_ref() {
138        Some(commit_hash) => format!("/rustc/{commit_hash}"),
139        None => format!("/rustc/{}", rustc.version),
140    };
141    (sysroot, to)
142}
143
144/// Path prefix remap rules for dependencies.
145fn package_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> (PathBuf, String) {
146    let pkg_root = unit.pkg.root();
147    let ws_root = build_runner.bcx.ws.root();
148    let source_id = unit.pkg.package_id().source_id();
149
150    if source_id.is_git() {
151        if let Some((from, rev)) = git_checkout(build_runner, pkg_root) {
152            const GIT_OID_LEN: usize = 7; // This matches MIN_ABBREV_LEN in git source
153            let repo = hex::short_hash(source_id.canonical_url());
154            let rev = &rev[..rev.len().min(GIT_OID_LEN)];
155            return (from.to_path_buf(), format!("/cargo/git/{repo}/{rev}"));
156        }
157    } else if source_id.is_registry() {
158        let registry_src = build_runner.bcx.gctx.registry_source_path();
159        let registry_src = registry_src.as_path_unlocked();
160        let from = pkg_root.parent().unwrap();
161        if from.starts_with(registry_src) {
162            let registry = hex::short_hash(&source_id);
163            return (from.to_path_buf(), format!("/cargo/registry/{registry}"));
164        }
165    }
166
167    // Handle path local dependencies and abnormal reg/git deps source location.
168    if pkg_root.strip_prefix(ws_root).is_ok() {
169        workspace_remap(build_runner, unit)
170    } else {
171        let from = pkg_root.to_path_buf();
172        let to = format!("/cargo/deps/{}-{}", unit.pkg.name(), unit.pkg.version());
173        (from, to)
174    }
175}
176
177/// Path prefix remap rules for dependencies within workspaces.
178fn workspace_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> (PathBuf, String) {
179    let ws_root = build_runner.bcx.ws.root();
180    // rustc working directory is usually workspace root.
181    // However, when `-Zroot-dir` is set, it may not be.
182    // We may need to remap to that otherwise debuginfo like `DW_AT_comp_dir`
183    // would point to rustc working directory,
184    // and won't be remapped by workspace root.
185    let (_, rustc_workdir) = path_args(build_runner.bcx.ws, unit);
186    let from = if ws_root.starts_with(&rustc_workdir) {
187        rustc_workdir
188    } else {
189        ws_root.to_path_buf()
190    };
191    let to = build_runner
192        .bcx
193        .gctx
194        .get_env(WS_REMAP_ENV)
195        .ok()
196        .filter(|prefix| !prefix.is_empty())
197        .unwrap_or(".")
198        .to_owned();
199    (from, to)
200}
201
202/// Finds the checkout root and revision directory name of a git dependency.
203///
204/// This is built under this layout: `$CARGO_HOME/git/checkouts/<repo>-<hash>[-shallow]/<rev>`.
205///
206/// `None` when the package does not live under the global git checkouts directory,
207/// for example a vendored git dependency.
208fn git_checkout<'a>(
209    build_runner: &BuildRunner<'_, '_>,
210    pkg_root: &'a Path,
211) -> Option<(&'a Path, &'a str)> {
212    let checkouts = build_runner.bcx.gctx.git_checkouts_path();
213    let checkouts = checkouts.as_path_unlocked();
214    let rel = pkg_root.strip_prefix(checkouts).ok()?;
215    let mut components = rel.components();
216    let (_repo, rev) = (components.next()?, components.next()?);
217    let rev = rev.as_os_str().to_str()?;
218    let checkout_root = pkg_root.ancestors().nth(components.count())?;
219    Some((checkout_root, rev))
220}
221
222/// Remap all paths pointing to `build.build-dir`,
223/// i.e., `[BUILD_DIR]/debug/deps/foo-[HASH].dwo` would be remapped to
224/// `/cargo/build-dir/debug/deps/foo-[HASH].dwo`
225/// (note the `/cargo/build-dir` prefix).
226///
227/// This covers scenarios like:
228///
229/// * Build script generated code. For example, a build script may call `file!`
230///   macros, and the associated crate uses [`include!`] to include the expanded
231///   [`file!`] macro in-place via the `OUT_DIR` environment.
232/// * On Linux, `DW_AT_GNU_dwo_name` that contains paths to split debuginfo
233///   files (dwp and dwo).
234fn build_dir_remap(build_runner: &BuildRunner<'_, '_>) -> (PathBuf, String) {
235    let from = build_runner.bcx.ws.build_dir().into_path_unlocked();
236    let to = "/cargo/build-dir".to_owned();
237    (from, to)
238}
239
240#[derive(Serialize)]
241#[serde(rename_all = "snake_case")]
242struct UnremapVersion {
243    v: u8,
244}
245
246#[derive(Serialize)]
247#[serde(rename_all = "snake_case")]
248struct UnremapMetadata<'a> {
249    rust_version: &'a str,
250    workspace_root: &'a Path,
251}
252
253#[derive(Serialize)]
254#[serde(rename_all = "snake_case")]
255struct Remap<'a> {
256    from: &'a str,
257    to: &'a Path,
258}
259
260/// Whether an unremap file is worth emitting beside its artifacts.
261pub(crate) fn should_emit_unremap_file(unit: &Unit) -> bool {
262    // The unremap file is a debug companion like a dSYM or PDB.
263    // Without debuginfo there is nothing worth unmapping.
264    if !unit.profile.debuginfo.is_turned_on() {
265        return false;
266    }
267
268    match unit.profile.trim_paths.as_ref() {
269        None => false,
270        Some(TomlTrimPaths::All) => true,
271        Some(TomlTrimPaths::Values(values)) => values.contains(&TomlTrimPathsValue::Object),
272    }
273}
274
275/// Writes the unremap file for a unit's final artifacts.
276pub(crate) fn write_unremap_file(
277    mut out: impl Write,
278    build_runner: &BuildRunner<'_, '_>,
279    unit: &Unit,
280) -> CargoResult<()> {
281    let mut remaps = BTreeMap::new();
282
283    let mut insert = |(from, to): (PathBuf, String)| match remaps.entry(to) {
284        Entry::Vacant(entry) => {
285            entry.insert(from);
286        }
287        Entry::Occupied(entry) if *entry.get() != from => {
288            debug!(
289                "conflicting unremap records for `{}`: `{}` and `{}`",
290                entry.key(),
291                entry.get().display(),
292                from.display(),
293            );
294        }
295        Entry::Occupied(_) => {}
296    };
297
298    insert(sysroot_remap(build_runner));
299    insert(build_dir_remap(build_runner));
300
301    let mut seen = HashSet::default();
302    let mut stack = vec![unit.clone()];
303    while let Some(unit) = stack.pop() {
304        if !seen.insert(unit.clone()) {
305            continue;
306        }
307        for dep in build_runner.unit_deps(&unit) {
308            stack.push(dep.unit.clone());
309        }
310        insert(package_remap(build_runner, &unit));
311    }
312
313    serde_json::to_writer(
314        &mut out,
315        &UnremapVersion {
316            v: CURRENT_UNREMAP_VERSION,
317        },
318    )?;
319    out.write_all(b"\n")?;
320    let rust_version = build_runner.bcx.rustc().version.to_string();
321    let metadata = UnremapMetadata {
322        rust_version: &rust_version,
323        workspace_root: build_runner.bcx.ws.root(),
324    };
325    serde_json::to_writer(&mut out, &metadata)?;
326    out.write_all(b"\n")?;
327    for (from, to) in &remaps {
328        serde_json::to_writer(&mut out, &Remap { from, to })?;
329        out.write_all(b"\n")?;
330    }
331
332    Ok(())
333}
334
335/// Appends the unremap file suffix to an artifact path.
336pub(crate) fn append_unremap_suffix(link: &PathBuf) -> PathBuf {
337    let mut link_buf = link.clone().into_os_string();
338    link_buf.push(UNREMAP_SUFFIX);
339    PathBuf::from(link_buf)
340}