From e02f8bd2a627a1a43c2c2846897d6c8c95cf99e4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 01:37:20 +0000 Subject: [PATCH 01/18] chore(deps): update actions/checkout action to v6.1.0 --- .github/workflows/audit.yml | 2 +- .github/workflows/contrib.yml | 2 +- .github/workflows/main.yml | 30 +++++++++++++++--------------- .github/workflows/release.yml | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index e56aa597e27..1811834f766 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -21,7 +21,7 @@ jobs: - advisories - bans licenses sources steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 # Prevent sudden announcement of a new advisory from failing ci: continue-on-error: ${{ matrix.checks == 'advisories' }} diff --git a/.github/workflows/contrib.yml b/.github/workflows/contrib.yml index bd591b69305..2b0cd86c8e0 100644 --- a/.github/workflows/contrib.yml +++ b/.github/workflows/contrib.yml @@ -18,7 +18,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Install mdbook run: | mkdir mdbook diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bf01e6868bf..abd809eaa58 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,7 +58,7 @@ jobs: rustfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update stable && rustup default stable - run: rustup component add rustfmt - run: cargo fmt --all --check @@ -79,7 +79,7 @@ jobs: env: CARGO_BUILD_WARNINGS: deny steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update stable && rustup default stable - run: rustup component add clippy - run: cargo clippy --workspace --all-targets --no-deps --keep-going @@ -87,14 +87,14 @@ jobs: stale-label: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update stable && rustup default stable - run: cargo stale-label lint-docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update stable && rustup default stable - run: cargo lint-docs --check @@ -102,7 +102,7 @@ jobs: lockfile: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update stable && rustup default stable - run: cargo update -p cargo --locked @@ -113,7 +113,7 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha != '' && github.event.pull_request.head.sha || github.sha }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 0 - run: rustup update stable && rustup default stable @@ -193,7 +193,7 @@ jobs: other: i686-pc-windows-gnu name: Tests ${{ matrix.name }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Dump Environment run: ci/dump-environment.sh # Some tests require stable. Make sure it is set to the most recent stable @@ -274,21 +274,21 @@ jobs: schema: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update stable && rustup default stable - run: cargo test -p cargo-util-schemas -F unstable-schema resolver: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update stable && rustup default stable - run: cargo test -p resolver-tests test_gitoxide: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update --no-self-update stable && rustup default stable - run: rustup target add i686-unknown-linux-gnu - run: rustup target add wasm32-unknown-unknown @@ -301,7 +301,7 @@ jobs: build_std: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update nightly && rustup default nightly - run: rustup component add rust-src - run: cargo build @@ -311,7 +311,7 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update nightly && rustup default nightly - run: rustup update stable - run: rustup component add rust-docs @@ -339,7 +339,7 @@ jobs: msrv: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - uses: taiki-e/install-action@16b05812d776ae1dfaabc8277e421fb6d2506419 # v2.82.7 with: tool: cargo-hack @@ -350,7 +350,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Actions Repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Spell Check Repo uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0 @@ -358,7 +358,7 @@ jobs: name: Timing HTML report runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - run: rustup update nightly && rustup default nightly - run: cargo build - name: Generate timing report for rustfix diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 19d1e6c72b1..76ffc8483e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout the source code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - name: Publish Cargo to crates.io run: ./publish.py From 46ab602015d63d32f8d689be88ae4d479a1069b4 Mon Sep 17 00:00:00 2001 From: Gaurav Kamathe Date: Sat, 1 Aug 2026 13:34:26 +0530 Subject: [PATCH 02/18] Remove unnecessary return at end of functions --- src/ops/cargo_test.rs | 2 +- src/ops/resolve.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ops/cargo_test.rs b/src/ops/cargo_test.rs index 94d9d18494c..ec9f71619e0 100644 --- a/src/ops/cargo_test.rs +++ b/src/ops/cargo_test.rs @@ -306,7 +306,7 @@ fn display_no_run_information( .verbose(|shell| shell.status("Executable", &cmd))?; } - return Ok(()); + Ok(()) } /// Creates a [`ProcessBuilder`] for executing a single test. diff --git a/src/ops/resolve.rs b/src/ops/resolve.rs index 105761243ac..0702aa71d93 100644 --- a/src/ops/resolve.rs +++ b/src/ops/resolve.rs @@ -908,7 +908,7 @@ fn emit_warnings_of_unused_patches( ws.gctx().shell().print_report(&warnings, false)?; } - return Ok(()); + Ok(()) } /// Informs `registry` and `version_pref` that `[patch]` entries are available From 8d0c67af22429a04e55cd53f8fa465e821d96b92 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 1 Aug 2026 11:22:07 +0200 Subject: [PATCH 03/18] make __CARGO_TEST_FORCE_ARGFILE available in distributed builds --- crates/cargo-util/src/process_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cargo-util/src/process_builder.rs b/crates/cargo-util/src/process_builder.rs index 2b9a735d865..2b929cd0b24 100644 --- a/crates/cargo-util/src/process_builder.rs +++ b/crates/cargo-util/src/process_builder.rs @@ -561,7 +561,7 @@ impl ProcessBuilder { /// /// You should set `__CARGO_TEST_FORCE_ARGFILE` to enable this. fn debug_force_argfile(retry_enabled: bool) -> bool { - cfg!(debug_assertions) && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok() && retry_enabled + retry_enabled && env::var("__CARGO_TEST_FORCE_ARGFILE").is_ok() } /// Creates new pipes for stderr, stdout, and optionally stdin. From 34a4d22e59702be564cfb3c2100bc146e747e4ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:29:51 +0000 Subject: [PATCH 04/18] chore(deps): update embarkstudios/cargo-deny-action action to v2.1.1 --- .github/workflows/audit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 1811834f766..e59e12feb29 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -22,7 +22,7 @@ jobs: - bans licenses sources steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 - - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20 + - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1 # Prevent sudden announcement of a new advisory from failing ci: continue-on-error: ${{ matrix.checks == 'advisories' }} with: From a3e60bfb4252228eb663783a6631481f9ca81e74 Mon Sep 17 00:00:00 2001 From: Gaurav Kamathe Date: Sat, 1 Aug 2026 18:29:53 +0530 Subject: [PATCH 05/18] Remove unnecessary to_path_buf --- src/compiler/layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/layout.rs b/src/compiler/layout.rs index ef1f208f92c..7c638ab04f9 100644 --- a/src/compiler/layout.rs +++ b/src/compiler/layout.rs @@ -466,7 +466,7 @@ impl BuildDirLayout { if self.is_new_layout { self.build_unit(pkg_dir).join("fingerprint") } else { - self.legacy_fingerprint().to_path_buf().join(pkg_dir) + self.legacy_fingerprint().join(pkg_dir) } } /// Fetch the fingerprint path. (old layout) From ae4012a1f0ece2e0228212dcfd7b4170c40e1b9c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 17:43:00 +0000 Subject: [PATCH 06/18] chore(deps): update cargo-semver-checks to v0.50.0 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index abd809eaa58..ba9afd3c377 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -120,7 +120,7 @@ jobs: - name: Install cargo-semver-checks run: | mkdir installed-bins - curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.49.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \ + curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.50.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \ | tar -xz --directory=./installed-bins echo `pwd`/installed-bins >> $GITHUB_PATH - run: ci/validate-version-bump.sh From f6e9c9084f101c7c72516a4be7a3b7d18535935a Mon Sep 17 00:00:00 2001 From: Raushan kumar Date: Thu, 30 Jul 2026 16:34:00 +0000 Subject: [PATCH 07/18] test(build-script): cover empty `package.build` --- tests/testsuite/build_script.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/testsuite/build_script.rs b/tests/testsuite/build_script.rs index 16a2ce3db4e..5e199adec05 100644 --- a/tests/testsuite/build_script.rs +++ b/tests/testsuite/build_script.rs @@ -1422,6 +1422,34 @@ Caused by: .run(); } +#[cargo_test] +fn build_script_empty() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2024" + build = "" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" + +thread 'main'[..]panicked at src/workspace/parser/targets.rs:[..] +called `Option::unwrap()` on a `None` value +[NOTE] run with `RUST_BACKTRACE=1` environment variable to display a backtrace + +"#]]) + .run(); +} + #[cargo_test] fn links_duplicates() { // this tests that the links_duplicates are caught at resolver time From 65f2ce9bf3e6076a36aaf3c22437fa98fb7ddea5 Mon Sep 17 00:00:00 2001 From: Raushan kumar Date: Tue, 28 Jul 2026 13:36:11 +0000 Subject: [PATCH 08/18] fix(manifest): prevent panic on empty `package.build` --- src/workspace/parser/targets.rs | 22 +++++++++++++++++++--- tests/testsuite/build_script.rs | 6 +++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/workspace/parser/targets.rs b/src/workspace/parser/targets.rs index af553fca1e0..5b56d7de56c 100644 --- a/src/workspace/parser/targets.rs +++ b/src/workspace/parser/targets.rs @@ -112,7 +112,7 @@ pub(super) fn to_targets( script_path .file_stem() .and_then(|s| s.to_str()) - .unwrap_or("") + .expect("previously normalized") ); targets.push(Target::custom_build_target( &name, @@ -906,7 +906,10 @@ fn validate_unique_names(targets: &[TomlTarget], target_kind: &str) -> CargoResu fn validate_unique_build_scripts(scripts: &[String]) -> CargoResult<()> { let mut seen = HashMap::default(); for script in scripts { - let stem = Path::new(script).file_stem().unwrap().to_str().unwrap(); + let stem = Path::new(script) + .file_stem() + .and_then(|s| s.to_str()) + .expect("previously normalized"); seen.entry(stem) .or_insert_with(Vec::new) .push(script.as_str()); @@ -1109,6 +1112,7 @@ pub fn normalize_build( // Explicitly no build script. Some(TomlPackageBuild::Auto(false)) => Ok(build.cloned()), Some(TomlPackageBuild::SingleScript(build_file)) => { + validate_build_name(build_file)?; let build_file = paths::normalize_path(Path::new(build_file)); let build = build_file.into_os_string().into_string().expect( "`build_file` started as a String and `normalize_path` shouldn't have changed that", @@ -1118,8 +1122,20 @@ pub fn normalize_build( Some(TomlPackageBuild::Auto(true)) => { Ok(Some(TomlPackageBuild::SingleScript(BUILD_RS.to_owned()))) } - Some(TomlPackageBuild::MultipleScript(_scripts)) => Ok(build.cloned()), + Some(TomlPackageBuild::MultipleScript(scripts)) => { + for script in scripts { + validate_build_name(script)?; + } + Ok(build.cloned()) + } + } +} + +fn validate_build_name(build: &str) -> CargoResult<()> { + if Path::new(build).file_stem().is_none() { + anyhow::bail!("invalid `package.build` file name"); } + Ok(()) } fn name_or_panic(target: &TomlTarget) -> &str { diff --git a/tests/testsuite/build_script.rs b/tests/testsuite/build_script.rs index 5e199adec05..5689b8c0a57 100644 --- a/tests/testsuite/build_script.rs +++ b/tests/testsuite/build_script.rs @@ -1441,10 +1441,10 @@ fn build_script_empty() { p.cargo("check") .with_status(101) .with_stderr_data(str![[r#" +[ERROR] failed to parse manifest at `[ROOT]/foo/Cargo.toml` -thread 'main'[..]panicked at src/workspace/parser/targets.rs:[..] -called `Option::unwrap()` on a `None` value -[NOTE] run with `RUST_BACKTRACE=1` environment variable to display a backtrace +Caused by: + invalid `package.build` file name "#]]) .run(); From e3dfce8cf4d42a8aa6a47f7ca984f2dacfb75a0c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 2 Aug 2026 19:11:37 -0400 Subject: [PATCH 09/18] Avoid parsing unchanged lockfiles --- src/ops/lockfile.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ops/lockfile.rs b/src/ops/lockfile.rs index 439dd8fd7d9..6a27714d086 100644 --- a/src/ops/lockfile.rs +++ b/src/ops/lockfile.rs @@ -208,6 +208,11 @@ fn serialize_resolve(resolve: &Resolve, orig: Option<&str>) -> String { #[tracing::instrument(skip_all)] fn are_equal_lockfiles(orig: &str, current: &str, ws: &Workspace<'_>) -> bool { + // Avoid deserializing lockfiles when their contents already match. + if orig.lines().eq(current.lines()) { + return true; + } + // If we want to try and avoid updating the lock file, parse both and // compare them; since this is somewhat expensive, don't do it in the // common case where we can update lock files. @@ -222,7 +227,7 @@ fn are_equal_lockfiles(orig: &str, current: &str, ws: &Workspace<'_>) -> bool { } } - orig.lines().eq(current.lines()) + false } fn emit_package(dep: &toml::Table, out: &mut String) { From 936fb9239f18abe513221d7750a4fd67069b8223 Mon Sep 17 00:00:00 2001 From: Adam Gemmell Date: Thu, 30 Jul 2026 14:54:39 +0100 Subject: [PATCH 10/18] refactor: move the sysroot path to the GlobalContext --- src/compiler/build_context/target_info.rs | 3 --- src/context/mod.rs | 11 +++++++++++ src/util/rustc.rs | 12 ++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/compiler/build_context/target_info.rs b/src/compiler/build_context/target_info.rs index c4c1bb6b64e..db7611db3f4 100644 --- a/src/compiler/build_context/target_info.rs +++ b/src/compiler/build_context/target_info.rs @@ -53,8 +53,6 @@ pub struct TargetInfo { pub supports_std: Option, /// Supported values for `-Csplit-debuginfo=` flag, queried from rustc support_split_debuginfo: Vec, - /// Path to the sysroot. - pub sysroot: PathBuf, /// Path to the "lib" directory in the sysroot which rustc uses for linking /// target libraries. pub sysroot_target_libdir: PathBuf, @@ -352,7 +350,6 @@ impl TargetInfo { return Ok(TargetInfo { crate_type_process, crate_types: RefCell::new(map), - sysroot, sysroot_target_libdir, rustflags: rustflags.into(), rustdocflags: extra_args( diff --git a/src/context/mod.rs b/src/context/mod.rs index 00131cfba97..5fa784be27c 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -225,6 +225,8 @@ pub struct GlobalContext { cargo_exe: OnceLock, /// The location of the rustdoc executable rustdoc: OnceLock, + /// The path to the sysroot + sysroot: OnceLock, /// Whether we are printing extra verbose messages extra_verbose: bool, /// `frozen` is the same as `locked`, but additionally will not access the @@ -379,6 +381,7 @@ impl GlobalContext { cli_config: None, cargo_exe: Default::default(), rustdoc: Default::default(), + sysroot: Default::default(), extra_verbose: false, frozen: false, locked: false, @@ -610,6 +613,14 @@ impl GlobalContext { .map(AsRef::as_ref) } + /// Get the sysroot path. + pub fn get_sysroot<'gctx>(&'gctx self, ws: Option<&Workspace<'gctx>>) -> CargoResult<&PathBuf> { + self.sysroot.try_borrow_with(|| { + let rustc = self.load_global_rustc(ws)?; + rustc.sysroot(self) + }) + } + /// Which package sources have been updated, used to ensure it is only done once. pub fn updated_sources(&self) -> MutexGuard<'_, HashSet> { self.updated_sources.lock().unwrap() diff --git a/src/util/rustc.rs b/src/util/rustc.rs index 97ff06db7a9..85059835545 100644 --- a/src/util/rustc.rs +++ b/src/util/rustc.rs @@ -64,6 +64,7 @@ impl Rustc { .wrapped(wrapper.as_deref()); apply_env_config(gctx, &mut cmd)?; cmd.env(crate::CARGO_ENV, gctx.cargo_exe()?); + cmd.arg("-vV"); let verbose_version = cache.cached_output(&cmd, 0)?.0; @@ -159,6 +160,17 @@ impl Rustc { .unwrap() .cached_output(cmd, extra_fingerprint) } + + /// Use the rustc executable to fetch the sysroot path. + pub fn sysroot(&self, gctx: &GlobalContext) -> CargoResult { + let mut cmd = self.workspace_process(); + apply_env_config(gctx, &mut cmd)?; + cmd.env(crate::CARGO_ENV, gctx.cargo_exe()?); + cmd.arg("--print=sysroot"); + + let (stdout, _) = self.cached_output(&cmd, 0)?; + Ok(stdout.trim().into()) + } } /// It is a well known fact that `rustc` is not the fastest compiler in the From 9f3c3266101a06efc785432eb3dbf35da25d7c58 Mon Sep 17 00:00:00 2001 From: Adam Gemmell Date: Thu, 30 Jul 2026 16:40:33 +0100 Subject: [PATCH 11/18] refactor: update all users of the sysroot path --- src/compiler/build_context/mod.rs | 11 +++++++++++ src/compiler/build_runner/compilation_files.rs | 2 +- src/compiler/rustdoc.rs | 4 ++-- src/compiler/standard_lib.rs | 15 ++++++++------- src/compiler/trim_paths.rs | 18 +++++++++--------- src/context/mod.rs | 15 ++++++++++----- src/ops/cargo_fix/mod.rs | 5 +++-- 7 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/compiler/build_context/mod.rs b/src/compiler/build_context/mod.rs index 67292e523a1..9de8a5cf910 100644 --- a/src/compiler/build_context/mod.rs +++ b/src/compiler/build_context/mod.rs @@ -1,5 +1,7 @@ //! [`BuildContext`] is a (mostly) static information about a build task. +use std::path::Path; + use crate::compiler::BuildConfig; use crate::compiler::CompileKind; use crate::compiler::Unit; @@ -162,6 +164,15 @@ impl<'a, 'gctx> BuildContext<'a, 'gctx> { pub fn extra_args_for(&self, unit: &Unit) -> Option<&Vec> { self.extra_compiler_args.get(unit) } + + /// Gets the path to the sysroot. + /// + /// Helper function that uses GlobalContext. + pub fn get_sysroot(&self) -> &'gctx Path { + self.gctx + .get_sysroot(Some(self.ws)) + .expect("able to invoke rustc") + } } #[derive(Copy, Clone, Default, Debug)] diff --git a/src/compiler/build_runner/compilation_files.rs b/src/compiler/build_runner/compilation_files.rs index 3285455325d..e2de7212543 100644 --- a/src/compiler/build_runner/compilation_files.rs +++ b/src/compiler/build_runner/compilation_files.rs @@ -735,7 +735,7 @@ fn compute_metadata( // SourceId for stdlib crates is an absolute path inside the sysroot. // Pass the sysroot as workspace root so that we hash a relative path. // This avoids the metadata hash changing depending on where the user installed rustc. - &bcx.target_data.get_info(unit.kind).unwrap().sysroot + &bcx.get_sysroot() } else { bcx.ws.root() }; diff --git a/src/compiler/rustdoc.rs b/src/compiler/rustdoc.rs index 3337e2b37cc..3dbe4aa8171 100644 --- a/src/compiler/rustdoc.rs +++ b/src/compiler/rustdoc.rs @@ -1,8 +1,8 @@ //! Utilities for building with rustdoc. +use crate::compiler::BuildContext; use crate::compiler::build_runner::BuildRunner; use crate::compiler::unit::Unit; -use crate::compiler::{BuildContext, CompileKind}; use crate::sources::CRATES_IO_REGISTRY; use crate::util::data_structures::HashMap; use crate::util::data_structures::HashSet; @@ -208,7 +208,7 @@ pub fn add_root_urls( let std_url = match &map.std { None | Some(RustdocExternMode::Remote) => None, Some(RustdocExternMode::Local) => { - let sysroot = &build_runner.bcx.target_data.info(CompileKind::Host).sysroot; + let sysroot = build_runner.bcx.get_sysroot(); let html_root = sysroot.join("share").join("doc").join("rust").join("html"); if html_root.exists() { let url = Url::from_file_path(&html_root).map_err(|()| { diff --git a/src/compiler/standard_lib.rs b/src/compiler/standard_lib.rs index ba5e1bc12f9..85f5aef80b8 100644 --- a/src/compiler/standard_lib.rs +++ b/src/compiler/standard_lib.rs @@ -54,7 +54,7 @@ pub fn resolve_std<'gctx>( crates: &[String], kinds: &[CompileKind], ) -> CargoResult<(PackageSet<'gctx>, Resolve, ResolvedFeatures)> { - let src_path = detect_sysroot_src_path(target_data)?; + let src_path = detect_sysroot_src_path(ws)?; let std_ws_manifest_path = src_path.join("Cargo.toml"); let gctx = ws.gctx(); // TODO: Consider doing something to enforce --locked? Or to prevent the @@ -217,15 +217,16 @@ fn generate_roots( Ok(()) } -fn detect_sysroot_src_path(target_data: &RustcTargetData<'_>) -> CargoResult { - if let Some(s) = target_data.gctx.get_env_os("__CARGO_TESTS_ONLY_SRC_ROOT") { +fn detect_sysroot_src_path(ws: &Workspace<'_>) -> CargoResult { + if let Some(s) = ws.gctx().get_env_os("__CARGO_TESTS_ONLY_SRC_ROOT") { return Ok(s.into()); } // NOTE: This is temporary until we figure out how to acquire the source. - let src_path = target_data - .info(CompileKind::Host) - .sysroot + let src_path = ws + .gctx() + .get_sysroot(Some(ws)) + .expect("able to invoke rustc") .join("lib") .join("rustlib") .join("src") @@ -238,7 +239,7 @@ fn detect_sysroot_src_path(target_data: &RustcTargetData<'_>) -> CargoResult { anyhow::bail!("{} --toolchain {}", msg, rustup_toolchain); } diff --git a/src/compiler/trim_paths.rs b/src/compiler/trim_paths.rs index 227b98f0415..0864ba39ffe 100644 --- a/src/compiler/trim_paths.rs +++ b/src/compiler/trim_paths.rs @@ -85,7 +85,7 @@ pub(crate) fn trim_paths_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) [ package_remap(build_runner, unit), build_dir_remap(build_runner), - sysroot_remap(build_runner, unit), + sysroot_remap(build_runner), ] } @@ -93,16 +93,16 @@ pub(crate) fn trim_paths_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) /// /// This remap logic aligns with rustc: /// -fn sysroot_remap(build_runner: &BuildRunner<'_, '_>, unit: &Unit) -> OsString { +fn sysroot_remap(build_runner: &BuildRunner<'_, '_>) -> OsString { let mut remap = OsString::new(); remap.push({ - // See also `detect_sysroot_src_path()`. - let mut sysroot = build_runner.bcx.target_data.info(unit.kind).sysroot.clone(); - sysroot.push("lib"); - sysroot.push("rustlib"); - sysroot.push("src"); - sysroot.push("rust"); - sysroot + build_runner + .bcx + .get_sysroot() + .join("lib") + .join("rustlib") + .join("src") + .join("rust") }); remap.push("="); remap.push("/rustc/"); diff --git a/src/context/mod.rs b/src/context/mod.rs index 5fa784be27c..339dfee8f57 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -614,11 +614,16 @@ impl GlobalContext { } /// Get the sysroot path. - pub fn get_sysroot<'gctx>(&'gctx self, ws: Option<&Workspace<'gctx>>) -> CargoResult<&PathBuf> { - self.sysroot.try_borrow_with(|| { - let rustc = self.load_global_rustc(ws)?; - rustc.sysroot(self) - }) + pub fn get_sysroot<'gctx>( + &'gctx self, + ws: Option<&Workspace<'gctx>>, + ) -> CargoResult<&'gctx Path> { + self.sysroot + .try_borrow_with(|| { + let rustc = self.load_global_rustc(ws)?; + rustc.sysroot(self) + }) + .map(AsRef::as_ref) } /// Which package sources have been updated, used to ensure it is only done once. diff --git a/src/ops/cargo_fix/mod.rs b/src/ops/cargo_fix/mod.rs index 90aeb7a4b21..0d5619255cf 100644 --- a/src/ops/cargo_fix/mod.rs +++ b/src/ops/cargo_fix/mod.rs @@ -53,7 +53,6 @@ use semver::Version; use tracing::{debug, trace, warn}; pub use self::fix_edition::fix_edition; -use crate::compiler::CompileKind; use crate::compiler::RustcTargetData; use crate::ops::resolve::WorkspaceResolve; use crate::ops::{self, CompileOptions}; @@ -185,7 +184,9 @@ pub fn fix( wrapper.env(IDIOMS_ENV_INTERNAL, "1"); } - let sysroot = &target_data.info(CompileKind::Host).sysroot; + let sysroot = gctx + .get_sysroot(Some(original_ws)) + .expect("able to invoke rustc"); if sysroot.is_dir() { wrapper.env(SYSROOT_INTERNAL, sysroot); } From 4c20b84f295f780f10878f92a0e0ca9903a75ed2 Mon Sep 17 00:00:00 2001 From: Adam Gemmell Date: Mon, 3 Aug 2026 16:58:47 +0100 Subject: [PATCH 12/18] test(cfg): use the gctx sysroot when building TargetInfo The move to passing in a Rustc rather than a Workspace was necessary as a 2nd Rustc is unable to update the rustc info cache. --- src/compiler/build_context/mod.rs | 9 +++-- src/compiler/build_context/target_info.rs | 35 +++++++++---------- src/compiler/standard_lib.rs | 3 +- src/context/mod.rs | 10 ++---- src/ops/cargo_fix/mod.rs | 5 ++- src/util/rustc.rs | 8 +++-- tests/testsuite/cfg.rs | 41 +++++++++-------------- 7 files changed, 49 insertions(+), 62 deletions(-) diff --git a/src/compiler/build_context/mod.rs b/src/compiler/build_context/mod.rs index 9de8a5cf910..ce58b53d019 100644 --- a/src/compiler/build_context/mod.rs +++ b/src/compiler/build_context/mod.rs @@ -169,9 +169,12 @@ impl<'a, 'gctx> BuildContext<'a, 'gctx> { /// /// Helper function that uses GlobalContext. pub fn get_sysroot(&self) -> &'gctx Path { - self.gctx - .get_sysroot(Some(self.ws)) - .expect("able to invoke rustc") + // cfg::bad_cfg_discovery tests that these panics aren't reachable + let rustc = self + .gctx + .load_global_rustc(Some(self.ws)) + .expect("rustc load ok"); + self.gctx.get_sysroot(&rustc).expect("sysroot fetch ok") } } diff --git a/src/compiler/build_context/target_info.rs b/src/compiler/build_context/target_info.rs index db7611db3f4..fc9268e88df 100644 --- a/src/compiler/build_context/target_info.rs +++ b/src/compiler/build_context/target_info.rs @@ -163,6 +163,8 @@ impl TargetInfo { /// invocation is cached by [`Rustc::cached_output`]. /// /// Search `Tricky` to learn why querying `rustc` several times is needed. + /// + /// When a Workspace is provided, #[tracing::instrument(skip_all)] pub fn new( gctx: &GlobalContext, @@ -172,15 +174,25 @@ impl TargetInfo { ) -> CargoResult { let mut rustflags = extra_args(gctx, requested_kinds, &rustc.host, None, kind, Flags::Rust)?; + + let sysroot = gctx.get_sysroot(rustc)?; + let sysroot_target_libdir = sysroot + .join("lib") + .join("rustlib") + .join(match &kind { + CompileKind::Host => rustc.host.as_str(), + CompileKind::Target(target) => target.short_name(), + }) + .join("lib"); + let mut turn = 0; loop { let extra_fingerprint = kind.fingerprint_hash(); // Query rustc for several kinds of info from each line of output: // 0) file-names (to determine output file prefix/suffix for given crate type) - // 1) sysroot - // 2) split-debuginfo - // 3) cfg + // 1) split-debuginfo + // 2) cfg // // Search `--print` to see what we query so far. let mut process = rustc.workspace_process(); @@ -214,7 +226,6 @@ impl TargetInfo { process.arg("--crate-type").arg(crate_type.as_str()); } - process.arg("--print=sysroot"); process.arg("--print=split-debuginfo"); process.arg("--print=crate-name"); // `___` as a delimiter. process.arg("--print=cfg"); @@ -236,22 +247,6 @@ impl TargetInfo { map.insert(crate_type.clone(), out); } - let Some(line) = lines.next() else { - return error_missing_print_output("sysroot", &process, &output, &error); - }; - let sysroot = PathBuf::from(line); - let sysroot_target_libdir = { - let mut libdir = sysroot.clone(); - libdir.push("lib"); - libdir.push("rustlib"); - libdir.push(match &kind { - CompileKind::Host => rustc.host.as_str(), - CompileKind::Target(target) => target.short_name(), - }); - libdir.push("lib"); - libdir - }; - let support_split_debuginfo = { // HACK: abuse `--print=crate-name` to use `___` as a delimiter. let mut res = Vec::new(); diff --git a/src/compiler/standard_lib.rs b/src/compiler/standard_lib.rs index 85f5aef80b8..3af824e2cb0 100644 --- a/src/compiler/standard_lib.rs +++ b/src/compiler/standard_lib.rs @@ -223,9 +223,10 @@ fn detect_sysroot_src_path(ws: &Workspace<'_>) -> CargoResult { } // NOTE: This is temporary until we figure out how to acquire the source. + let rustc = ws.gctx().load_global_rustc(Some(ws))?; let src_path = ws .gctx() - .get_sysroot(Some(ws)) + .get_sysroot(&rustc) .expect("able to invoke rustc") .join("lib") .join("rustlib") diff --git a/src/context/mod.rs b/src/context/mod.rs index 339dfee8f57..b743ea47168 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -614,15 +614,9 @@ impl GlobalContext { } /// Get the sysroot path. - pub fn get_sysroot<'gctx>( - &'gctx self, - ws: Option<&Workspace<'gctx>>, - ) -> CargoResult<&'gctx Path> { + pub fn get_sysroot<'gctx>(&'gctx self, rustc: &Rustc) -> CargoResult<&'gctx Path> { self.sysroot - .try_borrow_with(|| { - let rustc = self.load_global_rustc(ws)?; - rustc.sysroot(self) - }) + .try_borrow_with(|| rustc.sysroot(self)) .map(AsRef::as_ref) } diff --git a/src/ops/cargo_fix/mod.rs b/src/ops/cargo_fix/mod.rs index 0d5619255cf..4df6c69502d 100644 --- a/src/ops/cargo_fix/mod.rs +++ b/src/ops/cargo_fix/mod.rs @@ -184,9 +184,8 @@ pub fn fix( wrapper.env(IDIOMS_ENV_INTERNAL, "1"); } - let sysroot = gctx - .get_sysroot(Some(original_ws)) - .expect("able to invoke rustc"); + let rustc = gctx.load_global_rustc(Some(original_ws))?; + let sysroot = gctx.get_sysroot(&rustc).expect("able to invoke rustc"); if sysroot.is_dir() { wrapper.env(SYSROOT_INTERNAL, sysroot); } diff --git a/src/util/rustc.rs b/src/util/rustc.rs index 85059835545..1ab62ae0134 100644 --- a/src/util/rustc.rs +++ b/src/util/rustc.rs @@ -4,7 +4,7 @@ use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; use std::sync::Mutex; -use anyhow::Context as _; +use anyhow::{Context as _, bail}; use cargo_util::{ProcessBuilder, ProcessError, paths}; use filetime::FileTime; use serde::{Deserialize, Serialize}; @@ -169,7 +169,11 @@ impl Rustc { cmd.arg("--print=sysroot"); let (stdout, _) = self.cached_output(&cmd, 0)?; - Ok(stdout.trim().into()) + let path: PathBuf = stdout.trim().into(); + if !path.exists() { + bail!("sysroot path \"{}\" does not exist", path.display()); + } + Ok(path) } } diff --git a/tests/testsuite/cfg.rs b/tests/testsuite/cfg.rs index c185e3c4a50..a850c652765 100644 --- a/tests/testsuite/cfg.rs +++ b/tests/testsuite/cfg.rs @@ -347,6 +347,13 @@ fn bad_cfg_discovery() { print!("{}", run_rustc()); return; } + if mode == "no-sysroot" { + return; + } + if std::env::args_os().any(|a| a == "--print=sysroot") { + print!("{}", run_rustc()); + return; + } if mode == "no-crate-types" { return; } @@ -356,24 +363,19 @@ fn bad_cfg_discovery() { } let output = run_rustc(); let mut lines = output.lines(); - let sysroot = loop { + let mut line = loop { let line = lines.next().unwrap(); if line.contains("___") { - println!("{}", line); + println!("{line}"); } else { break line; } }; - if mode == "no-sysroot" { - return; - } - println!("{}", sysroot); if mode == "no-split-debuginfo" { return; } loop { - let line = lines.next().unwrap(); if line == "___" { println!("\n{line}"); break; @@ -382,6 +384,7 @@ fn bad_cfg_discovery() { // concat them into one line. print!("{line},"); } + line = lines.next().unwrap(); }; if mode != "bad-cfg" { @@ -411,32 +414,22 @@ foo p.cargo("check") .env("RUSTC", &funky_rustc) - .env("FUNKY_MODE", "no-crate-types") + .env("FUNKY_MODE", "no-sysroot") .with_status(101) .with_stderr_data(str![[r#" -[ERROR] malformed output when learning about crate-type bin information -command was: `[ROOT]/compiler/target/debug/compiler[..] --crate-name ___ [..]` -(no output received) +[ERROR] sysroot path "" does not exist "#]]) .run(); p.cargo("check") .env("RUSTC", &funky_rustc) - .env("FUNKY_MODE", "no-sysroot") + .env("FUNKY_MODE", "no-crate-types") .with_status(101) .with_stderr_data(str![[r#" -[ERROR] output of --print=sysroot missing when learning about target-specific information from rustc -command was: `[ROOT]/compiler/target/debug/compiler[..]--crate-type [..]` - ---- stdout -___[EXE] -lib___.rlib -[..]___.[..] -[..]___.[..] -[..]___.[..] -[..]___.[..] - +[ERROR] malformed output when learning about crate-type bin information +command was: `[ROOT]/compiler/target/debug/compiler[..] --crate-name ___ [..]` +(no output received) "#]]) .run(); @@ -456,7 +449,6 @@ lib___.rlib [..]___.[..] [..]___.[..] [..]___.[..] -[..] "#]]) @@ -474,7 +466,6 @@ lib___.rlib [..]___.[..] [..]___.[..] [..]___.[..] -[..] [..],[..] ___ 123 From 73d6ebec181c0194383730968166394b0daf15d1 Mon Sep 17 00:00:00 2001 From: "addie.sh" Date: Mon, 3 Aug 2026 13:37:04 -0400 Subject: [PATCH 13/18] test: Add a test for workspace fields that may produce suggestions --- tests/testsuite/workspaces.rs | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/testsuite/workspaces.rs b/tests/testsuite/workspaces.rs index 1870cf1d4eb..1541cd28519 100644 --- a/tests/testsuite/workspaces.rs +++ b/tests/testsuite/workspaces.rs @@ -2377,6 +2377,42 @@ Caused by: } } +#[cargo_test] +fn ws_err_unused_similar_suggest() { + let cases: &[(&str, &str)] = &[]; + for (table, suggestion) in cases { + let key = table.trim_start_matches('[').trim_end_matches(']'); + let key = key.split_once('.').map(|p| p.0).unwrap_or(key); + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [workspace] + members = ["a"] + + {table} + "#, + ), + ) + .file("a/Cargo.toml", &basic_lib_manifest("a")) + .file("a/src/lib.rs", "") + .build(); + p.cargo("check") + .with_status(101) + .with_stderr_data(&format!( + "\ +[ERROR] failed to parse manifest at `[..]/foo/Cargo.toml` + +Caused by: + this virtual manifest specifies a `{key}` section, which is not allowed + {suggestion} +", + )) + .run(); + } +} + #[cargo_test] fn ws_warn_unused() { for (key, name) in &[ From 5211c749c9eaedb50eaaa2afb6e64cd6bd23199f Mon Sep 17 00:00:00 2001 From: "addie.sh" Date: Mon, 3 Aug 2026 13:37:59 -0400 Subject: [PATCH 14/18] fix: Add a suggestion when adding `[lints]` to a workspace to use `[workspace.lints]` instead --- src/workspace/parser/mod.rs | 9 ++++++++- tests/testsuite/workspaces.rs | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/workspace/parser/mod.rs b/src/workspace/parser/mod.rs index 6790c746a9b..e42d5274bd6 100644 --- a/src/workspace/parser/mod.rs +++ b/src/workspace/parser/mod.rs @@ -582,7 +582,14 @@ fn normalize_toml( normalized_toml.badges = original_toml.badges.clone(); } else { if let Some(field) = original_toml.requires_package().next() { - bail!("this virtual manifest specifies a `{field}` section, which is not allowed"); + let suggestion = if field == "lints" { + "\nhelp: a similar field exists: `[workspace.lints]`" + } else { + "" + }; + bail!( + "this virtual manifest specifies a `{field}` section, which is not allowed{suggestion}" + ); } } diff --git a/tests/testsuite/workspaces.rs b/tests/testsuite/workspaces.rs index 1541cd28519..f5589e2d7f8 100644 --- a/tests/testsuite/workspaces.rs +++ b/tests/testsuite/workspaces.rs @@ -2345,7 +2345,6 @@ fn ws_err_unused() { "[features]", "[target]", "[badges]", - "[lints]", ] { let key = table.trim_start_matches('[').trim_end_matches(']'); let p = project() @@ -2379,7 +2378,20 @@ Caused by: #[cargo_test] fn ws_err_unused_similar_suggest() { - let cases: &[(&str, &str)] = &[]; + let cases: &[(&str, &str)] = &[ + ( + "[lints]", + "[HELP] a similar field exists: `[workspace.lints]`", + ), + ( + "[lints.rust]", + "[HELP] a similar field exists: `[workspace.lints]`", + ), + ( + "[lints.clippy]", + "[HELP] a similar field exists: `[workspace.lints]`", + ), + ]; for (table, suggestion) in cases { let key = table.trim_start_matches('[').trim_end_matches(']'); let key = key.split_once('.').map(|p| p.0).unwrap_or(key); From f97d6b1a24a7da2c6b3b8798969a1b408581f8bf Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Jul 2026 23:55:57 -0400 Subject: [PATCH 15/18] 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 16/18] 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 f29201b8f5007c15e1465804e524757defcdf23f Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Jul 2026 23:12:19 -0400 Subject: [PATCH 17/18] 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 | 23 ++ 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, 420 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..8f610e85ad8 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,28 @@ impl<'a, 'gctx: 'a> CompilationFiles<'a, 'gctx> { .collect(); outputs.extend(sbom_files.into_iter()); } + + // Only generates unremap files for root units. + // FIXME: might need to consider artifact dependencies? + 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..07bda2274c9 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, unit)); + 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..4684e93d542 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 unhased 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 9db4a63d3719af488b29012c4edbc318e50f411c Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Jul 2026 23:15:44 -0400 Subject: [PATCH 18/18] 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)*