Skip to content

Commit a932fab

Browse files
committed
Support intra-workspace dependencies via all_crate_deps
`crates_universe` drops dependency edges between crates of the same Cargo workspace, leaving users to restate them by hand in BUILD files while Cargo.toml already describes them. Track those edges instead of discarding them, and render them into their own `_FIRST_PARTY_*` maps that `all_crate_deps(first_party = True)` opts into. The choice is per target at load time rather than per repository at generation time, so enabling it needs no repin and no configuration. The Bazel repository holding the workspace's crates is derived from the rule's existing `cargo_lockfile` label, so there is no new user-facing attribute. That label stays out of the lockfile digest for the same reason `label_injection_mapping` does: canonical repository names are consumer-specific, and hashing one would demand a producer-side repin that a read-only bzlmod cache cannot perform. Third-party output is unaffected. Rendering the test fixtures before and after this change produces a diff of pure additions: the new `first_party` parameter, its docstring, and six empty map literals for workspaces without intra-workspace dependencies.
1 parent 7c229c2 commit a932fab

33 files changed

Lines changed: 972 additions & 21 deletions

.bazelci/presubmit.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,32 @@ tasks:
652652
test_targets:
653653
- "//..."
654654

655+
# --- intra_workspace_dependencies ---
656+
cu_integ_intra_workspace_dependencies_ubuntu2204:
657+
name: Crate Universe - intra_workspace_dependencies
658+
platform: ubuntu2204
659+
working_directory: crate_universe/tests/integration/intra_workspace_dependencies
660+
build_targets:
661+
- "//..."
662+
test_targets:
663+
- "//..."
664+
cu_integ_intra_workspace_dependencies_macos:
665+
name: Crate Universe - intra_workspace_dependencies
666+
platform: macos_arm64
667+
working_directory: crate_universe/tests/integration/intra_workspace_dependencies
668+
build_targets:
669+
- "//..."
670+
test_targets:
671+
- "//..."
672+
cu_integ_intra_workspace_dependencies_windows:
673+
name: Crate Universe - intra_workspace_dependencies
674+
platform: windows
675+
working_directory: crate_universe/tests/integration/intra_workspace_dependencies
676+
build_targets:
677+
- "//..."
678+
test_targets:
679+
- "//..."
680+
655681
# --- multi_package (linux + macos only, curl-sys Windows issues) ---
656682
cu_integ_multi_package_ubuntu2204:
657683
name: Crate Universe - multi_package

crate_universe/extensions.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,9 @@ def _generate_hub_and_spokes(
591591
workspace_name = cfg.name,
592592
generate_binaries = cfg.generate_binaries,
593593
render_config = render_config,
594+
# The hub repository's `crates.bzl` lives outside the module being
595+
# generated for, so first-party labels need an explicit repository.
596+
cargo_lockfile_label = str(cfg.cargo_lockfile) if cfg.cargo_lockfile else None,
594597
repository_ctx = module_ctx,
595598
),
596599
)

crate_universe/private/crates_vendor.bzl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ def _write_config_file(ctx):
264264
output_pkg = _get_output_package(ctx),
265265
workspace_name = workspace_name,
266266
render_config = dict(json.decode(ctx.attr.render_config)) if ctx.attr.render_config else None,
267+
# Vendored output is committed into the workspace that owns the Cargo
268+
# workspace, so first-party labels stay repository-relative.
269+
cargo_lockfile_label = "//{}:{}".format(
270+
ctx.attr.cargo_lockfile.package,
271+
ctx.attr.cargo_lockfile.name,
272+
) if ctx.attr.cargo_lockfile else None,
267273
),
268274
)
269275

@@ -285,6 +291,7 @@ def generate_config_file(
285291
output_pkg,
286292
workspace_name,
287293
render_config,
294+
cargo_lockfile_label = None,
288295
repository_ctx = None):
289296
"""Writes the rendering config to cargo-bazel-config.json.
290297
@@ -301,6 +308,9 @@ def generate_config_file(
301308
output_pkg: The path to the package containing the build files.
302309
workspace_name (str): The name of the workspace.
303310
render_config: The render config to use.
311+
cargo_lockfile_label (str, optional): The label of the `cargo_lockfile`. Its repository
312+
is where `all_crate_deps(first_party = True)` looks for the Cargo workspace's own
313+
crates.
304314
repository_ctx (repository_ctx, optional): A repository context object
305315
used for enabling certain functionality.
306316
@@ -378,6 +388,7 @@ def generate_config_file(
378388
render_config = render_config,
379389
supported_platform_triples = supported_platform_triples,
380390
repository_name = repository_name or ctx.label.name,
391+
cargo_lockfile_label = cargo_lockfile_label,
381392
repository_ctx = repository_ctx,
382393
)
383394

crate_universe/private/generate_utils.bzl

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,15 @@ def _read_cargo_config(repository_ctx):
218218
return repository_ctx.read(config)
219219
return None
220220

221-
def _update_render_config(config, repository_name):
222-
"""Add the repository name to the render config
221+
def _update_render_config(config, repository_name, cargo_lockfile_label):
222+
"""Add rendering details that come from the rule rather than the user
223223
224224
Args:
225225
config (dict): A `render_config` struct
226226
repository_name (str): The name of the repository that owns the config
227+
cargo_lockfile_label (str): The label of the rule's `cargo_lockfile`, or None. Its
228+
repository is what `all_crate_deps(first_party = True)` renders labels against,
229+
since that is the Bazel repository holding the Cargo workspace's own crates.
227230
228231
Returns:
229232
struct: An updated `render_config`.
@@ -232,6 +235,9 @@ def _update_render_config(config, repository_name):
232235
# Add the repository name as it's very relevant to rendering.
233236
config.update({"repository_name": repository_name})
234237

238+
if cargo_lockfile_label:
239+
config.update({"cargo_lockfile_label": cargo_lockfile_label})
240+
235241
return struct(**config)
236242

237243
def _get_render_config(repository_ctx):
@@ -256,6 +262,7 @@ def compile_config(
256262
render_config,
257263
supported_platform_triples,
258264
repository_name,
265+
cargo_lockfile_label = None,
259266
repository_ctx = None):
260267
"""Create a config file for generating crate targets
261268
@@ -271,6 +278,9 @@ def compile_config(
271278
render_config (dict): The deserialized dict of the `render_config` function.
272279
supported_platform_triples (list): A list of platform triples
273280
repository_name (str): The name of the repository being generated
281+
cargo_lockfile_label (str, optional): The label of the rule's `cargo_lockfile`. Used to
282+
locate the Bazel repository that owns the Cargo workspace's own crates when
283+
rendering `all_crate_deps(first_party = True)`.
274284
repository_ctx (repository_ctx, optional): A repository context object used for enabling
275285
certain functionality.
276286
@@ -310,6 +320,7 @@ def compile_config(
310320
rendering = _update_render_config(
311321
config = render_config,
312322
repository_name = repository_name,
323+
cargo_lockfile_label = cargo_lockfile_label,
313324
),
314325
supported_platform_triples = supported_platform_triples,
315326
)
@@ -335,6 +346,9 @@ def generate_config(repository_ctx):
335346
render_config = _get_render_config(repository_ctx),
336347
supported_platform_triples = repository_ctx.attr.supported_platform_triples,
337348
repository_name = repository_ctx.name,
349+
# The hub repository's `crates.bzl` lives outside the workspace being
350+
# generated for, so first-party labels need an explicit repository.
351+
cargo_lockfile_label = str(repository_ctx.attr.cargo_lockfile),
338352
repository_ctx = repository_ctx,
339353
)
340354

crate_universe/src/api/lockfile.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ mod test {
170170
got_pkg_a.normal_deps().values(),
171171
vec![
172172
CrateDependency {
173+
workspace_member: false,
173174
id: CrateId {
174175
name: String::from("anyhow"),
175176
version: Version::new(1, 0, 69),
@@ -179,6 +180,7 @@ mod test {
179180
local_path: None,
180181
},
181182
CrateDependency {
183+
workspace_member: false,
182184
id: CrateId {
183185
name: String::from("reqwest"),
184186
version: Version::new(0, 11, 14),

crate_universe/src/config.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,21 @@ pub(crate) struct RenderConfig {
132132
/// continue to write subpackage `BUILD.bazel`s into the hub repo directly.
133133
#[serde(default)]
134134
pub(crate) crates_vendor_synthesizes_subpackages: bool,
135+
136+
/// Internal: the label of the rule's `cargo_lockfile`. Only its repository
137+
/// is used, to locate the Bazel repository holding the Cargo workspace's
138+
/// own crates so `all_crate_deps(first_party = True)` can emit labels for
139+
/// them. Injected by the rules, never set by users — a workspace member's
140+
/// Bazel package is already known (`Context::workspace_members`); the
141+
/// repository is the one piece the renderer cannot infer.
142+
///
143+
/// Excluded from the digest by [`crate::lockfile::Digest::new`], mirroring
144+
/// `label_injection_mapping`: canonical repository names are consumer-
145+
/// specific, so hashing this would make a root-level
146+
/// `single_version_override` demand a producer-side repin that a
147+
/// registry-distributed lockfile in a read-only cache can never perform.
148+
#[serde(default, skip_serializing_if = "Option::is_none")]
149+
pub(crate) cargo_lockfile_label: Option<Label>,
135150
}
136151

137152
// Default is manually implemented so that the default values match the default
@@ -156,6 +171,7 @@ impl Default for RenderConfig {
156171
generate_rules_license_metadata: default_generate_rules_license_metadata(),
157172
incompatible_no_root_alias_targets: false,
158173
crates_vendor_synthesizes_subpackages: false,
174+
cargo_lockfile_label: Option::default(),
159175
}
160176
}
161177
}

crate_universe/src/context.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,16 @@ impl Context {
127127
})
128128
.collect::<Result<BTreeMap<CrateId, String>>>()?;
129129

130+
// Workspace members are deliberately absent here: no repository is
131+
// generated for a crate that lives in the Cargo workspace, so listing
132+
// one as a direct dependency would only add noise to the lockfile.
130133
let add_crate_ids = |crates: &mut BTreeSet<CrateId>,
131134
deps: &Select<BTreeSet<Dependency>>| {
132-
for dep in deps.values() {
135+
for dep in deps
136+
.values()
137+
.into_iter()
138+
.filter(|dep| !dep.workspace_member)
139+
{
133140
crates.insert(CrateId::from(
134141
&annotations.metadata.packages[&dep.package_id],
135142
));

crate_universe/src/context/crate_context.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use crate::metadata::{
1212
CrateAnnotation, Dependency, PairedExtras, SourceAnnotation, TreeResolverMetadata,
1313
};
1414
use crate::select::Select;
15-
use crate::utils::sanitize_module_name;
1615
use crate::utils::starlark::{Glob, Label};
16+
use crate::utils::{is_false, sanitize_module_name};
1717

1818
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1919
pub struct CrateDependency {
@@ -32,6 +32,12 @@ pub struct CrateDependency {
3232
/// `[dependencies]` table and the `[patches]` table so they can be used in rendering.
3333
#[serde(default, skip_serializing_if = "Option::is_none")]
3434
pub(crate) local_path: Option<Utf8PathBuf>,
35+
36+
/// Whether the dependency is another member of the same Cargo workspace. The
37+
/// rendered dependency maps keep these separate from third-party crates so
38+
/// `all_crate_deps(first_party = True)` can opt into them.
39+
#[serde(default, skip_serializing_if = "is_false")]
40+
pub workspace_member: bool,
3541
}
3642

3743
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
@@ -409,6 +415,7 @@ impl CrateContext {
409415
id: CrateId::new(pkg.name.clone(), pkg.version.clone()),
410416
target,
411417
alias: dep.alias,
418+
workspace_member: dep.workspace_member,
412419
local_path: match source_annotations.get(&dep.package_id) {
413420
Some(SourceAnnotation::Path { path }) => Some(path.clone()),
414421
_ => None,

crate_universe/src/context/platforms.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ mod test {
132132
let mut deps: Select<BTreeSet<CrateDependency>> = Select::default();
133133
deps.insert(
134134
CrateDependency {
135+
workspace_member: false,
135136
id: CrateId::new("mock_crate_b".to_owned(), VERSION_ZERO_ONE_ZERO),
136137
target: "mock_crate_b".to_owned(),
137138
alias: None,
@@ -191,6 +192,7 @@ mod test {
191192
let mut deps: Select<BTreeSet<CrateDependency>> = Select::default();
192193
deps.insert(
193194
CrateDependency {
195+
workspace_member: false,
194196
id: CrateId::new("mock_crate_b".to_owned(), VERSION_ZERO_ONE_ZERO),
195197
target: "mock_crate_b".to_owned(),
196198
alias: None,
@@ -278,6 +280,7 @@ mod test {
278280
let mut deps: Select<BTreeSet<CrateDependency>> = Select::default();
279281
deps.insert(
280282
CrateDependency {
283+
workspace_member: false,
281284
id: CrateId::new("mock_crate_b".to_owned(), VERSION_ZERO_ONE_ZERO),
282285
target: "mock_crate_b".to_owned(),
283286
alias: None,
@@ -345,6 +348,7 @@ mod test {
345348
let mut deps: Select<BTreeSet<CrateDependency>> = Select::default();
346349
deps.insert(
347350
CrateDependency {
351+
workspace_member: false,
348352
id: CrateId::new("mock_crate_b".to_owned(), VERSION_ZERO_ONE_ZERO),
349353
target: "mock_crate_b".to_owned(),
350354
alias: None,

crate_universe/src/lockfile.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use regex::Regex;
1313
use serde::{Deserialize, Serialize};
1414
use sha2::{Digest as Sha2Digest, Sha256};
1515

16-
use crate::config::Config;
16+
use crate::config::{Config, RenderConfig};
1717
use crate::context::Context;
1818
use crate::metadata::Cargo;
1919
use crate::splicing::{SplicingManifest, SplicingMetadata};
@@ -78,9 +78,15 @@ impl Digest {
7878
// `single_version_override` would shift the canonical names, change
7979
// the digest, and force a producer-side repin to recover — which is
8080
// impossible for registry-distributed producers whose lockfile lives
81-
// in a read-only bzlmod cache.
81+
// in a read-only bzlmod cache. `rendering.cargo_lockfile_label` carries
82+
// a consumer-side canonical repository name too, so it is cleared for
83+
// exactly the same reason.
8284
let config_for_hash = Config {
8385
label_injection_mapping: Default::default(),
86+
rendering: RenderConfig {
87+
cargo_lockfile_label: None,
88+
..config.rendering.clone()
89+
},
8490
..config.clone()
8591
};
8692

0 commit comments

Comments
 (0)