Skip to content

Recompilation across crates in workspace when there is a proc-macro dependency #15382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
RedBreadcat opened this issue Apr 3, 2025 · 3 comments
Labels
A-proc-macro Area: compiling proc-macros A-rebuild-detection Area: rebuild detection and fingerprinting C-bug Category: bug S-triage Status: This issue is waiting on initial triage.

Comments

@RedBreadcat
Copy link

Problem

Hi,
I am attempting to get "hot reloading" working for a project, however a shared dependency that seemingly does not need to be recompiled is being recompiled. My project makes use of Bevy ECS, and the shared dependency is my "components" crate (components is an ECS term), which I do not which to be recompiled as this modifies the TypeId values of the structs contained within, confusing Bevy.
I've simplified down the project into the following hierarchy, and a link to this project can be found in the "steps" section of this issue:

  • workspace
    • game_bin
      • components
        • common
          • fnv
      • fnv
    • hot
      • components
        • common
          • fnv

In reality, "common" is bevy_reflect_derive - a proc-macro crate that is a dependency of a dependency, and thus I don't have much control over it. I've made "common" a proc-macro crate in my example.
"fnv" can be any crate as far as I'm aware, as long as it is present in both "common" and "game_bin". In reality for me it's the uuid crate, however I'm using fnv for this example as it is very simple, with no dependencies, to keep debugging easier.

If I compile game_bin and then hot, the dependencies are recompiled. Example output from cargo:

$ cargo b -p game_bin
   Compiling fnv v1.0.7
   Compiling common v0.1.0 (C:\Users\robys\git\simple\common)
   Compiling components v0.1.0 (C:\Users\robys\git\simple\components)
   Compiling game_bin v0.1.0 (C:\Users\robys\git\simple\game_bin)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
$ cargo b -p hot
   Compiling fnv v1.0.7
   Compiling common v0.1.0 (C:\Users\robys\git\simple\common)
   Compiling components v0.1.0 (C:\Users\robys\git\simple\components)
   Compiling hot v0.1.0 (C:\Users\robys\git\simple\hot)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s

If I look into the target/debug/.fingerprint directory I see duplicates of common, components and fnv. And if I run game_bin and a separate process that loads hot.dll and calls the exported function I get different TypeId values for each (I can provide a script to do this if required, but I don't think it'll be useful as we can just look at the presence of fingerprint files to determine if something was recompiled).

If I remove "fnv" from either game_bin or common, or make common no longer proc-macro, then nothing is recompiled:

$ cargo b -p game_bin
   Compiling fnv v1.0.7
   Compiling common v0.1.0 (C:\Users\robys\git\simple\common)
   Compiling components v0.1.0 (C:\Users\robys\git\simple\components)
   Compiling game_bin v0.1.0 (C:\Users\robys\git\simple\game_bin)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
$ cargo b -p hot
   Compiling hot v0.1.0 (C:\Users\robys\git\simple\hot)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s

Steps

git clone [email protected]:RedBreadcat/recompile.git
cd recompile
cargo b -p game_bin
cargo b -p hot # Note how this logs fnv, common and components being recompiled. You can also see duplicated fingerprints in target\debug\.fingerprint.

Possible Solution(s)

No response

Notes

To dig into the issue, I first set CARGO_LOG="cargo::core::compiler::fingerprint=trace". Here's what is logged during the cargo b -p hot call.

   0.011175200s DEBUG prepare_target{force=false package_id=hot v0.1.0 (C:\Users\foo\git\recompile\hot) target="hot"}: cargo::core::compiler::fingerprint: fingerprint at: C:\Users\foo\git\recompile\target\debug\.fingerprint\hot-3508d5915a918114\lib-hot
   0.011396700s DEBUG prepare_target{force=false package_id=hot v0.1.0 (C:\Users\foo\git\recompile\hot) target="hot"}: cargo::core::compiler::fingerprint: failed to get mtime of "C:\\Users\\foo\\git\\recompile\\target\\debug\\deps\\libfnv-a05d1c57e54b212a.rlib": failed to load metadata for path `C:\Users\foo\git\recompile\target\debug\deps\libfnv-a05d1c57e54b212a.rlib`
   0.011569100s DEBUG prepare_target{force=false package_id=hot v0.1.0 (C:\Users\foo\git\recompile\hot) target="hot"}: cargo::core::compiler::fingerprint: failed to get mtime of "C:\\Users\\foo\\git\\recompile\\target\\debug\\deps\\common-712e8f7a3e2ebcb6.dll": failed to load metadata for path `C:\Users\foo\git\recompile\target\debug\deps\common-712e8f7a3e2ebcb6.dll`
   0.011689700s DEBUG prepare_target{force=false package_id=hot v0.1.0 (C:\Users\foo\git\recompile\hot) target="hot"}: cargo::core::compiler::fingerprint: failed to get mtime of "C:\\Users\\foo\\git\\recompile\\target\\debug\\deps\\libcomponents-60eddae38580c867.rlib": failed to load metadata for path `C:\Users\foo\git\recompile\target\debug\deps\libcomponents-60eddae38580c867.rlib`
   0.011773000s DEBUG prepare_target{force=false package_id=hot v0.1.0 (C:\Users\foo\git\recompile\hot) target="hot"}: cargo::core::compiler::fingerprint: failed to get mtime of "C:\\Users\\foo\\git\\recompile\\target\\debug\\deps\\hot.dll": failed to load metadata for path `C:\Users\foo\git\recompile\target\debug\deps\hot.dll`
   0.011839800s  INFO prepare_target{force=false package_id=hot v0.1.0 (C:\Users\foo\git\recompile\hot) target="hot"}: cargo::core::compiler::fingerprint: fingerprint error for hot v0.1.0 (C:\Users\foo\git\recompile\hot)/Build/TargetInner { name_inferred: true, ..: lib_target("hot", ["dylib"], "C:\\Users\\foo\\git\\recompile\\hot\\src\\lib.rs", Edition2024) }
   0.011903900s  INFO prepare_target{force=false package_id=hot v0.1.0 (C:\Users\foo\git\recompile\hot) target="hot"}: cargo::core::compiler::fingerprint:     err: failed to read `C:\Users\foo\git\recompile\target\debug\.fingerprint\hot-3508d5915a918114\lib-hot`

Caused by:
    The system cannot find the file specified. (os error 2)
   0.012483300s DEBUG prepare_target{force=false package_id=components v0.1.0 (C:\Users\foo\git\recompile\components) target="components"}: cargo::core::compiler::fingerprint: fingerprint at: C:\Users\foo\git\recompile\target\debug\.fingerprint\components-60eddae38580c867\lib-components
   0.012614200s  INFO prepare_target{force=false package_id=components v0.1.0 (C:\Users\foo\git\recompile\components) target="components"}: cargo::core::compiler::fingerprint: fingerprint error for components v0.1.0 (C:\Users\foo\git\recompile\components)/Build/TargetInner { name_inferred: true, ..: lib_target("components", ["lib"], "C:\\Users\\foo\\git\\recompile\\components\\src\\lib.rs", Edition2024) }
   0.012707500s  INFO prepare_target{force=false package_id=components v0.1.0 (C:\Users\foo\git\recompile\components) target="components"}: cargo::core::compiler::fingerprint:     err: failed to read `C:\Users\foo\git\recompile\target\debug\.fingerprint\components-60eddae38580c867\lib-components`

Caused by:
    The system cannot find the file specified. (os error 2)
   0.013026600s DEBUG prepare_target{force=false package_id=common v0.1.0 (C:\Users\foo\git\recompile\common) target="common"}: cargo::core::compiler::fingerprint: fingerprint at: C:\Users\foo\git\recompile\target\debug\.fingerprint\common-712e8f7a3e2ebcb6\lib-common
   0.013123300s  INFO prepare_target{force=false package_id=common v0.1.0 (C:\Users\foo\git\recompile\common) target="common"}: cargo::core::compiler::fingerprint: fingerprint error for common v0.1.0 (C:\Users\foo\git\recompile\common)/Build/TargetInner { name_inferred: true, for_host: true, proc_macro: true, ..: lib_target("common", ["proc-macro"], "C:\\Users\\foo\\git\\recompile\\common\\src\\lib.rs", Edition2024) }
   0.013181000s  INFO prepare_target{force=false package_id=common v0.1.0 (C:\Users\foo\git\recompile\common) target="common"}: cargo::core::compiler::fingerprint:     err: failed to read `C:\Users\foo\git\recompile\target\debug\.fingerprint\common-712e8f7a3e2ebcb6\lib-common`

Caused by:
    The system cannot find the file specified. (os error 2)
   0.013479400s DEBUG prepare_target{force=false package_id=fnv v1.0.7 target="fnv"}: cargo::core::compiler::fingerprint: fingerprint at: C:\Users\foo\git\recompile\target\debug\.fingerprint\fnv-a05d1c57e54b212a\lib-fnv
   0.013589100s  INFO prepare_target{force=false package_id=fnv v1.0.7 target="fnv"}: cargo::core::compiler::fingerprint: fingerprint error for fnv v1.0.7/Build/TargetInner { ..: lib_target("fnv", ["lib"], "C:\\Users\\foo\\.cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\fnv-1.0.7\\lib.rs", Edition2015) }
   0.013658000s  INFO prepare_target{force=false package_id=fnv v1.0.7 target="fnv"}: cargo::core::compiler::fingerprint:     err: failed to read `C:\Users\foo\git\recompile\target\debug\.fingerprint\fnv-a05d1c57e54b212a\lib-fnv`

Caused by:
    The system cannot find the file specified. (os error 2)
   Compiling fnv v1.0.7
   0.057486600s DEBUG cargo::core::compiler::fingerprint: write fingerprint (35b8201a9faf7669) : C:\Users\foo\git\recompile\target\debug\.fingerprint\fnv-a05d1c57e54b212a\lib-fnv
   Compiling common v0.1.0 (C:\Users\foo\git\recompile\common)
   0.146906400s DEBUG cargo::core::compiler::fingerprint: write fingerprint (b8df32d7c3a96588) : C:\Users\foo\git\recompile\target\debug\.fingerprint\common-712e8f7a3e2ebcb6\lib-common
   Compiling components v0.1.0 (C:\Users\foo\git\recompile\components)
   0.200354300s DEBUG cargo::core::compiler::fingerprint: write fingerprint (b52de2fb640ef34f) : C:\Users\foo\git\recompile\target\debug\.fingerprint\components-60eddae38580c867\lib-components
   Compiling hot v0.1.0 (C:\Users\foo\git\recompile\hot)
   0.376939800s DEBUG cargo::core::compiler::fingerprint: write fingerprint (5de95d45e59f1d48) : C:\Users\foo\git\recompile\target\debug\.fingerprint\hot-3508d5915a918114\lib-hot
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s

I expected to see a fingerprint "DirtyReason" message - and in fact I did see a UnitDependencyInfoChanged message previously in a larger not-as-simplified example project, but not any longer. Perhaps that means it was a red herring?

Regardless, I attempted to see why different fingerprints were created. I'm not at all familiar with cargo's code, but I hacked in some code to print the various parts that make up the fingerprint.
It all came down to the unit.profile component of profile_hash:

let profile_hash = util::hash_u64((

And breaking it down further, the only change was to debuginfo for "fnv". For game_bin it's Resolved(Full), and for hot it's Resolved(None). If I remove proc-macro=true from common, then both are Resolved(Full).
Furthermore, it seems that the addition of proc-macro=true causes common to be Resolved(None). I don't believe this causes the issue as it's the same for both game_bin and hot, but it's notable how it's different from the removed proc-macro version.

game_bin, with proc-macro = true
fnv: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: false, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
common: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(None), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
components: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
game_bin: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }

hot, with proc-macro = true
fnv: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(None), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: false, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
common: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(None), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
components: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
hot: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }

Both game_bin and hot, with proc-macro = false
fnv: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: false, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
common: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
components: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }
game_bin: Profile { name: "dev", opt_level: "0", root: Debug, lto: Bool(false), codegen_backend: None, codegen_units: None, debuginfo: Resolved(Full), split_debuginfo: None, debug_assertions: true, overflow_checks: true, rpath: false, incremental: true, panic: Unwind, strip: Deferred(None), rustflags: [], trim_paths: None }

That's as far as I've looked. I'm not sure what determines these profiles, or whether there's a way to work around this.
I am aware that if I run cargo b -p game_bin -p hot then everything is compiled together, but for my original purposes game_bin will be executing at the time I run that command, as I'm only interested in recompiling "hot" for the purposes of hot reloading.

Version

cargo 1.85.0 (d73d2caf9 2024-12-31)
release: 1.85.0
commit-hash: d73d2caf9e41a39daf2a8d6ce60ec80bf354d2a7
commit-date: 2024-12-31
host: x86_64-pc-windows-msvc
libgit2: 1.8.1 (sys:0.19.0 vendored)
libcurl: 8.9.0-DEV (sys:0.4.74+curl-8.9.0 vendored ssl:Schannel)
os: Windows 10.0.26100 (Windows 11 Core) [64-bit]
@RedBreadcat RedBreadcat added C-bug Category: bug S-triage Status: This issue is waiting on initial triage. labels Apr 3, 2025
@epage
Copy link
Contributor

epage commented Apr 10, 2025

game_bin requires fnv be built for both the host (proc macro) and the target (application) while hot only requires fnv be built for the host (proc macro).

I'm assuming Cargo has custom profile settings for host dependencies to build them faster. However, I believe Cargo also tries to detect when those special custom profile settings prevent sharing a build between host and target. So in one run, cargo is unifying them, causing one set of debug info to be used. Then in the next run, cargo is only build the host version which has different debug info.

Looking at the table, it seems that cargo should generate different copies on disk of fnv for each version of debuginfo, so on initial build, hot will need to rebuild fnv but not on subsequent builds.

You could workaround this by having hot depend on fnv. I'm assuming though there are other reasons you aren't doing that.

@epage epage added A-rebuild-detection Area: rebuild detection and fingerprinting A-proc-macro Area: compiling proc-macros labels Apr 10, 2025
@RedBreadcat
Copy link
Author

Thank you for the insight.

Looking at the table, it seems that cargo should generate different copies on disk of fnv for each version of debuginfo, so on initial build, hot will need to rebuild fnv but not on subsequent builds.

This appears to be the case. However, it's not helpful to this "hot reload" situation, as there are still two components subcrates (including across subsequent recompiles), meaning types within the have different TypeIds which causes bevy to not work.

You could workaround this by having hot depend on fnv. I'm assuming though there are other reasons you aren't doing that.

Correct, the reason I'm not doing this is that fnv in this example represents potentially multiple different crates of specific versions and feature sets across dependencies of dependencies. Keeping an up-to-date list of them in hot doesn't appeal to me.

Looking into this further, although in my initial post I said cargo b -p game_bin -p hot was not a solution for me as game_bin would be executing at the time I run the command, I've actually found that it works well - as long as I ensure I don't modify any dependencies of game_bin to trigger its recompilation. As long as I only modify the contents of hot, only hot is compiled (Obviously in hindsight, but I was concerned that cargo would check regardless and I'd get an error).

@weihanglo
Copy link
Member

If I understand correctly, adding this to either Cargo.toml or .cargo/config.toml should unify their profiles, and fnv will not recompile:

[profile.dev.build-override]
debug = true

That change was made in #11252 to heuristically skip debuginfo generation if a crate is not shared between runtime and build dependencies.

See https://doc.rust-lang.org/nightly/cargo/reference/profiles.html#build-dependencies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-proc-macro Area: compiling proc-macros A-rebuild-detection Area: rebuild detection and fingerprinting C-bug Category: bug S-triage Status: This issue is waiting on initial triage.
Projects
None yet
Development

No branches or pull requests

3 participants