From 6c4cb4508693cc0f2411c7fd1bfada1753574780 Mon Sep 17 00:00:00 2001 From: Abraham Zukor Date: Sat, 25 Jul 2026 19:23:42 +0000 Subject: [PATCH] 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 it needs no configuration. A lockfile written before this change carries no such edges, so it must be repinned once before the flag returns anything. 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. Coverage goes in the existing `cargo_workspace` integration test rather than a new one: its `printer` crate now reaches its sibling `rng` through the flag, and `printer:unit_test` calls into that sibling at runtime, while `num_printer` keeps declaring `//printer` by hand so both styles stay exercised. Its lockfile is repinned accordingly; most of that diff is the pending `extra_deps` build script migration rather than this change. 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. --- crate_universe/extensions.bzl | 3 + crate_universe/private/crates_vendor.bzl | 11 ++ crate_universe/private/generate_utils.bzl | 18 ++- crate_universe/src/api/lockfile.rs | 2 + crate_universe/src/config.rs | 16 ++ crate_universe/src/context.rs | 9 +- crate_universe/src/context/crate_context.rs | 9 +- crate_universe/src/context/platforms.rs | 4 + crate_universe/src/lockfile.rs | 10 +- crate_universe/src/metadata/dependency.rs | 63 +++++++- crate_universe/src/rendering.rs | 139 ++++++++++++++++++ .../src/rendering/template_engine.rs | 48 ++++++ .../src/rendering/templates/module_bzl.j2 | 65 +++++++- .../templates/partials/module/deps_map.j2 | 20 ++- crate_universe/src/test.rs | 16 ++ crate_universe/src/utils.rs | 6 + .../cargo_workspace/cargo-bazel-lock.json | 56 +++---- .../cargo_workspace/printer/BUILD.bazel | 4 +- 18 files changed, 449 insertions(+), 50 deletions(-) diff --git a/crate_universe/extensions.bzl b/crate_universe/extensions.bzl index 2ec7465f8d..7327f36f55 100644 --- a/crate_universe/extensions.bzl +++ b/crate_universe/extensions.bzl @@ -591,6 +591,9 @@ def _generate_hub_and_spokes( workspace_name = cfg.name, generate_binaries = cfg.generate_binaries, render_config = render_config, + # The hub repository's `crates.bzl` lives outside the module being + # generated for, so first-party labels need an explicit repository. + cargo_lockfile_label = str(cfg.cargo_lockfile) if cfg.cargo_lockfile else None, repository_ctx = module_ctx, ), ) diff --git a/crate_universe/private/crates_vendor.bzl b/crate_universe/private/crates_vendor.bzl index d562c482a6..bd632a68a4 100644 --- a/crate_universe/private/crates_vendor.bzl +++ b/crate_universe/private/crates_vendor.bzl @@ -264,6 +264,12 @@ def _write_config_file(ctx): output_pkg = _get_output_package(ctx), workspace_name = workspace_name, render_config = dict(json.decode(ctx.attr.render_config)) if ctx.attr.render_config else None, + # Vendored output is committed into the workspace that owns the Cargo + # workspace, so first-party labels stay repository-relative. + cargo_lockfile_label = "//{}:{}".format( + ctx.attr.cargo_lockfile.package, + ctx.attr.cargo_lockfile.name, + ) if ctx.attr.cargo_lockfile else None, ), ) @@ -285,6 +291,7 @@ def generate_config_file( output_pkg, workspace_name, render_config, + cargo_lockfile_label = None, repository_ctx = None): """Writes the rendering config to cargo-bazel-config.json. @@ -301,6 +308,9 @@ def generate_config_file( output_pkg: The path to the package containing the build files. workspace_name (str): The name of the workspace. render_config: The render config to use. + cargo_lockfile_label (str, optional): The label of the `cargo_lockfile`. Its repository + is where `all_crate_deps(first_party = True)` looks for the Cargo workspace's own + crates. repository_ctx (repository_ctx, optional): A repository context object used for enabling certain functionality. @@ -378,6 +388,7 @@ def generate_config_file( render_config = render_config, supported_platform_triples = supported_platform_triples, repository_name = repository_name or ctx.label.name, + cargo_lockfile_label = cargo_lockfile_label, repository_ctx = repository_ctx, ) diff --git a/crate_universe/private/generate_utils.bzl b/crate_universe/private/generate_utils.bzl index 8e7e46fc95..9d00eac282 100644 --- a/crate_universe/private/generate_utils.bzl +++ b/crate_universe/private/generate_utils.bzl @@ -218,12 +218,15 @@ def _read_cargo_config(repository_ctx): return repository_ctx.read(config) return None -def _update_render_config(config, repository_name): - """Add the repository name to the render config +def _update_render_config(config, repository_name, cargo_lockfile_label): + """Add rendering details that come from the rule rather than the user Args: config (dict): A `render_config` struct repository_name (str): The name of the repository that owns the config + cargo_lockfile_label (str): The label of the rule's `cargo_lockfile`, or None. Its + repository is what `all_crate_deps(first_party = True)` renders labels against, + since that is the Bazel repository holding the Cargo workspace's own crates. Returns: struct: An updated `render_config`. @@ -232,6 +235,9 @@ def _update_render_config(config, repository_name): # Add the repository name as it's very relevant to rendering. config.update({"repository_name": repository_name}) + if cargo_lockfile_label: + config.update({"cargo_lockfile_label": cargo_lockfile_label}) + return struct(**config) def _get_render_config(repository_ctx): @@ -256,6 +262,7 @@ def compile_config( render_config, supported_platform_triples, repository_name, + cargo_lockfile_label = None, repository_ctx = None): """Create a config file for generating crate targets @@ -271,6 +278,9 @@ def compile_config( render_config (dict): The deserialized dict of the `render_config` function. supported_platform_triples (list): A list of platform triples repository_name (str): The name of the repository being generated + cargo_lockfile_label (str, optional): The label of the rule's `cargo_lockfile`. Used to + locate the Bazel repository that owns the Cargo workspace's own crates when + rendering `all_crate_deps(first_party = True)`. repository_ctx (repository_ctx, optional): A repository context object used for enabling certain functionality. @@ -310,6 +320,7 @@ def compile_config( rendering = _update_render_config( config = render_config, repository_name = repository_name, + cargo_lockfile_label = cargo_lockfile_label, ), supported_platform_triples = supported_platform_triples, ) @@ -335,6 +346,9 @@ def generate_config(repository_ctx): render_config = _get_render_config(repository_ctx), supported_platform_triples = repository_ctx.attr.supported_platform_triples, repository_name = repository_ctx.name, + # The hub repository's `crates.bzl` lives outside the workspace being + # generated for, so first-party labels need an explicit repository. + cargo_lockfile_label = str(repository_ctx.attr.cargo_lockfile), repository_ctx = repository_ctx, ) diff --git a/crate_universe/src/api/lockfile.rs b/crate_universe/src/api/lockfile.rs index d509f37d87..e05137fb9d 100644 --- a/crate_universe/src/api/lockfile.rs +++ b/crate_universe/src/api/lockfile.rs @@ -170,6 +170,7 @@ mod test { got_pkg_a.normal_deps().values(), vec![ CrateDependency { + workspace_member: false, id: CrateId { name: String::from("anyhow"), version: Version::new(1, 0, 69), @@ -179,6 +180,7 @@ mod test { local_path: None, }, CrateDependency { + workspace_member: false, id: CrateId { name: String::from("reqwest"), version: Version::new(0, 11, 14), diff --git a/crate_universe/src/config.rs b/crate_universe/src/config.rs index 6adb6b2afd..3f8c5f52df 100644 --- a/crate_universe/src/config.rs +++ b/crate_universe/src/config.rs @@ -132,6 +132,21 @@ pub(crate) struct RenderConfig { /// continue to write subpackage `BUILD.bazel`s into the hub repo directly. #[serde(default)] pub(crate) crates_vendor_synthesizes_subpackages: bool, + + /// Internal: the label of the rule's `cargo_lockfile`. Only its repository + /// is used, to locate the Bazel repository holding the Cargo workspace's + /// own crates so `all_crate_deps(first_party = True)` can emit labels for + /// them. Injected by the rules, never set by users — a workspace member's + /// Bazel package is already known (`Context::workspace_members`); the + /// repository is the one piece the renderer cannot infer. + /// + /// Excluded from the digest by [`crate::lockfile::Digest::new`], mirroring + /// `label_injection_mapping`: canonical repository names are consumer- + /// specific, so hashing this would make a root-level + /// `single_version_override` demand a producer-side repin that a + /// registry-distributed lockfile in a read-only cache can never perform. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) cargo_lockfile_label: Option