Skip to content

Commit 00a4bfb

Browse files
authored
Allow a no-cargo setup for bzlmod (#2565)
Usage looks like so: ``` crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate") crate.spec(package = "anyhow", version = "1.0.77") .... crate.from_specs(name = "crates") ``` It might make sense to merge the annotation attributes into the spec so we don't have to duplicate things, but we can probably iterate on this in the future, this API is still experimental, yeah?
1 parent 5ded574 commit 00a4bfb

File tree

8 files changed

+186
-17
lines changed

8 files changed

+186
-17
lines changed

.bazelci/presubmit.yml

+6
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,12 @@ tasks:
686686
- "@rules_rust//tools/rust_analyzer:gen_rust_project"
687687
test_targets:
688688
- "//..."
689+
bzlmod_no_cargo:
690+
name: Cargo-less bzlmod
691+
platform: ubuntu2004
692+
working_directory: examples/bzlmod/hello_world_no_cargo
693+
build_targets:
694+
- "//..."
689695

690696
buildifier:
691697
version: latest

crate_universe/extension.bzl

+120-16
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,35 @@ _generate_repo = repository_rule(
4040
),
4141
)
4242

43-
def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations):
44-
cargo_lockfile = module_ctx.path(cfg.cargo_lockfile)
43+
def _annotations_for_repo(module_annotations, repo_specific_annotations):
44+
"""Merges the set of global annotations with the repo-specific ones
45+
46+
Args:
47+
module_annotations (dict): The annotation tags that apply to all repos, keyed by crate.
48+
repo_specific_annotations (dict): The annotation tags that apply to only this repo, keyed by crate.
49+
"""
50+
51+
if not repo_specific_annotations:
52+
return module_annotations
53+
54+
annotations = dict(module_annotations)
55+
for crate, values in repo_specific_annotations.items():
56+
_get_or_insert(annotations, crate, []).extend(values)
57+
return annotations
58+
59+
def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations, cargo_lockfile = None, manifests = {}, packages = {}):
60+
"""Generates repositories for the transitive closure of crates defined by manifests and packages.
61+
62+
Args:
63+
module_ctx (module_ctx): The module context object.
64+
cargo_bazel (function): A function that can be called to execute cargo_bazel.
65+
cfg (object): The module tag from `from_cargo` or `from_specs`
66+
annotations (dict): The set of annotation tag classes that apply to this closure, keyed by crate name.
67+
cargo_lockfile (path): Path to Cargo.lock, if we have one. This is optional for `from_specs` closures.
68+
manifests (dict): The set of Cargo.toml manifests that apply to this closure, if any, keyed by path.
69+
packages (dict): The set of extra cargo crate tags that apply to this closure, if any, keyed by package name.
70+
"""
71+
4572
tag_path = module_ctx.path(cfg.name)
4673

4774
rendering_config = json.decode(render_config(
@@ -67,32 +94,35 @@ def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations):
6794
),
6895
)
6996

70-
manifests = {module_ctx.path(m): m for m in cfg.manifests}
7197
splicing_manifest = tag_path.get_child("splicing_manifest.json")
7298
module_ctx.file(
7399
splicing_manifest,
74100
executable = False,
75101
content = generate_splicing_manifest(
76-
packages = {},
102+
packages = packages,
77103
splicing_config = "",
78104
cargo_config = cfg.cargo_config,
79-
manifests = {str(k): str(v) for k, v in manifests.items()},
105+
manifests = manifests,
80106
manifest_to_path = module_ctx.path,
81107
),
82108
)
83109

84110
splicing_output_dir = tag_path.get_child("splicing-output")
85-
cargo_bazel([
111+
splice_args = [
86112
"splice",
87113
"--output-dir",
88114
splicing_output_dir,
89115
"--config",
90116
config_file,
91117
"--splicing-manifest",
92118
splicing_manifest,
93-
"--cargo-lockfile",
94-
cargo_lockfile,
95-
])
119+
]
120+
if cargo_lockfile:
121+
splice_args.extend([
122+
"--cargo-lockfile",
123+
cargo_lockfile,
124+
])
125+
cargo_bazel(splice_args)
96126

97127
# Create a lockfile, since we need to parse it to generate spoke
98128
# repos.
@@ -102,7 +132,7 @@ def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations):
102132
cargo_bazel([
103133
"generate",
104134
"--cargo-lockfile",
105-
cargo_lockfile,
135+
cargo_lockfile or splicing_output_dir.get_child("Cargo.lock"),
106136
"--config",
107137
config_file,
108138
"--splicing-manifest",
@@ -181,6 +211,15 @@ def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations):
181211
else:
182212
fail("Invalid repo: expected Http or Git to exist for crate %s-%s, got %s" % (name, version, repo))
183213

214+
def _package_to_json(p):
215+
# Avoid adding unspecified properties.
216+
# If we add them as empty strings, cargo-bazel will be unhappy.
217+
return json.encode({
218+
k: v
219+
for k, v in structs.to_dict(p).items()
220+
if v
221+
})
222+
184223
def _crate_impl(module_ctx):
185224
cargo_bazel = get_cargo_bazel_runner(module_ctx)
186225
all_repos = []
@@ -216,19 +255,34 @@ def _crate_impl(module_ctx):
216255
).append(annotation)
217256

218257
local_repos = []
219-
for cfg in mod.tags.from_cargo:
258+
259+
for cfg in mod.tags.from_cargo + mod.tags.from_specs:
220260
if cfg.name in local_repos:
221261
fail("Defined two crate universes with the same name in the same MODULE.bazel file. Use the name tag to give them different names.")
222262
elif cfg.name in all_repos:
223263
fail("Defined two crate universes with the same name in different MODULE.bazel files. Either give one a different name, or use use_extension(isolate=True)")
224-
225-
annotations = {k: v for k, v in module_annotations.items()}
226-
for crate, values in repo_specific_annotations.get(cfg.name, {}).items():
227-
_get_or_insert(annotations, crate, []).extend(values)
228-
_generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations)
229264
all_repos.append(cfg.name)
230265
local_repos.append(cfg.name)
231266

267+
for cfg in mod.tags.from_cargo:
268+
annotations = _annotations_for_repo(
269+
module_annotations,
270+
repo_specific_annotations.get(cfg.name),
271+
)
272+
273+
cargo_lockfile = module_ctx.path(cfg.cargo_lockfile)
274+
manifests = {str(module_ctx.path(m)): str(m) for m in cfg.manifests}
275+
_generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations, cargo_lockfile = cargo_lockfile, manifests = manifests)
276+
277+
for cfg in mod.tags.from_specs:
278+
annotations = _annotations_for_repo(
279+
module_annotations,
280+
repo_specific_annotations.get(cfg.name),
281+
)
282+
283+
packages = {p.package: _package_to_json(p) for p in mod.tags.spec}
284+
_generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations, packages = packages)
285+
232286
for repo in repo_specific_annotations:
233287
if repo not in local_repos:
234288
fail("Annotation specified for repo %s, but the module defined repositories %s" % (repo, local_repos))
@@ -290,10 +344,60 @@ _annotation = tag_class(
290344
),
291345
)
292346

347+
_from_specs = tag_class(
348+
doc = "Generates a repo @crates from the defined `spec` tags",
349+
attrs = dict(
350+
name = attr.string(doc = "The name of the repo to generate", default = "crates"),
351+
cargo_config = CRATES_VENDOR_ATTRS["cargo_config"],
352+
generate_binaries = CRATES_VENDOR_ATTRS["generate_binaries"],
353+
generate_build_scripts = CRATES_VENDOR_ATTRS["generate_build_scripts"],
354+
supported_platform_triples = CRATES_VENDOR_ATTRS["supported_platform_triples"],
355+
),
356+
)
357+
358+
# This should be kept in sync with crate_universe/private/crate.bzl.
359+
_spec = tag_class(
360+
attrs = dict(
361+
package = attr.string(
362+
doc = "The explicit name of the package.",
363+
mandatory = True,
364+
),
365+
version = attr.string(
366+
doc = "The exact version of the crate. Cannot be used with `git`.",
367+
),
368+
artifact = attr.string(
369+
doc = "Set to 'bin' to pull in a binary crate as an artifact dependency. Requires a nightly Cargo.",
370+
),
371+
lib = attr.bool(
372+
doc = "If using `artifact = 'bin'`, additionally setting `lib = True` declares a dependency on both the package's library and binary, as opposed to just the binary.",
373+
),
374+
default_features = attr.bool(
375+
doc = "Maps to the `default-features` flag.",
376+
),
377+
features = attr.string_list(
378+
doc = "A list of features to use for the crate.",
379+
),
380+
git = attr.string(
381+
doc = "The Git url to use for the crate. Cannot be used with `version`.",
382+
),
383+
branch = attr.string(
384+
doc = "The git branch of the remote crate. Tied with the `git` param. Only one of branch, tag or rev may be specified. Specifying `rev` is recommended for fully-reproducible builds.",
385+
),
386+
tag = attr.string(
387+
doc = "The git tag of the remote crate. Tied with the `git` param. Only one of branch, tag or rev may be specified. Specifying `rev` is recommended for fully-reproducible builds.",
388+
),
389+
rev = attr.string(
390+
doc = "The git revision of the remote crate. Tied with the `git` param. Only one of branch, tag or rev may be specified.",
391+
),
392+
),
393+
)
394+
293395
crate = module_extension(
294396
implementation = _crate_impl,
295397
tag_classes = dict(
296398
from_cargo = _from_cargo,
297399
annotation = _annotation,
400+
from_specs = _from_specs,
401+
spec = _spec,
298402
),
299403
)

crate_universe/private/crates_vendor.bzl

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def _write_splicing_manifest(ctx):
118118
return args, runfiles
119119

120120
def generate_splicing_manifest(packages, splicing_config, cargo_config, manifests, manifest_to_path):
121-
# Deserialize information about direct packges
121+
# Deserialize information about direct packages
122122
direct_packages_info = {
123123
# Ensure the data is using kebab-case as that's what `cargo_toml::DependencyDetail` expects.
124124
pkg: kebab_case_keys(dict(json.decode(data)))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
common --experimental_enable_bzlmod
2+
common --noenable_workspace
3+
common --enable_runfiles
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/bazel-*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
load("@rules_rust//rust:defs.bzl", "rust_binary")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
rust_binary(
6+
name = "hello_world",
7+
srcs = ["src/main.rs"],
8+
deps = [
9+
"@crates//:anyhow",
10+
],
11+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""bazelbuild/rules_rust - bzlmod no-cargo example"""
2+
3+
module(name = "hello_world_no_cargo")
4+
5+
bazel_dep(
6+
name = "rules_rust",
7+
version = "0.0.0",
8+
)
9+
local_path_override(
10+
module_name = "rules_rust",
11+
path = "../../..",
12+
)
13+
14+
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
15+
rust.toolchain(edition = "2021")
16+
use_repo(rust, "rust_toolchains")
17+
18+
register_toolchains("@rust_toolchains//:all")
19+
20+
crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate")
21+
crate.spec(
22+
package = "anyhow",
23+
version = "1.0.77",
24+
)
25+
crate.from_specs()
26+
use_repo(crate, "crates")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2015 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
fn main() -> anyhow::Result<()> {
16+
println!("Hello, world!");
17+
Ok(())
18+
}

0 commit comments

Comments
 (0)