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