feat: support trim paths with clang-cl.exe - #1799
Merged
Merged
Conversation
MSVC-family compilers were skipped, including `clang-cl.exe`, which actually accepts Clang driver options through `/clang:<arg>`. This enables the existing remap flags for clang-cl via `/clang:<arg>`. The behavior remains unchanged for `cl.exe`. Do note this only remaps fields covered by Clang's prefix-map options. It does not mean every absolute path disappears from Windows artifacts. * A debug `.obj` can still contain its absolute output name in CodeView `S_OBJNAME` somehow, and the recorded clang command line still contains the old side of the remap pairs. * A separate PDB still contains linker module/object paths. Cargo also says `object` does not sanitize debug info stored separately on Windows MSVC. <https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#profile-trim-paths-option> * The `.exe` stores the PDB filename and GUID. Two builds can have no checkout path in the executable and still not be byte-identical when the PDB GUID changes. <https://llvm.org/docs/PDB/PdbStream.html#matching-a-pdb-to-its-executable> So far as I tested it, the final `.exe` no longer exposes the checkout path, though it does not make the Windows artifacts byte-reproducible. ### How to review Commit by commit. * The first one lets the test shim record nested MSVC flag probes. * The second makes the existing remap test data work with the Windows path-list separator. * The third captures the current `cl.exe` and `clang-cl.exe` behavior. * The last one is the feature commit that probes and forwards the Clang flags for `clang-cl.exe`. ### How to test I used the LLVM tools installed with Visual Studio: ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" $hostTriple = rustc +nightly --print host-tuple $llvmArch = switch -Regex ($hostTriple) { '^aarch64-' { 'ARM64'; break } '^(x86_64|i686)-' { 'x64'; break } default { throw "unsupported host: $hostTriple" } } $clangCl = & $vswhere -latest -products * -find "**\Llvm\$llvmArch\bin\clang-cl.exe" | Select-Object -First 1 if (!$clangCl) { throw "clang-cl.exe was not found" } & $clangCl --version $env:CC = $clangCl cargo +nightly -Zscript .\trim-paths-repro.rs ``` Tested with clang-cl 19.1.5. <details><summary>repro script</summary> <p> ```rust #!/usr/bin/env -S cargo +nightly -Zscript --- [package] edition = "2024" --- //! End-to-end repro: cargo `-Ztrim-paths` -> cc -> clang-cl. //! //! Put this under the cc-rs repo root and run it with `CC` set to clang-cl. //! //! - `__FILE__` carrier: absolute `.file()` + `debug = false` //! -> trimmed by `-fmacro-prefix-map` (scopes `macro`, `object`, `all`) //! - CodeView carrier: relative `.file()` + `debug = true` //! (debug info embeds the source path) //! -> trimmed by `-fdebug-prefix-map` (scopes `object`, `all`); //! `macro` must leave it EMBEDDED, mirroring rustc scope semantics. use std::path::{Path, PathBuf}; use std::process::Command; use std::{env, fs}; fn main() { let cc_root = env::current_dir().unwrap(); assert!( fs::read_to_string("Cargo.toml") .unwrap_or_default() .contains("name = \"cc\""), "run this from the cc-rs repo root" ); let work = env::temp_dir().join(format!("cc-trim-e2e-{}", std::process::id())); let _ = fs::remove_dir_all(&work); fs::create_dir_all(&work).unwrap(); // Keep the normal drive-letter spelling. `canonicalize()` produces a // `\\?\` path on Windows, while Cargo's remap source uses `C:\...`. // Prefix-map matching is textual. let abs = work.display().to_string(); eprintln!("work directory: {}", work.display()); let mut ok = true; for debug in [false, true] { eprintln!("== {} carrier ==", if debug { "CodeView" } else { "__FILE__" }); for scope in ["all", "object", "macro", "none"] { // `none` trims nothing; `macro` does not touch debug info. let expect = scope == "none" || (debug && scope == "macro"); let object = build(&cc_root, &work, debug, scope); let found = if debug { codeview_filenames_contain(&object, &abs) } else { contains(&fs::read(object).unwrap(), abs.as_bytes()) }; let emb = |b| if b { "EMBEDDED" } else { "absent" }; eprintln!( "trim-paths = {scope:8}: absolute path {:8} (expected {})", emb(found), emb(expect) ); if found != expect { ok = false; } } } if !ok { eprintln!( "\nFAIL (does this cc checkout support clang-cl trim-paths,\n\ and does this cargo set CARGO_TRIM_PATHS_* for build scripts?)" ); eprintln!("build files retained under {}", work.display()); std::process::exit(1); } let _ = fs::remove_dir_all(&work); eprintln!("\nOK: cc inherited cargo's trim-paths remap end to end"); } /// Scaffold a package /// /// * `build.rs` compiles `hello.c` through this cc checkout /// * `cargo build` /// * return the compiled object path fn build(cc_root: &Path, work: &Path, debug: bool, scope: &str) -> PathBuf { let pkg = work.join(format!("pkg-{}-{scope}", u8::from(debug))); fs::create_dir_all(pkg.join("src")).unwrap(); // Absolute `.file()` makes `__FILE__` carry the absolute path; a // relative one leaves only CodeView carrying an absolute path. let src = if debug { PathBuf::from("src/hello.c") } else { pkg.join("src/hello.c") }; fs::write( pkg.join("Cargo.toml"), format!( r#"cargo-features = ["trim-paths"] [package] name = "trim-repro" edition = "2021" [build-dependencies] cc = {{ path = '{}' }} [profile.dev] trim-paths = "{scope}" debug = {debug} "#, cc_root.display() ), ) .unwrap(); fs::write( pkg.join("build.rs"), format!( "fn main() {{ cc::Build::new().file(r\"{}\").compile(\"hello\"); }}\n", src.display() ), ) .unwrap(); fs::write(pkg.join("src/lib.rs"), "").unwrap(); fs::write( pkg.join("src/hello.c"), "const char *embedded = __FILE__;\nconst char *get(void) { return embedded; }\n", ) .unwrap(); let target_dir = pkg.join("target"); let out = Command::new("cargo") .args(["+nightly", "build", "-Ztrim-paths", "--target-dir"]) .arg(&target_dir) .current_dir(&pkg) .output() .unwrap(); assert!( out.status.success(), "cargo build failed:\n{}", String::from_utf8_lossy(&out.stderr) ); fs::read_dir(target_dir.join("debug/build")) .unwrap() .find_map(|e| { fs::read_dir(e.ok()?.path().join("out")) .ok()? .find_map(|e| { let object = e.ok()?.path(); object .file_name()? .to_string_lossy() .ends_with("-hello.o") .then_some(object) }) }) .expect("no hello object produced") } fn codeview_filenames_contain(object: &Path, needle: &str) -> bool { let mut readobj = PathBuf::from(env::var_os("CC").expect("CC must name clang-cl")); readobj.set_file_name("llvm-readobj.exe"); let out = Command::new(readobj) .arg("--codeview") .arg(object) .output() .unwrap(); assert!( out.status.success(), "llvm-readobj failed:\n{}", String::from_utf8_lossy(&out.stderr) ); String::from_utf8_lossy(&out.stdout) .lines() .filter(|line| line.trim_start().starts_with("Filename:")) .any(|line| line.contains(needle)) } fn contains(hay: &[u8], needle: &[u8]) -> bool { hay.windows(needle.len()).any(|w| w == needle) } ``` </p> </details>
NobodyXu
approved these changes
Jul 21, 2026
NobodyXu
left a comment
Contributor
There was a problem hiding this comment.
Thank you, LGTM! Next release scheduled for this Friday
Member
Author
|
Thank you for the review and the release! 🫶🏾 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A follow-up of #1794
MSVC-family compilers were skipped before this, including
clang-cl.exe,which actually accepts Clang driver options through
/clang:<arg>.This PR enables the existing remap flags for clang-cl via
/clang:<arg>.The behavior remains unchanged for
cl.exe.Do note this only remaps fields covered by Clang's prefix-map options.
It does not mean every absolute path disappears from Windows artifacts.
.objcan still contain its absolute output name in CodeViewS_OBJNAMEsomehow, and the recorded clang command line still containsthe old side of the remap pairs.
Cargo also says
objectdoes not sanitize debug info stored separately on Windows MSVC.https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#profile-trim-paths-option
.exestores the PDB filename and GUID.Two builds can have no checkout path in the executable
and still not be byte-identical when the PDB GUID changes.
https://llvm.org/docs/PDB/PdbStream.html#matching-a-pdb-to-its-executable
So far as I tested it,
the final
.exeno longer exposes the checkout path,though it does not make the Windows artifacts byte-reproducible.
How to review
Commit by commit.
cl.exeandclang-cl.exebehavior.clang-cl.exe.How to test
I used the LLVM tools installed with Visual Studio:
Tested with clang-cl 19.1.5.
repro script