Skip to content

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

Open
@RedBreadcat

Description

@RedBreadcat

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]

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-proc-macroArea: compiling proc-macrosA-rebuild-detectionArea: rebuild detection and fingerprintingC-bugCategory: bugS-triageStatus: This issue is waiting on initial triage.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions