Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow a no-cargo setup for bzlmod #2565

Merged
merged 1 commit into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,12 @@ tasks:
- "@rules_rust//tools/rust_analyzer:gen_rust_project"
test_targets:
- "//..."
bzlmod_no_cargo:
name: Cargo-less bzlmod
platform: ubuntu2004
working_directory: examples/bzlmod/hello_world_no_cargo
build_targets:
- "//..."

buildifier:
version: latest
Expand Down
136 changes: 120 additions & 16 deletions crate_universe/extension.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,35 @@ _generate_repo = repository_rule(
),
)

def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations):
cargo_lockfile = module_ctx.path(cfg.cargo_lockfile)
def _annotations_for_repo(module_annotations, repo_specific_annotations):
UebelAndre marked this conversation as resolved.
Show resolved Hide resolved
"""Merges the set of global annotations with the repo-specific ones

Args:
module_annotations (dict): The annotation tags that apply to all repos, keyed by crate.
repo_specific_annotations (dict): The annotation tags that apply to only this repo, keyed by crate.
"""

if not repo_specific_annotations:
return module_annotations

annotations = dict(module_annotations)
for crate, values in repo_specific_annotations.items():
_get_or_insert(annotations, crate, []).extend(values)
return annotations

def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations, cargo_lockfile = None, manifests = {}, packages = {}):
UebelAndre marked this conversation as resolved.
Show resolved Hide resolved
"""Generates repositories for the transitive closure of crates defined by manifests and packages.

Args:
module_ctx (module_ctx): The module context object.
cargo_bazel (function): A function that can be called to execute cargo_bazel.
cfg (object): The module tag from `from_cargo` or `from_specs`
annotations (dict): The set of annotation tag classes that apply to this closure, keyed by crate name.
cargo_lockfile (path): Path to Cargo.lock, if we have one. This is optional for `from_specs` closures.
manifests (dict): The set of Cargo.toml manifests that apply to this closure, if any, keyed by path.
packages (dict): The set of extra cargo crate tags that apply to this closure, if any, keyed by package name.
"""

tag_path = module_ctx.path(cfg.name)

rendering_config = json.decode(render_config(
Expand All @@ -67,32 +94,35 @@ def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations):
),
)

manifests = {module_ctx.path(m): m for m in cfg.manifests}
splicing_manifest = tag_path.get_child("splicing_manifest.json")
module_ctx.file(
splicing_manifest,
executable = False,
content = generate_splicing_manifest(
packages = {},
packages = packages,
splicing_config = "",
cargo_config = cfg.cargo_config,
manifests = {str(k): str(v) for k, v in manifests.items()},
manifests = manifests,
manifest_to_path = module_ctx.path,
),
)

splicing_output_dir = tag_path.get_child("splicing-output")
cargo_bazel([
splice_args = [
"splice",
"--output-dir",
splicing_output_dir,
"--config",
config_file,
"--splicing-manifest",
splicing_manifest,
"--cargo-lockfile",
cargo_lockfile,
])
]
if cargo_lockfile:
splice_args.extend([
"--cargo-lockfile",
cargo_lockfile,
])
cargo_bazel(splice_args)

# Create a lockfile, since we need to parse it to generate spoke
# repos.
Expand All @@ -102,7 +132,7 @@ def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations):
cargo_bazel([
"generate",
"--cargo-lockfile",
cargo_lockfile,
cargo_lockfile or splicing_output_dir.get_child("Cargo.lock"),
"--config",
config_file,
"--splicing-manifest",
Expand Down Expand Up @@ -181,6 +211,15 @@ def _generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations):
else:
fail("Invalid repo: expected Http or Git to exist for crate %s-%s, got %s" % (name, version, repo))

def _package_to_json(p):
# Avoid adding unspecified properties.
# If we add them as empty strings, cargo-bazel will be unhappy.
return json.encode({
k: v
for k, v in structs.to_dict(p).items()
if v
})

def _crate_impl(module_ctx):
cargo_bazel = get_cargo_bazel_runner(module_ctx)
all_repos = []
Expand Down Expand Up @@ -216,19 +255,34 @@ def _crate_impl(module_ctx):
).append(annotation)

local_repos = []
for cfg in mod.tags.from_cargo:

for cfg in mod.tags.from_cargo + mod.tags.from_specs:
if cfg.name in local_repos:
fail("Defined two crate universes with the same name in the same MODULE.bazel file. Use the name tag to give them different names.")
elif cfg.name in all_repos:
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)")

annotations = {k: v for k, v in module_annotations.items()}
for crate, values in repo_specific_annotations.get(cfg.name, {}).items():
_get_or_insert(annotations, crate, []).extend(values)
_generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations)
all_repos.append(cfg.name)
local_repos.append(cfg.name)

for cfg in mod.tags.from_cargo:
annotations = _annotations_for_repo(
module_annotations,
repo_specific_annotations.get(cfg.name),
)

cargo_lockfile = module_ctx.path(cfg.cargo_lockfile)
manifests = {str(module_ctx.path(m)): str(m) for m in cfg.manifests}
_generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations, cargo_lockfile = cargo_lockfile, manifests = manifests)

for cfg in mod.tags.from_specs:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Looks like a lot of duplicated code between this and the above section. Could we pull the common stuff into a function?

Copy link
Contributor Author

@dzbarsky dzbarsky Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, I rearranged this a bit to share the repo validation. So the duplication now is the 3 lines for annotations. I'm hesitating a bit because sharing this either requires using a lambda or a function that takes module_annotations, repo_specific_annotations, and cfg.name. Alternately, it can look like this_repo_annotations = update_annotations(module_annotations, repo_specific_annotations.get(cfg.name, {})) or something.

Do you still think it's worth pulling out or keep it as-is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quite like the repo_specific_annotations idea. I think that'd probably be the cleanest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I factored that out, PTAL

annotations = _annotations_for_repo(
module_annotations,
repo_specific_annotations.get(cfg.name),
)

packages = {p.package: _package_to_json(p) for p in mod.tags.spec}
_generate_hub_and_spokes(module_ctx, cargo_bazel, cfg, annotations, packages = packages)

for repo in repo_specific_annotations:
if repo not in local_repos:
fail("Annotation specified for repo %s, but the module defined repositories %s" % (repo, local_repos))
Expand Down Expand Up @@ -290,10 +344,60 @@ _annotation = tag_class(
),
)

_from_specs = tag_class(
doc = "Generates a repo @crates from the defined `spec` tags",
attrs = dict(
name = attr.string(doc = "The name of the repo to generate", default = "crates"),
cargo_config = CRATES_VENDOR_ATTRS["cargo_config"],
generate_binaries = CRATES_VENDOR_ATTRS["generate_binaries"],
generate_build_scripts = CRATES_VENDOR_ATTRS["generate_build_scripts"],
supported_platform_triples = CRATES_VENDOR_ATTRS["supported_platform_triples"],
),
)

# This should be kept in sync with crate_universe/private/crate.bzl.
_spec = tag_class(
UebelAndre marked this conversation as resolved.
Show resolved Hide resolved
attrs = dict(
package = attr.string(
doc = "The explicit name of the package.",
mandatory = True,
),
version = attr.string(
doc = "The exact version of the crate. Cannot be used with `git`.",
),
artifact = attr.string(
doc = "Set to 'bin' to pull in a binary crate as an artifact dependency. Requires a nightly Cargo.",
),
lib = attr.bool(
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.",
),
default_features = attr.bool(
doc = "Maps to the `default-features` flag.",
),
features = attr.string_list(
doc = "A list of features to use for the crate.",
),
git = attr.string(
doc = "The Git url to use for the crate. Cannot be used with `version`.",
),
branch = attr.string(
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.",
),
tag = attr.string(
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.",
),
rev = attr.string(
doc = "The git revision of the remote crate. Tied with the `git` param. Only one of branch, tag or rev may be specified.",
),
),
)

crate = module_extension(
implementation = _crate_impl,
tag_classes = dict(
from_cargo = _from_cargo,
annotation = _annotation,
from_specs = _from_specs,
spec = _spec,
),
)
2 changes: 1 addition & 1 deletion crate_universe/private/crates_vendor.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def _write_splicing_manifest(ctx):
return args, runfiles

def generate_splicing_manifest(packages, splicing_config, cargo_config, manifests, manifest_to_path):
# Deserialize information about direct packges
# Deserialize information about direct packages
direct_packages_info = {
# Ensure the data is using kebab-case as that's what `cargo_toml::DependencyDetail` expects.
pkg: kebab_case_keys(dict(json.decode(data)))
Expand Down
3 changes: 3 additions & 0 deletions examples/bzlmod/hello_world_no_cargo/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
common --experimental_enable_bzlmod
common --noenable_workspace
common --enable_runfiles
1 change: 1 addition & 0 deletions examples/bzlmod/hello_world_no_cargo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/bazel-*
11 changes: 11 additions & 0 deletions examples/bzlmod/hello_world_no_cargo/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
load("@rules_rust//rust:defs.bzl", "rust_binary")

package(default_visibility = ["//visibility:public"])

rust_binary(
name = "hello_world",
srcs = ["src/main.rs"],
deps = [
"@crates//:anyhow",
],
)
26 changes: 26 additions & 0 deletions examples/bzlmod/hello_world_no_cargo/MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""bazelbuild/rules_rust - bzlmod no-cargo example"""

module(name = "hello_world_no_cargo")

bazel_dep(
name = "rules_rust",
version = "0.0.0",
)
local_path_override(
module_name = "rules_rust",
path = "../../..",
)

rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(edition = "2021")
use_repo(rust, "rust_toolchains")

register_toolchains("@rust_toolchains//:all")

crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate")
crate.spec(
package = "anyhow",
version = "1.0.77",
)
crate.from_specs()
use_repo(crate, "crates")
18 changes: 18 additions & 0 deletions examples/bzlmod/hello_world_no_cargo/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2015 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

fn main() -> anyhow::Result<()> {
println!("Hello, world!");
Ok(())
}
Loading