Reindeer computes its public-target set from the first-order dependencies of all workspace members, but resolves the renames that name those targets from the root package only. In a virtual workspace (no root package) renames are therefore ignored entirely. This leaves no native way to resolve a real collision: when two semver-incompatible majors of one crate are both public, reindeer emits two bare alias rules with the same name, which buck2 rejects as a duplicate target.
In a virtual workspace, suppose member A depends on parquet 57 and member B on parquet 58. Both are first-order deps of a workspace member, so reindeer marks both public and emits:
alias(name = "parquet", actual = ":parquet-57")
alias(name = "parquet", actual = ":parquet-58")
This fails with a double-registration error. The versioned library targets (parquet-57, parquet-58) are disambiguated correctly by CollisionInfo; only the bare public aliases collide.
The existing escape hatches don't work here; Cargo dependency rename (parquet57 = { package = "parquet" }) — this is the intended mechanism (it flows through dep_renamed → public_rule_name, and CollisionInfo::new_with_reserved even reserves the renamed alias name). But dep_renamed is built solely from root_pkg.dependencies, with the comment:
▎ // Only the root package's renames matter. We don't attempt to merge different rename choices made by different workspace members.
- A virtual manifest has no root package, so the map is empty and the rename is silently dropped. Declaring the rename in a member has no effect.
- visibility fixup — only tunes the bare alias; the versioned rust_library is hardcoded to Visibility::Private (non-root, non-split), so there's no way to expose :parquet-57 directly instead.
- reindeer.toml — no field to rename/remap/suppress an alias or pick a winner among versions.
The public-set computation iterates every workspace member ("first-order dependencies of any workspace member"), so reindeer will happily make a non-root member's dependency public — but then refuses to let that same member rename it, because rename resolution is root-only. The rename map feeds the naming of targets that are themselves derived from all members; sourcing it from a different (narrower) set than the thing it names is the root of the bug.
In the canonical single-third-party-manifest setup there is a root package, so this never surfaces. But virtual workspaces with member-level third-party deps are a supported configuration everywhere else in the code, and this is the one place that assumes a root package.
Candidate fixes (looking for direction before I send a PR)
- Source renames from all workspace members, consistent with the public-set logic. This needs a defined merge policy, since the same dependency can be referenced by multiple members — e.g. a declared rename takes precedence over the bare name, identical renames agree, and two distinct renames for the same target are a hard error rather than an order-dependent silent pick. (Today, with root-only, conflicts are impossible, so this introduces a new — but loud — error class only for the newly-supported case.)
- A backward-compatible variant: behave exactly as today when a root package exists, and only consult member renames when there is no root. This keeps the change invisible to every current root-package user and confines the new behavior + error to the virtual case that's broken today, at the cost of two code paths.
- An explicit config surface instead of overloading Cargo renames e.g. a per-(package, version) alias-name setting in reindeer.toml or a fixup, decoupling "what's the public alias name" from Cargo's rename mechanism entirely.
I have a working implementation of (1) (with the merge policy above and unit tests) and can adapt it to (2) easily. Before I open a PR, which direction would you prefer — or is there a reason the root-only restriction is intentional that I'm missing?
Reindeer computes its public-target set from the first-order dependencies of all workspace members, but resolves the renames that name those targets from the root package only. In a virtual workspace (no root package) renames are therefore ignored entirely. This leaves no native way to resolve a real collision: when two semver-incompatible majors of one crate are both public, reindeer emits two bare alias rules with the same name, which buck2 rejects as a duplicate target.
In a virtual workspace, suppose member A depends on parquet 57 and member B on parquet 58. Both are first-order deps of a workspace member, so reindeer marks both public and emits:
alias(name = "parquet", actual = ":parquet-57")
alias(name = "parquet", actual = ":parquet-58")
This fails with a double-registration error. The versioned library targets (parquet-57, parquet-58) are disambiguated correctly by CollisionInfo; only the bare public aliases collide.
The existing escape hatches don't work here; Cargo dependency rename (parquet57 = { package = "parquet" }) — this is the intended mechanism (it flows through dep_renamed → public_rule_name, and CollisionInfo::new_with_reserved even reserves the renamed alias name). But dep_renamed is built solely from root_pkg.dependencies, with the comment:
▎ // Only the root package's renames matter. We don't attempt to merge different rename choices made by different workspace members.
The public-set computation iterates every workspace member ("first-order dependencies of any workspace member"), so reindeer will happily make a non-root member's dependency public — but then refuses to let that same member rename it, because rename resolution is root-only. The rename map feeds the naming of targets that are themselves derived from all members; sourcing it from a different (narrower) set than the thing it names is the root of the bug.
In the canonical single-third-party-manifest setup there is a root package, so this never surfaces. But virtual workspaces with member-level third-party deps are a supported configuration everywhere else in the code, and this is the one place that assumes a root package.
Candidate fixes (looking for direction before I send a PR)
I have a working implementation of (1) (with the merge policy above and unit tests) and can adapt it to (2) easily. Before I open a PR, which direction would you prefer — or is there a reason the root-only restriction is intentional that I'm missing?