Description
Build scripts that invoke rustc bake the exec root into their output, because cargo_build_script does not include --remap-path-prefix in the CARGO_ENCODED_RUSTFLAGS it passes to them.
rustix's build.rs probes for compiler features by running rustc with the source on stdin and --emit=metadata. Since the source arrives on stdin, the working directory is the only path input rustc has, and it folds that into the emitted metadata. The probe writes to $OUT_DIR, a declared build-script output, which rules_rust hashes into the cache key of every dependent crate.
Current behaviour: the same source built at two different absolute paths produces different build-script output, and therefore different cache keys for the whole dependent chain.
Expected behaviour: build-script rustc invocations are path-independent, as every other rustc invocation in the ruleset already is. construct_arguments() (rust/private/rustc.bzl:928) applies --remap-path-prefix to all of them, under the comment "For determinism to help with build distribution and such". Build scripts reach rustc indirectly, via CARGO_ENCODED_RUSTFLAGS rather than construct_arguments(), so they never inherited it. rustdoc is the only other exception and is documented as such.
Reproduction steps
Stand-alone repo: https://github.com/cpcwood/bazel-rules-rust-rustix-cache-bust-demo
The CI pipeline shows the proof, but you can run it locally too:
nix develop -c ./minimal-repro.sh # no Bazel
nix develop -c ./prove.sh # four Bazel workspaces
minimal-repro.sh isolates the mechanism to rustc and two directories.
prove.sh builds a two-crate workspace (mylib → rustix, myapp → mylib) unpatched and patched, at a short and a long path, each with an explicit --output_base, because the exec root, not the workspace path, is what reaches rustc.
Additional context
Probe output ($OUT_DIR/rustix_test_can_compile):
unpatched e27190bc… (short path)
unpatched 01079d73… (long path) differ
patched 7e887e12… (both paths) identical
Actions genuinely re-executed on the second path (cacheHit: false in the execution log):
unpatched 3 Rustc actions: rustix, //:mylib, //:myapp
patched 0
The whole dependent chain re-executes, not just rustix.
Two things that keep this hidden most of the time:
- The
CargoBuildScriptRun action's own cache key is path-independent, so on a warm cache the build script is served as a hit and the divergent bytes never propagate. It only bites when the build script genuinely executes at a path that has not populated the cache. prove.sh forces that condition explicitly.
- Bazel's default output base is an MD5 of the workspace path, so a CI job that always checks out to the same path never sees it.
Impact
Intermittent cache misses on unchanged source wherever a cache is shared across environments with different checkout paths, a developer's machine and CI being the common case. When it hits, the entire dependent chain of any crate whose build script shells out to rustc rebuilds.
Not a blocker, and no upgrade is prevented. Affected users can patch it locally via single_version_override, which is what the reproduction repo does.
Bazel and rules_rust version
- Bazel 8.4.2
- rules_rust 0.73.0 (from BCR)
- rustix 1.1.4, Rust toolchain 1.97.1, edition 2021
- Reproduced on
aarch64-darwin and ubuntu-latest. Hashes are platform-specific.
PR
I've made a PR to fix this here: #4202
Description
Build scripts that invoke
rustcbake the exec root into their output, becausecargo_build_scriptdoes not include--remap-path-prefixin theCARGO_ENCODED_RUSTFLAGSit passes to them.rustix'sbuild.rsprobes for compiler features by runningrustcwith the source on stdin and--emit=metadata. Since the source arrives on stdin, the working directory is the only path inputrustchas, and it folds that into the emitted metadata. The probe writes to$OUT_DIR, a declared build-script output, whichrules_rusthashes into the cache key of every dependent crate.Current behaviour: the same source built at two different absolute paths produces different build-script output, and therefore different cache keys for the whole dependent chain.
Expected behaviour: build-script rustc invocations are path-independent, as every other rustc invocation in the ruleset already is.
construct_arguments()(rust/private/rustc.bzl:928) applies--remap-path-prefixto all of them, under the comment "For determinism to help with build distribution and such". Build scripts reachrustcindirectly, viaCARGO_ENCODED_RUSTFLAGSrather thanconstruct_arguments(), so they never inherited it.rustdocis the only other exception and is documented as such.Reproduction steps
Stand-alone repo: https://github.com/cpcwood/bazel-rules-rust-rustix-cache-bust-demo
The CI pipeline shows the proof, but you can run it locally too:
minimal-repro.shisolates the mechanism torustcand two directories.prove.shbuilds a two-crate workspace (mylib→rustix,myapp→mylib) unpatched and patched, at a short and a long path, each with an explicit--output_base, because the exec root, not the workspace path, is what reachesrustc.Additional context
Probe output (
$OUT_DIR/rustix_test_can_compile):Actions genuinely re-executed on the second path (
cacheHit: falsein the execution log):The whole dependent chain re-executes, not just
rustix.Two things that keep this hidden most of the time:
CargoBuildScriptRunaction's own cache key is path-independent, so on a warm cache the build script is served as a hit and the divergent bytes never propagate. It only bites when the build script genuinely executes at a path that has not populated the cache.prove.shforces that condition explicitly.Impact
Intermittent cache misses on unchanged source wherever a cache is shared across environments with different checkout paths, a developer's machine and CI being the common case. When it hits, the entire dependent chain of any crate whose build script shells out to
rustcrebuilds.Not a blocker, and no upgrade is prevented. Affected users can patch it locally via
single_version_override, which is what the reproduction repo does.Bazel and rules_rust version
aarch64-darwinandubuntu-latest. Hashes are platform-specific.PR
I've made a PR to fix this here: #4202