unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
build-systemsconfiguration-managementrefactoring

bootstrap: leverage cargo trim-paths

Project: rust-lang/rust | Language: Rust | Concepts: build-systems, configuration-management, refactoring | Difficulty: mid
Link: https://github.com/rust-lang/rust/pull/161049

The problem

The Rust bootstrap build system was manually remapping file paths in debug info to make builds reproducible and to avoid leaking local filesystem paths into distributed artifacts. This was done using custom environment variables and logic to pass remap flags to both Cargo and the compiler. With recent improvements in Cargo (-Ztrim-paths), much of this manual remapping could be replaced with standardized, built-in tooling.

Why it's non-trivial

A naive fix would simply enable -Ztrim-paths without considering:

  • The need to preserve existing remap conventions for certain artifacts (like /rustc/<sha>, /rustc-dev/<sha>, etc).
  • The fact that Cargo's remapping behavior and environment variable contracts are subtle and evolving (e.g., __CARGO_RUSTC_BOOTSTRAP_WS_REMAP).
  • Ensuring that all build steps (including C/C++ compilation for LLVM) still produce correct, reproducible debug info.
  • Removing old, now-unnecessary code without breaking existing workflows or introducing regressions.

The approach

1. Remove manual remap flag handling from build scripts

Previously, the build scripts for both the compiler and LLVM manually parsed environment variables and passed remap flags:

-    // Remap ci-llvm include paths in debug info for reproducible builds.
-    if let Some(maps) = tracked_env_var_os("RUSTC_DEBUGINFO_MAP")
-        && let Some(maps_str) = maps.to_str()
-    {
-        for map in maps_str.split('\t') {
-            cfg.flag_if_supported(&format!("-ffile-prefix-map={map}"));
-        }
-    }

This code in rustc_llvm/build.rs is removed. It used to manually remap paths for LLVM debug info.

Similarly, in the bootstrap wrapper for rustc:

-    // The remap flags for the compiler and standard library sources.
-    if let Ok(maps) = env::var("RUSTC_DEBUGINFO_MAP") {
-        for map in maps.split('\t') {
-            cmd.arg("--remap-path-prefix").arg(map);
-        }
-    }
-    // The remap flags for Cargo registry sources need to be passed after the remapping for the
-    // Rust source code directory, to handle cases when $CARGO_HOME is inside the source directory.
-    if let Ok(maps) = env::var("RUSTC_CARGO_REGISTRY_SRC_TO_REMAP") {
-        for map in maps.split('\t') {
-            cmd.arg("--remap-path-prefix").arg(map);
-        }
-    }

This code in src/bootstrap/src/bin/rustc.rs is also removed, eliminating the manual propagation of remap flags.

2. Use Cargo's -Ztrim-paths and new environment variables

The new approach leverages Cargo's unstable -Ztrim-paths feature and its internal contract for remapping workspace sources:

+        let trim_paths = |cargo: &mut BootstrapCommand, ws_remap: &str| {
+            cargo.arg("-Ztrim-paths");
+            cargo.arg("--config").arg("profile.release.trim-paths='all'");
+            cargo.arg("--config").arg("profile.dev.trim-paths='all'");
+            // This is an internal contract with cargo.
+            // bootstrap needs workspace sources remapped to `/rust{c,-dev}/<sha>` instead of `.`
+            // See <https://github.com/rust-lang/cargo/issues/17309>.
+            cargo.env("__CARGO_RUSTC_BOOTSTRAP_WS_REMAP", ws_remap);
+        };

This closure is used to configure Cargo with path trimming and the correct workspace remap base.

The relevant call sites now look like:

-                if let Some(ref map_to) =
-                    self.sess.debuginfo_map_to(GitRepo::Rustc, RemapScheme::Compiler)
-                {
-                    // When building compiler sources, we want to apply the compiler remap scheme.
-                    let map = [
-                        // Cargo use relative paths for workspace members, so let's remap those.
-                        format!("compiler/={map_to}/compiler"),
-                        // rustc creates absolute paths (in part bc of the `rust-src` unremap
-                        // and for working directory) so let's remap the build directory as well.
-                        format!("{}={map_to}", self.sess.src.display()),
-                        // remap OUT_DIR so they don't leak into artifacts.
-                        format!("{}={map_to}/out", self.sess.out.display()),
-                        // on windows, rustc may use forward slashes internally
-                        #[cfg(windows)]
-                        format!(
-                            "{}={map_to}\\out",
-                            self.sess.out.display().to_string().replace('/', "\\")
-                        ),
-                    ]
-                    .join("\t");
-                    cargo.env("RUSTC_DEBUGINFO_MAP", map);
-                }
+                if let Some(ref map_to) = self.sess.debuginfo_map_to(RemapScheme::Compiler) {
+                    trim_paths(&mut cargo, map_to);
+                }

This replaces a large block of manual mapping with a single call to the new helper.

3. Update C/C++ debug info remap for LLVM

For LLVM builds, the remapping is now handled by a dedicated function:

+/// C/C++ debug info remap flags for LLVM build.
+///
+/// The remap is observable when LLVM is compiled with debug info,
+/// for example, with `llvm.release-debuginfo = true`.
+fn debuginfo_map_cflags(builder: &Builder<'_>, target: TargetSelection) -> Vec<String> {
+    if !builder.config.rust_remap_debuginfo {
+        return Vec::new();
+    }
+
+    let mut flags = Vec::new();
+    let map = format!("{}=/rustc/llvm", builder.src.display());
+    let cc = builder.cc_tool(target);
+    if cc.is_like_clang() || cc.is_like_gnu() {
+        flags.push(format!("-fdebug-prefix-map={map}"));
+    } else if cc.is_like_clang_cl() {
+        flags.push("-Xclang".into());
+        flags.push(format!("-fdebug-prefix-map={map}"));
+    }
+    flags
+}

This function generates the correct remap flags for various C/C++ compilers used to build LLVM.

It is then chained into the CMake configuration:

-    for flag in builder
-        .cc_handled_cflags(target, CLang::C)
-        .into_iter()
-        .chain(builder.cc_unhandled_cflags(target, GitRepo::Llvm, CLang::C))
+    for flag in builder
+        .cc_handled_cflags(target, CLang::C)
+        .into_iter()
+        .chain(builder.cc_unhandled_cflags(target, CLang::C))
+        .chain(debuginfo_map_cflags(builder, target))

4. Remove now-unused code and types

For example, the GitRepo enum and its usages are removed:

-pub(crate) enum GitRepo {
-    Rustc,
-    Llvm,
-}

This enum and its usages are no longer needed since remap logic is simplified.

From the review thread

No notable review discussion on this PR.

Takeaways

  • Prefer built-in, standardized tooling over custom scripts for build reproducibility. Using Cargo's -Ztrim-paths reduces maintenance burden and improves consistency.
  • When replacing custom logic with upstream features, ensure all edge cases and legacy conventions are preserved. This includes handling special remap paths for different artifact types.
  • Remove obsolete code aggressively when upstream solutions render it unnecessary. This reduces technical debt and the risk of subtle bugs from outdated logic.