Skip to content

cargo_build_script omits --remap-path-prefix, making build script output path-dependent #4201

Description

@cpcwood

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 (mylibrustix, myappmylib) 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageThe ticket needs maintainer attention.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions