Skip to content

rust: hash proc-macro externs by their crate metadata - #2882

Open
xdustinface wants to merge 4 commits into
mozilla:mainfrom
xdustinface:rust-proc-macro-extern-metadata
Open

xdustinface wants to merge 4 commits into
mozilla:mainfrom
xdustinface:rust-proc-macro-extern-metadata

Conversation

@xdustinface

@xdustinface xdustinface commented Oct 7, 2026 •

Copy link
Copy Markdown

On macOS the linker writes a dylib's own output path into it. sccache hashes each --extern file whole, so the same proc-macro built into two target directories (two git worktrees, two CI checkouts) hashes differently, and every crate that uses the macro misses the cache.

This hashes a dylib extern by its .rustc metadata section instead. That section doesn't depend on the output path: serde_derive built in four checkouts had four different file hashes and one metadata hash. It still changes with the macro's source and dependencies. Every other extern, and a dylib without the section, is hashed whole as before.

test_rust_cargo_proc_macro_in_another_target_dir builds a proc-macro and a crate using it into two target directories and expects the second build to hit. It fails without this change on macOS. I've only run the tests on macOS.

Related to #2652, but independent of the base-dir work there.

Comment thread src/util.rs Outdated
/// metadata section rather than as a whole file. On macOS the linker writes the
/// library's own output path into it, so the same proc-macro built into two target
/// directories would hash differently and every crate using it would miss the cache.
/// The metadata section does not depend on the output path. Everything else, and a

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you please make this comment shorter? 2 lines should be enough :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, it's two lines now.

Comment thread src/util.rs Outdated
return Ok(digest);
}
}
Digest::file(path, pool).await

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when there is no section, we read the file twice.
could we hash data directly in the closure instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. It now hashes the bytes it already read when there's no .rustc section, so each dylib is read once.

Comment thread src/util.rs

fn rustc_metadata_section(data: &[u8]) -> Option<&[u8]> {
let file = object::File::parse(data).ok()?;
let section = file.section_by_name(".rustc")?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about a proc-macro linking native code (a build.rs with cc)?
if the C code changes, .rustc stays the same and we would get a stale hit, no?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, the metadata doesn't cover native code linked into the macro. In practice that means a local proc-macro crate with a cc-built helper edited in place, since a published version never changes. I've added a line to docs/Rust.md next to the existing caveat about proc-macros that read files.

There is an exact alternative. When I build serde_derive as a dependency in two target directories, the dylibs differ only by the install name the linker takes from the output path. With -C link-arg=-Wl,-install_name,@rpath/<file> they come out byte-identical, so sccache could add that when it runs a proc-macro compile on macOS and keep hashing externs whole. The catch is that sccache would then change a compile it doesn't cache, and -Wl, assumes cc drives the linker. If you'd prefer that route anyway, I'm happy to rework the PR around it.

Comment thread src/util.rs
use std::path::Path;

#[test]
fn test_hash_all_externs_hashes_whole_files_without_rustc_metadata() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this only tests the fallback path.
please add a test where the .rustc section is used, e.g. two objects written with object::write that differ outside that section and hash the same

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test_hash_all_externs_hashes_a_dylib_by_its_metadata: two dylibs with the same .rustc section and different code hash the same, and a changed section hashes differently. It uses object's write feature as a dev-dependency.

@codecov-commenter

codecov-commenter commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.31973% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 74.63%. Comparing base (eb55704) to head (9c891e5).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
tests/sccache_cargo.rs 98.57% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2882      +/-   ##
==========================================
- Coverage   77.58%   74.63%   -2.96%     
==========================================
  Files          72       72              
  Lines       41409    41519     +110     
==========================================
- Hits        32126    30986    -1140     
- Misses       9283    10533    +1250     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@xdustinface
xdustinface force-pushed the rust-proc-macro-extern-metadata branch from 03d842f to 9c891e5 Compare October 8, 2026 01:36
@xdustinface
xdustinface requested a review from sylvestre October 9, 2026 02:16
The proc-macro dylib a crate links against was hashed as a whole file. On macOS the linker writes the dylib's own output path into it, so the same proc-macro built into two target directories hashed differently and every crate using it missed the cache. Hash a dylib extern by its `.rustc` metadata section instead, which does not depend on the output path.
- Shorten the doc comment on `hash_all_externs` to two lines.
- Read a dylib once: hash its `.rustc` section, or the bytes already read when there is none.
- Test the metadata path with two `object::write` objects that differ outside `.rustc` (adds `object`'s `write` feature as a dev-dependency).
- Document in docs/Rust.md that native code linked into a proc-macro is not part of its key.
@xdustinface
xdustinface force-pushed the rust-proc-macro-extern-metadata branch from 3560c2e to 3f63368 Compare October 11, 2026 01:11
blaise-pov added a commit to blaise-pov/sccache that referenced this pull request Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants