Collapse three Cargo workspaces into one, defusing the double-serde link - #3
Merged
Merged
Conversation
…erde link
//server:server_bin linked TWO distinct serde rlibs. corex/, server/ and
combos/ were three independent Cargo workspaces with three Cargo.lock files,
so crate_universe built three separate repos, and rules_rust gives every target
its own codegen metadata id (rustc.bzl: --codegen=metadata=-%s), which means two
Bazel targets built from the same source can never unify. Six crates were
doubled that way: serde, serde_derive, syn, quote, proc-macro2, unicode-ident.
The consequence was not theoretical. Any corex type crossing into server hit
error[E0277]: the trait bound `User: Serialize` is not satisfied
note: there are multiple different versions of crate `serde`
and it was unfixable from server/BUILD.bazel. It compiled under cargo only
because cargo could not see corex at all -- the two build systems disagreed by
construction. The repo's own guide had already hit this and written it off as
"you might encounter version conflicts".
The lockfiles had also already drifted: proc-macro2 was 1.0.96 in corex and
1.0.97 in server, syn 2.0.104 vs 2.0.105. Nothing could detect it.
What changed
- New root Cargo.toml: one [workspace], resolver 3, four members, with
[workspace.package] supplying version/edition/rust-version so members
inherit one source of truth.
- Deleted combos/Cargo.toml. Its nested [workspace] table would make
cargo-bazel's splicer fail outright ("manifests are not allowed to be from
different workspaces", splicer.rs) once combos members joined the root
workspace.
- Deleted all three Cargo.lock files; one root lock, 110 packages.
- server/Cargo.toml gains `corex = { path = "../corex" }`, so cargo sees the
same edge Bazel does. all_crate_deps() never emits workspace members, so
"//corex:corex_lib" stays hand-written in BUILD.bazel.
- MODULE.bazel: three crate.from_cargo calls -> one, named `crates`.
The four BUILD files load from @crates.
- .bazelignore: one root target/ instead of five.
- CI: cargo now runs once at the root instead of three times, and the doc-test
step no longer needs its "does a lib target exist" guard, because
--workspace --doc only visits members that have one.
Proof, not assertion
server/src/main.rs gains a real handler:
async fn get_shared_user(Path(name): Path<String>) -> Json<corex::User>
That is precisely the line that could not compile under Bazel before, and it
now builds and is covered by a regression test. Verified in the graph:
deps(//server:server_bin) contains exactly one serde -> crates__serde-1.0.229
server_bin deps: axum, corex, serde, tokio
duplicate crates in rust-project.json: syn only, at 2.0.119 and 3.0.3 --
genuinely different majors, which is correct, not a duplication
Dependency versions moved
Regenerating from scratch re-resolved within the existing semver ranges:
axum 0.8.4 -> 0.8.9, serde 1.0.219 -> 1.0.229, tokio 1.47.1 -> 1.53.1,
matchit 0.8.4 -> 0.8.6. No declared constraint changed. Reverse any of them
with `cargo update -p <crate> --precise <version>` if a pin is wanted.
Docs
BAZEL_RUST_GUIDE.md told readers to give every new crate its own Cargo.lock
and its own crate.from_cargo repo -- the exact policy that produced this bug,
scaling quadratically in first-party crates sharing a proc-macro dep. Rewritten
to the single-workspace recipe. Its path-dependency ban is corrected too: path
deps only break crate_universe when the manifest is not listed in
`manifests`, and banning them is what forced the two graphs apart.
Verified: cargo fmt/clippy -D warnings/test/doc all green at the workspace root;
bazel build, test (4/4) and --config=lint all green.
NOTE: bcr.bazel.build is unreachable from this sandbox, so Bazel ran against a
local BCR mirror via a gitignored .bazelrc.user. CI resolves from the real BCR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D3MwkT9fzAzZweK4birHYz
This was referenced Aug 2, 2026
codeitlikemiley
marked this pull request as ready for review
August 2, 2026 03:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
//server:server_binlinked two distinctserderlibs. This is the fix, and it is proven by a handler that could not have compiled before.The bug
corex/,server/andcombos/were three independent Cargo workspaces with threeCargo.lockfiles, so crate_universe built three separate repos. rules_rust gives every target its own codegen metadata id (--codegen=metadata=-%s), so two Bazel targets built from the same source can never unify. Six crates were doubled:serde,serde_derive,syn,quote,proc-macro2,unicode-ident.Any
corextype crossing intoserverhit:Unfixable from
server/BUILD.bazel. It compiled under cargo only because cargo could not seecorexat all — the two build systems disagreed by construction. The repo's own guide had already hit this and written it off as "you might encounter version conflicts."The lockfiles had also already drifted, undetectably:
proc-macro21.0.96 vs 1.0.97,syn2.0.104 vs 2.0.105.Proof, not assertion
server/src/main.rsgains a real handler:That is exactly the line that failed before. It now builds, and a regression test covers it. From the actual graph:
The only crate still appearing twice is
syn, at 2.0.119 and 3.0.3 — genuinely different majors, which is correct resolution, not duplication.What changed
Cargo.toml: one[workspace],resolver = "3", four members, with[workspace.package]supplying version/edition/rust-version so members inherit one source of truth.combos/Cargo.toml. Its nested[workspace]would make cargo-bazel's splicer fail outright (manifests are not allowed to be from different workspaces) once combos members joined the root workspace.Cargo.lockfiles; one root lock, 110 packages.server/Cargo.tomlgainscorex = { path = "../corex" }so cargo sees the same edge Bazel does.all_crate_deps()never emits workspace members, so//corex:corex_libstays hand-written.MODULE.bazel: threecrate.from_cargocalls → one, namedcrates..bazelignore: one roottarget/instead of five.--workspace --doconly visits members that have one.Dependency versions moved
Regenerating from scratch re-resolved within the existing semver ranges — no declared constraint changed:
Reverse any with
cargo update -p <crate> --precise <version>if you'd rather pin.Docs
BAZEL_RUST_GUIDE.mdtold readers to give every new crate its ownCargo.lockand its owncrate.from_cargorepo — the exact policy that produced this bug, scaling quadratically in first-party crates sharing a proc-macro dependency. Following it after this PR would have recreated the defect. Rewritten to the single-workspace recipe.Its path-dependency ban is corrected too: path deps only break crate_universe when the manifest isn't listed in
manifests. Banning them outright is what forced the two graphs apart in the first place.Verification
cargo fmt --all --checkcargo clippy --workspace --all-targets --locked -D warningscargo test --workspace --all-targets --lockedcargo test --workspace --doc --lockedbazel build --config=ci //...bazel test --config=ci //...bazel build --config=ci --config=lint //...bcr.bazel.buildis unreachable from the sandbox this was developed in, so Bazel ran against a local BCR mirror behind a gitignored.bazelrc.user. CI resolves from the real BCR, so its run is the authoritative check.Sequencing
This should land before the
MODULE.bazel.lockwork: it changes the module graph, so any lock generated beforehand would immediately need regenerating.Generated by Claude Code