Skip to content

fix: canonicalize manifest labels in lockfile digests#3901

Open
brianduff wants to merge 1 commit intobazelbuild:mainfrom
brianduff:bduff/rules-rust-lockfile-digest
Open

fix: canonicalize manifest labels in lockfile digests#3901
brianduff wants to merge 1 commit intobazelbuild:mainfrom
brianduff:bduff/rules-rust-lockfile-digest

Conversation

@brianduff
Copy link

@brianduff brianduff commented Mar 16, 2026

Problem & Solution Overview

cargo-bazel-lock.json digests currently include SplicingMetadata.manifests, which is keyed by full Bazel labels.
That makes the digest sensitive to whether the same Cargo.toml is evaluated in the root module context or as a dependency module under bzlmod.

Concretely, the same manifest can appear as:

  • //impl/rust:Cargo.toml
  • @@published_ruleset+//impl/rust:Cargo.toml

Those labels serialize differently even when the manifest contents and resulting Cargo graph are identical, so a checked-in cargo-bazel-lock.json can validate in the producer repo and then fail downstream with Digests do not match when that repo is consumed as a dependency.

This change canonicalizes manifest labels only for lockfile hashing:

  • preserve package and target
  • drop repository identity
  • leave the runtime splicing/rendering path unchanged

To avoid introducing a new hard failure mode, the digest path now serializes canonicalized manifest entries in a collision-tolerant way:

  • the common case still hashes like a plain manifest map entry
  • if multiple manifest labels collapse to the same repo-neutral label, their manifest payloads are serialized as a stable sorted list under that key

That keeps root-vs-dependency lockfiles portable without panicking or erroring when canonicalization removes repository-name distinctions.

This PR also repins the checked-in example lockfiles whose checksums change under the new digest algorithm.

Testing Done

  • bazel test //crate_universe:unit_test --test_output=errors
  • Added a unit test showing that //:Cargo.toml and @@published_ruleset+//:Cargo.toml produce the same digest
  • Added a unit test showing that canonicalization collisions remain digest-stable across insertion order
  • Repinned the affected example lockfiles under examples/crate_universe
  • Reran bazel run //vendor_external:crates_vendor --show_progress_rate_limit=5 --curses=yes --color=yes --terminal_columns=143 --show_timestamps --verbose_failures --jobs=30 --announce_rc --experimental_repository_cache_hardlinks --disk_cache= --sandbox_tmpfs_path=/tmp in examples/crate_universe and confirmed it reaches Analyzed target //vendor_external:crates_vendor / Build completed successfully instead of failing on a stale digest

I also validated the behavior with a minimal producer/consumer repro outside this repo:

Producer MODULE.bazel:

module(
    name = "lockfile_repro",
    version = "0.0.1",
)

bazel_dep(name = "rules_rust", version = "0.68.1")

rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(edition = "2021")
use_repo(rust, "rust_toolchains")
register_toolchains("@rust_toolchains//:all")

crate = use_extension("@rules_rust//crate_universe:extensions.bzl", "crate")
crate.from_cargo(
    name = "crates",
    cargo_lockfile = "//:Cargo.lock",
    lockfile = "//:cargo-bazel-lock.json",
    manifests = ["//:Cargo.toml"],
)
use_repo(crate, "crates")

Consumer MODULE.bazel:

module(
    name = "lockfile_repro_consumer",
    version = "0.0.1",
)

bazel_dep(name = "rules_rust", version = "0.68.1")
bazel_dep(name = "lockfile_repro", version = "0.0.1")

local_path_override(
    module_name = "lockfile_repro",
    path = "../producer",
)

rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(edition = "2021")
use_repo(rust, "rust_toolchains")
register_toolchains("@rust_toolchains//:all")

Repro steps:

  1. In the producer, run CARGO_BAZEL_REPIN=1 bazel build //:repro_lib
  2. In the consumer, run bazel build @lockfile_repro//:repro_lib

Before this patch, step 2 failed because the digest changed under @@lockfile_repro+//.
With this patch, both builds succeed.

Notes for Reviewers

The main tradeoff here is that repository identity is intentionally removed from the digest input for manifest labels.
That is deliberate because repository identity changes between root and dependency contexts, while the Cargo inputs stay the same.
If maintainers think repository identity should remain semantically relevant here, then the long-term fix likely needs a different repo-neutral manifest identity instead of raw Bazel labels.

Ignore repository names when hashing splicing metadata so cargo-bazel lockfiles remain portable between root and dependency contexts. Reject canonicalization collisions explicitly and cover both behaviors with unit tests.
@brianduff brianduff force-pushed the bduff/rules-rust-lockfile-digest branch from f2932f0 to 1b3fded Compare March 16, 2026 17:29
@brianduff brianduff marked this pull request as ready for review March 16, 2026 18:42
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.

1 participant