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