Add a Cargo toolchain (#4339) This change adds a stand-alone toolchain for Cargo. This change aims to help reduce the size of `rust_toolchain` by removing components not needed by core `@rules_rust//rust/...`.
diff --git a/cargo/BUILD.bazel b/cargo/BUILD.bazel index d1bad6d..8a1952e 100644 --- a/cargo/BUILD.bazel +++ b/cargo/BUILD.bazel
@@ -1,9 +1,20 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("//cargo:cargo_toolchain.bzl", "cargo_toolchain_alias") exports_files( glob(["*.bzl"]) + ["Cargo.toml"], ) +toolchain_type( + name = "toolchain_type", + visibility = ["//visibility:public"], +) + +cargo_toolchain_alias( + name = "current_cargo_toolchain", + visibility = ["//visibility:public"], +) + bzl_library( name = "bzl_lib", srcs = glob(["*.bzl"]),
diff --git a/cargo/cargo_toolchain.bzl b/cargo/cargo_toolchain.bzl new file mode 100644 index 0000000..cf47bd9 --- /dev/null +++ b/cargo/cargo_toolchain.bzl
@@ -0,0 +1,14 @@ +"""# cargo_toolchain.bzl""" + +load( + "//cargo/private:cargo_toolchain.bzl", + _cargo_toolchain = "cargo_toolchain", + _cargo_toolchain_alias = "cargo_toolchain_alias", + _cargo_version_mismatch = "cargo_version_mismatch", + _find_cargo = "find_cargo", +) + +cargo_toolchain = _cargo_toolchain +cargo_toolchain_alias = _cargo_toolchain_alias +cargo_version_mismatch = _cargo_version_mismatch +find_cargo = _find_cargo
diff --git a/cargo/defs.bzl b/cargo/defs.bzl index e9d4326..68cec0f 100644 --- a/cargo/defs.bzl +++ b/cargo/defs.bzl
@@ -20,6 +20,11 @@ _cargo_env = "cargo_env", ) load( + "//cargo:cargo_toolchain.bzl", + _cargo_toolchain = "cargo_toolchain", + _cargo_toolchain_alias = "cargo_toolchain_alias", +) +load( "//cargo:extract_cargo_lints.bzl", _extract_cargo_lints = "extract_cargo_lints", ) @@ -31,6 +36,9 @@ cargo_build_script = _cargo_build_script cargo_dep_env = _cargo_dep_env +cargo_toolchain = _cargo_toolchain +cargo_toolchain_alias = _cargo_toolchain_alias + extract_cargo_lints = _extract_cargo_lints cargo_toml_env_vars = _cargo_toml_env_vars
diff --git a/cargo/private/cargo_build_script.bzl b/cargo/private/cargo_build_script.bzl index cb9f0c5..6a76b64 100644 --- a/cargo/private/cargo_build_script.bzl +++ b/cargo/private/cargo_build_script.bzl
@@ -5,6 +5,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") load("@rules_cc//cc:action_names.bzl", "ACTION_NAMES") load("@rules_cc//cc/common:cc_common.bzl", "cc_common") +load("//cargo/private:cargo_toolchain.bzl", "cargo_version_mismatch", "find_cargo") load("//rust:defs.bzl", "rust_common") load("//rust:rust_common.bzl", "BuildInfo", "CrateGroupInfo", "DepInfo") @@ -444,7 +445,11 @@ if pkg_name == "": pkg_name = name_to_pkg_name(ctx.label.name) - toolchain_tools = [toolchain.all_files] + cargo = find_cargo(ctx, toolchain) + mismatch = cargo_version_mismatch(cargo.toolchain, toolchain) + if mismatch: + fail("{}: {}".format(ctx.label, mismatch)) + toolchain_tools = [toolchain.all_files, cargo.files] env = {} @@ -458,8 +463,8 @@ if use_default_shell_env: env.update(ctx.configuration.default_shell_env) - if toolchain.cargo: - env["CARGO"] = "${pwd}/%s" % toolchain.cargo.path + if cargo.cargo: + env["CARGO"] = "${pwd}/%s" % cargo.cargo.path env.update({ "CARGO_CRATE_NAME": name_to_crate_name(pkg_name), @@ -942,6 +947,7 @@ fragments = ["cpp"], toolchains = [ str(Label("//rust:toolchain_type")), + config_common.toolchain_type(rust_common.cargo_toolchain_type, mandatory = False), config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False), ], )
diff --git a/cargo/private/cargo_toolchain.bzl b/cargo/private/cargo_toolchain.bzl new file mode 100644 index 0000000..b75ed88 --- /dev/null +++ b/cargo/private/cargo_toolchain.bzl
@@ -0,0 +1,187 @@ +"""The `cargo_toolchain` rule and helpers for rules which run Cargo.""" + +# buildifier: disable=bzl-visibility +load("//rust/private:common.bzl", "rust_common") + +# buildifier: disable=bzl-visibility +load("//rust/private:lint_test.bzl", "rlocationpath") + +def _cargo_toolchain_impl(ctx): + cargo = ctx.file.cargo + + make_variables = { + "CARGO": cargo.path, + "CARGO_RLOCATIONPATH": rlocationpath(cargo, ctx.workspace_name), + } + make_variable_info = platform_common.TemplateVariableInfo(make_variables) + + toolchain = platform_common.ToolchainInfo( + cargo = cargo, + all_files = depset([cargo]), + make_variables = make_variable_info, + version = ctx.attr.version, + iso_date = ctx.attr.iso_date, + channel = ctx.attr.channel, + ) + + return [ + toolchain, + make_variable_info, + ] + +cargo_toolchain = rule( + doc = """\ +A toolchain providing a [Cargo](https://doc.rust-lang.org/cargo/) binary. + +Rules which run Cargo on the execution platform, such as `crates_vendor` and +`cargo_build_script`, resolve `@rules_rust//cargo:toolchain_type` and use the +binary it provides. The toolchain carries Cargo alone, so compile actions, +which never run Cargo, do not depend on it. + +Cargo is released together with `rustc` and is only tested against the +compiler it shipped with, so the `rust` module extension declares a +`cargo_toolchain` in the same repository as every `rust_toolchain` it +registers and registers the two with the same constraints and channel setting. +The toolchain carries the same `version`, `iso_date` and `channel` identifiers +as `rust_toolchain`; `cargo_build_script` fails when the Cargo and Rust +toolchains it resolves state different ones. + +`$(CARGO)` and `$(CARGO_RLOCATIONPATH)` are available as Make variables to +targets which list `@rules_rust//cargo:current_cargo_toolchain`, a +`cargo_toolchain_alias`, in `toolchains`. +""", + implementation = _cargo_toolchain_impl, + attrs = { + "cargo": attr.label( + doc = "The `cargo` binary. Can be a direct source or a filegroup containing one item.", + allow_single_file = True, + cfg = "exec", + mandatory = True, + ), + "channel": attr.string( + doc = "The Rust release channel Cargo comes from (`stable`, `nightly`, or `beta`), as in `rust_toolchain.channel`.", + default = "", + ), + "iso_date": attr.string( + doc = "The ISO date of the nightly or beta release Cargo comes from, as in `rust_toolchain.iso_date`. Empty for stable releases.", + default = "", + ), + "version": attr.string( + doc = ( + "The version of the Rust release Cargo comes from (e.g. `1.94.1`), as in `rust_toolchain.version`. " + + "Rules which run Cargo next to `rustc` compare the two; leave empty to opt out." + ), + default = "", + ), + }, +) + +def cargo_version_mismatch(cargo_toolchain, rust_toolchain): + """Describe how a `cargo_toolchain` and a `rust_toolchain` disagree on their release. + + Each of `version`, `iso_date` and `channel` is compared only when both + toolchains state it, so toolchains which leave them empty are never + rejected. + + Args: + cargo_toolchain (ToolchainInfo): The resolved `cargo_toolchain`, or `None`. + rust_toolchain (ToolchainInfo): The resolved `rust_toolchain`, or `None`. + + Returns: + str: A description of the mismatch, or `None` when the toolchains agree. + """ + if not cargo_toolchain or not rust_toolchain: + return None + + for field in ("version", "iso_date", "channel"): + cargo_value = getattr(cargo_toolchain, field, "") + rust_value = getattr(rust_toolchain, field, "") + if cargo_value and rust_value and cargo_value != rust_value: + return ( + "the `cargo_toolchain` ({} {}) and the `rust_toolchain` ({} {}) come from different " + + "Rust releases. Cargo is only tested with the `rustc` it was released with; register " + + "a `cargo_toolchain` from the same release as the `rust_toolchain`." + ).format(field, cargo_value, field, rust_value) + return None + +def _cargo_toolchain_alias_impl(ctx): + toolchain = ctx.toolchains[rust_common.cargo_toolchain_type] + if not toolchain: + # Until the deprecated `rust_toolchain.cargo` is removed, stand in for + # it so that this target resolves wherever `find_cargo` would. + rust_toolchain = ctx.toolchains[rust_common.toolchain_type] + if rust_toolchain and rust_toolchain.cargo: + make_variables = platform_common.TemplateVariableInfo({ + "CARGO": rust_toolchain.cargo.path, + "CARGO_RLOCATIONPATH": rlocationpath(rust_toolchain.cargo, ctx.workspace_name), + }) + toolchain = platform_common.ToolchainInfo( + cargo = rust_toolchain.cargo, + all_files = depset([rust_toolchain.cargo]), + make_variables = make_variables, + version = rust_toolchain.version, + iso_date = rust_toolchain.iso_date, + channel = rust_toolchain.channel, + ) + if not toolchain: + if ctx.attr.mandatory: + fail("{}: no `cargo_toolchain` is registered for the current platform".format(ctx.label)) + return [] + + return [ + toolchain, + toolchain.make_variables, + DefaultInfo( + files = depset([toolchain.cargo]), + runfiles = ctx.runfiles(transitive_files = toolchain.all_files), + ), + ] + +cargo_toolchain_alias = rule( + doc = """\ +A target standing in for the `cargo_toolchain` resolved for the current platform. + +The alias forwards the toolchain's providers, its Make variables (`$(CARGO)`, +`$(CARGO_RLOCATIONPATH)`) and the `cargo` binary. +`@rules_rust//cargo:current_cargo_toolchain` is the alias `rules_rust` +declares. While the deprecated `cargo` attribute of `rust_toolchain` exists, +the alias stands in for it when no `cargo_toolchain` is registered. + +This follows `cc_toolchain_alias` from `rules_cc`. +""", + implementation = _cargo_toolchain_alias_impl, + attrs = { + "mandatory": attr.bool( + doc = "Whether to fail when no toolchain is registered. When `False`, the target provides nothing instead.", + default = True, + ), + }, + toolchains = [ + config_common.toolchain_type(rust_common.cargo_toolchain_type, mandatory = False), + config_common.toolchain_type(rust_common.toolchain_type, mandatory = False), + ], +) + +def find_cargo(ctx, rust_toolchain): + """The Cargo binary a rule should run, and the files it needs at run time. + + The registered `cargo_toolchain` is preferred. When none is registered the + `cargo` of the `rust_toolchain` is used, which is how rules found Cargo + before `@rules_rust//cargo:toolchain_type` existed. + + Args: + ctx (ctx): The rule context. The rule must list + `config_common.toolchain_type(rust_common.cargo_toolchain_type, mandatory = False)` + in `toolchains`. + rust_toolchain (ToolchainInfo): The resolved `rust_toolchain`, or `None`. + + Returns: + struct: `cargo`, the binary (a `File`, or `None` when neither toolchain + provides one), `files`, a `depset` to add to the action's tools, and + `toolchain`, the `cargo_toolchain` used, or `None` when falling back. + """ + cargo_toolchain = ctx.toolchains[rust_common.cargo_toolchain_type] + if cargo_toolchain: + return struct(cargo = cargo_toolchain.cargo, files = cargo_toolchain.all_files, toolchain = cargo_toolchain) + cargo = rust_toolchain.cargo if rust_toolchain else None + return struct(cargo = cargo, files = depset([cargo] if cargo else []), toolchain = None)
diff --git a/cargo/tests/cargo_build_script/cargo_toolchain/BUILD.bazel b/cargo/tests/cargo_build_script/cargo_toolchain/BUILD.bazel new file mode 100644 index 0000000..ea8fbe2 --- /dev/null +++ b/cargo/tests/cargo_build_script/cargo_toolchain/BUILD.bazel
@@ -0,0 +1,22 @@ +load("//cargo:defs.bzl", "cargo_build_script") +load("//rust:defs.bzl", "rust_test") +load(":cargo_toolchain_test.bzl", "cargo_toolchain_test_suite") + +# `cargo_build_script` hands build scripts the Cargo of the registered +# `cargo_toolchain` as `$CARGO`, not the one of the `rust_toolchain`. +cargo_build_script( + name = "cargo_toolchain_build_rs", + srcs = ["build.rs"], + edition = "2021", +) + +rust_test( + name = "cargo_toolchain_test", + srcs = ["test.rs"], + edition = "2021", + deps = [":cargo_toolchain_build_rs"], +) + +cargo_toolchain_test_suite( + name = "cargo_toolchain_test_suite", +)
diff --git a/cargo/tests/cargo_build_script/cargo_toolchain/build.rs b/cargo/tests/cargo_build_script/cargo_toolchain/build.rs new file mode 100644 index 0000000..5e5b3d3 --- /dev/null +++ b/cargo/tests/cargo_build_script/cargo_toolchain/build.rs
@@ -0,0 +1,22 @@ +//! Checks the `CARGO` a build script is given. + +use std::env; +use std::path::Path; + +fn main() { + let cargo = env::var("CARGO").expect("`cargo_build_script` sets `CARGO`"); + assert!( + Path::new(&cargo).is_file(), + "`CARGO` should name an existing file, got `{cargo}`" + ); + + // The `cargo_toolchain` points at the distribution's own `bin/cargo`. + // The deprecated `rust_toolchain.cargo` is a symlink inside the + // generated sysroot, under a `rust_toolchain/` directory. + assert!( + !cargo.replace('\\', "/").contains("/rust_toolchain/"), + "`CARGO` should come from the `cargo_toolchain`, not the `rust_toolchain` sysroot, got `{cargo}`" + ); + + println!("cargo:rustc-env=CARGO_FROM_BUILD_SCRIPT={cargo}"); +}
diff --git a/cargo/tests/cargo_build_script/cargo_toolchain/cargo_toolchain_test.bzl b/cargo/tests/cargo_build_script/cargo_toolchain/cargo_toolchain_test.bzl new file mode 100644 index 0000000..c09680c --- /dev/null +++ b/cargo/tests/cargo_build_script/cargo_toolchain/cargo_toolchain_test.bzl
@@ -0,0 +1,104 @@ +"""Tests that the Cargo toolchain stays in lockstep with the Rust toolchain.""" + +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest") +load("//cargo:cargo_toolchain.bzl", "cargo_version_mismatch") +load("//rust:defs.bzl", "rust_common") + +_ResolvedVersionsInfo = provider( + doc = "The release identifiers of the resolved Cargo and Rust toolchains.", + fields = ["cargo", "rust"], +) + +def _identifiers(toolchain): + return struct( + version = toolchain.version, + iso_date = toolchain.iso_date, + channel = toolchain.channel, + ) + +def _resolved_versions_impl(ctx): + return [_ResolvedVersionsInfo( + cargo = _identifiers(ctx.toolchains[rust_common.cargo_toolchain_type]), + rust = _identifiers(ctx.toolchains[rust_common.toolchain_type]), + )] + +resolved_versions = rule( + implementation = _resolved_versions_impl, + doc = "Resolves both toolchains for the current platform and reports their release identifiers.", + toolchains = [ + config_common.toolchain_type(rust_common.cargo_toolchain_type), + config_common.toolchain_type(rust_common.toolchain_type), + ], +) + +def _registered_toolchains_match_test_impl(ctx): + env = analysistest.begin(ctx) + info = analysistest.target_under_test(env)[_ResolvedVersionsInfo] + + asserts.true(env, bool(info.rust.version), "The registered `rust_toolchain` should state a version") + asserts.true(env, bool(info.cargo.version), "The registered `cargo_toolchain` should state a version") + for field in ("version", "iso_date", "channel"): + asserts.equals( + env, + getattr(info.rust, field), + getattr(info.cargo, field), + "`cargo_toolchain` and `rust_toolchain` should agree on `{}`".format(field), + ) + asserts.equals(env, None, cargo_version_mismatch(info.cargo, info.rust)) + + return analysistest.end(env) + +registered_toolchains_match_test = analysistest.make(_registered_toolchains_match_test_impl) + +def _cargo_version_mismatch_test_impl(ctx): + env = unittest.begin(ctx) + + stable = struct(version = "1.98.0", iso_date = "", channel = "stable") + nightly = struct(version = "1.100.0", iso_date = "2026-08-20", channel = "nightly") + other_nightly = struct(version = "1.100.0", iso_date = "2026-09-03", channel = "nightly") + unversioned = struct(version = "", iso_date = "", channel = "") + + asserts.equals(env, None, cargo_version_mismatch(stable, stable)) + asserts.equals(env, None, cargo_version_mismatch(nightly, nightly)) + + # Toolchains which do not state identifiers are never rejected. + asserts.equals(env, None, cargo_version_mismatch(unversioned, stable)) + asserts.equals(env, None, cargo_version_mismatch(stable, unversioned)) + asserts.equals(env, None, cargo_version_mismatch(None, stable)) + asserts.equals(env, None, cargo_version_mismatch(stable, None)) + + # Any stated identifier that differs is a mismatch. + asserts.true(env, "version 1.98.0" in cargo_version_mismatch(stable, nightly)) + asserts.true(env, "iso_date 2026-08-20" in cargo_version_mismatch(nightly, other_nightly)) + + return unittest.end(env) + +cargo_version_mismatch_test = unittest.make(_cargo_version_mismatch_test_impl) + +def cargo_toolchain_test_suite(name): + """Entry-point macro called from the BUILD file. + + Args: + name (str): The name of the test suite. + """ + resolved_versions( + name = "resolved_versions", + tags = ["manual"], + ) + + registered_toolchains_match_test( + name = "registered_toolchains_match_test", + target_under_test = ":resolved_versions", + ) + + cargo_version_mismatch_test( + name = "cargo_version_mismatch_test", + ) + + native.test_suite( + name = name, + tests = [ + ":registered_toolchains_match_test", + ":cargo_version_mismatch_test", + ], + )
diff --git a/cargo/tests/cargo_build_script/cargo_toolchain/test.rs b/cargo/tests/cargo_build_script/cargo_toolchain/test.rs new file mode 100644 index 0000000..57954c5 --- /dev/null +++ b/cargo/tests/cargo_build_script/cargo_toolchain/test.rs
@@ -0,0 +1,11 @@ +//! The build script saw the `cargo_toolchain`'s Cargo. + +#[test] +fn build_script_was_given_cargo() { + let cargo = std::path::Path::new(env!("CARGO_FROM_BUILD_SCRIPT")); + let stem = cargo + .file_stem() + .and_then(|stem| stem.to_str()) + .expect("`CARGO` ends in a file name"); + assert_eq!(stem, "cargo", "unexpected binary {}", cargo.display()); +}
diff --git a/crate_universe/BUILD.bazel b/crate_universe/BUILD.bazel index 184aa05..af04d12 100644 --- a/crate_universe/BUILD.bazel +++ b/crate_universe/BUILD.bazel
@@ -109,14 +109,14 @@ crate = ":cargo_bazel", data = glob(["test_data/**"]) + [ "//crate_universe/test_data/serialized_configs", - "@rules_rust//rust/toolchain:current_cargo_files", + "@rules_rust//cargo:current_cargo_toolchain", "@rules_rust//rust/toolchain:current_rustc_files", ], proc_macro_deps = all_crate_deps( proc_macro_dev = True, ), rustc_env = { - "CARGO": "$(rootpath @rules_rust//rust/toolchain:current_cargo_files)", + "CARGO": "$(rootpath @rules_rust//cargo:current_cargo_toolchain)", "RUSTC": "$(rootpath @rules_rust//rust/toolchain:current_rustc_files)", }, # TODO: `crate_universe` currently does not work on Windows. @@ -139,12 +139,12 @@ srcs = ["tests/cargo_integration_test.rs"], aliases = aliases(), data = glob(["test_data/**"]) + [ - "@rules_rust//rust/toolchain:current_cargo_files", + "@rules_rust//cargo:current_cargo_toolchain", "@rules_rust//rust/toolchain:current_rustc_files", ], edition = "2021", env = { - "CARGO": "$(rlocationpath @rules_rust//rust/toolchain:current_cargo_files)", + "CARGO": "$(rlocationpath @rules_rust//cargo:current_cargo_toolchain)", "RUSTC": "$(rlocationpath @rules_rust//rust/toolchain:current_rustc_files)", }, proc_macro_deps = all_crate_deps(
diff --git a/crate_universe/private/crates_vendor.bzl b/crate_universe/private/crates_vendor.bzl index d562c48..bea0afb 100644 --- a/crate_universe/private/crates_vendor.bzl +++ b/crate_universe/private/crates_vendor.bzl
@@ -1,8 +1,10 @@ """Rules for vendoring Bazel targets into existing workspaces""" +load("//cargo:cargo_toolchain.bzl", "cargo_version_mismatch", "find_cargo") load("//crate_universe/private:generate_utils.bzl", "compile_config", generate_render_config = "render_config") load("//crate_universe/private:splicing_utils.bzl", "kebab_case_keys", generate_splicing_config = "splicing_config") load("//crate_universe/private:urls.bzl", "CARGO_BAZEL_LABEL") +load("//rust:defs.bzl", "rust_common") load("//rust/platform:triple_mappings.bzl", "SUPPORTED_PLATFORM_TRIPLES") _UNIX_WRAPPER = """\ @@ -388,10 +390,16 @@ def _crates_vendor_impl(ctx): toolchain = ctx.toolchains[Label("@rules_rust//rust:toolchain_type")] + cargo = find_cargo(ctx, toolchain) + if not cargo.cargo: + fail("{}: no `cargo_toolchain` is registered for the current platform and the `rust_toolchain` has no `cargo`".format(ctx.label)) + mismatch = cargo_version_mismatch(cargo.toolchain, toolchain) + if mismatch: + fail("{}: {}".format(ctx.label, mismatch)) is_windows = _is_windows(ctx) environ = [ - _sys_runfile_env(ctx, "CARGO", toolchain.cargo, is_windows), + _sys_runfile_env(ctx, "CARGO", cargo.cargo, is_windows), _sys_runfile_env(ctx, "RUSTC", toolchain.rustc, is_windows), ] @@ -467,7 +475,10 @@ is_executable = True, ) - runfiles = ctx.runfiles(files = cargo_bazel_runfiles, transitive_files = toolchain.all_files) + runfiles = ctx.runfiles( + files = cargo_bazel_runfiles, + transitive_files = depset(transitive = [toolchain.all_files, cargo.files]), + ) if runner.basename.endswith(".sh"): runfiles = runfiles.merge(ctx.attr._bash_runfiles[DefaultInfo].default_runfiles) @@ -667,7 +678,10 @@ """, attrs = CRATES_VENDOR_ATTRS, executable = True, - toolchains = ["@rules_rust//rust:toolchain_type"], + toolchains = [ + "@rules_rust//rust:toolchain_type", + config_common.toolchain_type(rust_common.cargo_toolchain_type, mandatory = False), + ], ) def _resolve_vendor_files(repository_ctx):
diff --git a/crate_universe/test_data/private/BUILD.bazel b/crate_universe/test_data/private/BUILD.bazel index 8e0993f..653a94d 100644 --- a/crate_universe/test_data/private/BUILD.bazel +++ b/crate_universe/test_data/private/BUILD.bazel
@@ -4,11 +4,11 @@ name = "metadata_generator", srcs = ["metadata_generator.py"], data = [ - "@rules_rust//rust/toolchain:current_cargo_files", + "@rules_rust//cargo:current_cargo_toolchain", "@rules_rust//rust/toolchain:current_rustc_files", ], env = { - "CARGO": "$(rootpath @rules_rust//rust/toolchain:current_cargo_files)", + "CARGO": "$(rootpath @rules_rust//cargo:current_cargo_toolchain)", "RUSTC": "$(rootpath @rules_rust//rust/toolchain:current_rustc_files)", }, )
diff --git a/crate_universe/tools/cross_installer/BUILD.bazel b/crate_universe/tools/cross_installer/BUILD.bazel index 18daac0..7392dd2 100644 --- a/crate_universe/tools/cross_installer/BUILD.bazel +++ b/crate_universe/tools/cross_installer/BUILD.bazel
@@ -18,12 +18,12 @@ data = [ "Cross.toml", ":cross", - "@rules_rust//rust/toolchain:current_cargo_files", + "@rules_rust//cargo:current_cargo_toolchain", ], edition = "2021", proc_macro_deps = all_crate_deps(proc_macro = True), rustc_env = { - "CARGO": "$(rlocationpath @rules_rust//rust/toolchain:current_cargo_files)", + "CARGO": "$(rlocationpath @rules_rust//cargo:current_cargo_toolchain)", "CROSS_BIN": "$(rlocationpath :cross)", "CROSS_CONFIG_RLOCATION": "$(rlocationpath :Cross.toml)", },
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 5da6684..5722b8e 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel
@@ -268,6 +268,18 @@ ) stardoc( + name = "cargo_toolchain", + out = "src/cargo_toolchain.md", + input = "@rules_rust//cargo:cargo_toolchain.bzl", + symbol_names = [ + "cargo_toolchain", + "cargo_toolchain_alias", + "find_cargo", + ], + deps = [":all_docs"], +) + +stardoc( name = "crate_universe_workspace", out = "src/crate_universe_workspace.md", input = "@rules_rust//crate_universe:defs.bzl", @@ -354,6 +366,7 @@ ":cargo_build_script", ":cargo_env", ":cargo_settings", + ":cargo_toolchain", ":crate_universe_bzlmod", ":crate_universe_workspace", ":extract_cargo_lints",
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 4224c10..ffcf8f9 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md
@@ -41,6 +41,7 @@ - [Toolchains](./rust_toolchains.md) - [rust_toolchain](./rust_toolchain.md) - [rustfmt_toolchain](./rustfmt_toolchain.md) + - [cargo_toolchain](./cargo_toolchain.md) - [rust_analyzer_toolchain](./rust_analyzer_toolchain.md) - [rust_stdlib_filegroup](./rust_stdlib_filegroup.md) - [Bzlmod](./rust_bzlmod.md)
diff --git a/docs/src/cargo.md b/docs/src/cargo.md index 2caf9b0..bbc8241 100644 --- a/docs/src/cargo.md +++ b/docs/src/cargo.md
@@ -19,3 +19,13 @@ - [extract_cargo_lints](./extract_cargo_lints.md) — Read the `[lints]` table from a `Cargo.toml` and produce a [`rust_lint_config`](./rust_lint_config.md) target from it, so Cargo-defined lints apply to Bazel-built crates. + +## Toolchain + +- [cargo_toolchain](./cargo_toolchain.md) — Provide the `cargo` binary that + `cargo_build_script` exposes to build scripts as `$CARGO` and that + [crates_vendor](./crates_vendor.md) runs. Registered for + `@rules_rust//cargo:toolchain_type`, which rules declare as + `rust_common.cargo_toolchain_type`. The same page documents `find_cargo`, which + falls back to the `cargo` of the `rust_toolchain` when no `cargo_toolchain` is + registered.
diff --git a/docs/src/rust_toolchains.md b/docs/src/rust_toolchains.md index a3f4ba3..3d72e87 100644 --- a/docs/src/rust_toolchains.md +++ b/docs/src/rust_toolchains.md
@@ -2,8 +2,8 @@ A Rust build in Bazel selects its compiler, standard library, and companion tools through a set of registered [Bazel toolchains][bazel_toolchains]. `rules_rust` -ships one toolchain type per companion tool so that Rust, `rust-analyzer`, and -`rustfmt` can be resolved independently. +ships one toolchain type per companion tool so that Rust, `rust-analyzer`, +`rustfmt`, and `cargo` can be resolved independently. [bazel_toolchains]: https://bazel.build/extending/toolchains @@ -22,6 +22,16 @@ - [rust_analyzer_toolchain](./rust_analyzer_toolchain.md) — Declare a toolchain providing the `rust-analyzer` binary and proc-macro server used by the [rust_analyzer](./rust_analyzer.md) integration. +- [cargo_toolchain](./cargo_toolchain.md) — Declare a toolchain providing the + `cargo` binary used by rules which run Cargo, such as + [cargo_build_script](./cargo_build_script.md) and + [crates_vendor](./crates_vendor.md). The `rust` module extension declares one + in the repository of every `rust_toolchain` it registers and registers the two + together, so Cargo always comes from the same release as `rustc`; the + toolchain carries the same `version`, `iso_date` and `channel` identifiers and + `cargo_build_script` fails when they differ. The `cargo` attribute of + `rust_toolchain` is deprecated in its favor and is only consulted when no + `cargo_toolchain` is registered. ## Data rules
diff --git a/rust/extensions.bzl b/rust/extensions.bzl index d2ceb9b..319ce72 100644 --- a/rust/extensions.bzl +++ b/rust/extensions.bzl
@@ -127,6 +127,7 @@ # so that the `register_toolchains()` in MODULES.bazel is still # valid. _empty_repository(name = "rust_toolchains") + else: extra_rustc_flags = toolchain.extra_rustc_flags if toolchain.extra_rustc_flags else toolchain.extra_rustc_flags_triples extra_exec_rustc_flags = toolchain.extra_exec_rustc_flags if toolchain.extra_rustc_flags else toolchain.extra_exec_rustc_flags_triples
diff --git a/rust/private/common.bzl b/rust/private/common.bzl index 575221d..0a12881 100644 --- a/rust/private/common.bzl +++ b/rust/private/common.bzl
@@ -104,6 +104,13 @@ test_crate_info = TestCrateInfo, crate_group_info = CrateGroupInfo, default_version = DEFAULT_RUST_VERSION, + # The toolchain types `rules_rust` defines, for rules which resolve them: + # `toolchains = [config_common.toolchain_type(rust_common.cargo_toolchain_type, mandatory = False)]` + # and `ctx.toolchains[rust_common.cargo_toolchain_type]`. + toolchain_type = Label("//rust:toolchain_type"), + cargo_toolchain_type = Label("//cargo:toolchain_type"), + rust_analyzer_toolchain_type = Label("//rust/rust_analyzer:toolchain_type"), + rustfmt_toolchain_type = Label("//rust/rustfmt:toolchain_type"), ) COMMON_PROVIDERS = [
diff --git a/rust/private/repositories.bzl b/rust/private/repositories.bzl index eaf24d3..00ce961 100644 --- a/rust/private/repositories.bzl +++ b/rust/private/repositories.bzl
@@ -7,6 +7,7 @@ load("//rust/private:nightly_versions.bzl", "NIGHTLY_VERSION_TRANSITIONS") load( "//rust/private:repository_utils.bzl", + "BUILD_for_cargo_toolchain", "BUILD_for_rust_analyzer_proc_macro_srv", "BUILD_for_rust_analyzer_toolchain", "BUILD_for_rust_toolchain", @@ -49,6 +50,26 @@ _COMPACT_WINDOWS_NAMES = True +# The toolchains declared next to every `rust_toolchain` in its tools +# repository, keyed by the name of their target there and in the proxy +# repository. They come from the same Rust release as `rustc` and must stay +# with it, so they are registered with the same constraints and target +# settings as the `rust_toolchain` they accompany and resolve in lockstep. +COMPANION_TOOLCHAIN_TYPES = { + "cargo_toolchain": "@rules_rust//cargo:toolchain_type", +} + +def _companion_toolchain_labels(tools_repo_name): + """The `toolchain` implementation labels of the companions in `tools_repo_name`.""" + return { + name: "@{}//:{}".format(tools_repo_name, name) + for name in COMPANION_TOOLCHAIN_TYPES + } + +def _companion_hub_name(toolchain_name, companion): + """The name of a companion's `toolchain` target in the hub repository.""" + return "{}__{}".format(toolchain_name, companion.removesuffix("_toolchain")) + _RUST_TOOLCHAIN_VERSIONS = [ rust_common.default_version, DEFAULT_NIGHTLY_VERSION, @@ -216,6 +237,15 @@ toolchain_types[toolchain.name] = "@rules_rust//rust:toolchain" toolchain_target_settings[toolchain.name] = ["@rules_rust//rust/toolchain/channel:{}".format(toolchain.channel.name)] + target_settings + for companion, companion_type in COMPANION_TOOLCHAIN_TYPES.items(): + companion_name = _companion_hub_name(toolchain.name, companion) + toolchain_names.append(companion_name) + toolchain_labels[companion_name] = "@{}//:{}".format(toolchain.name + "_tools", companion) + exec_compatible_with_by_toolchain[companion_name] = exec_compatible_with_by_toolchain[toolchain.name] + target_compatible_with_by_toolchain[companion_name] = toolchain.target_constraints + toolchain_types[companion_name] = companion_type + toolchain_target_settings[companion_name] = toolchain_target_settings[toolchain.name] + # Rustfmt toolchains per exec triple for exec_triple, name in rustfmt_toolchain_triples.items(): rustfmt_repo_name = "rustfmt_{}__{}".format(rustfmt_version.replace("/", "-"), exec_triple) @@ -515,6 +545,15 @@ iso_date = iso_date, )) + # Cargo accompanies the `rust_toolchain` from the same release. + build_components.append(BUILD_for_cargo_toolchain( + name = "cargo_toolchain", + cargo = "//:cargo", + version = toolchain_version, + channel = channel, + iso_date = iso_date, + )) + # Not all target triples are expected to have dev components if ctx.attr.dev_components: rustc_dev_sha256 = load_rustc_dev_nightly( @@ -566,14 +605,26 @@ repository_ctx.name, )) - repository_ctx.file("BUILD.bazel", BUILD_for_toolchain( - name = "toolchain", - toolchain = repository_ctx.attr.toolchain, - target_settings = repository_ctx.attr.target_settings, - toolchain_type = repository_ctx.attr.toolchain_type, - target_compatible_with = repository_ctx.attr.target_compatible_with, - exec_compatible_with = repository_ctx.attr.exec_compatible_with, - )) + toolchains = {"toolchain": (repository_ctx.attr.toolchain, repository_ctx.attr.toolchain_type)} + for name, toolchain in repository_ctx.attr.extra_toolchains.items(): + if name not in repository_ctx.attr.extra_toolchain_types: + fail("`extra_toolchain_types` is missing the toolchain type of `{}`. Please update {}".format( + name, + repository_ctx.name, + )) + toolchains[name] = (toolchain, repository_ctx.attr.extra_toolchain_types[name]) + + repository_ctx.file("BUILD.bazel", "\n".join([ + BUILD_for_toolchain( + name = name, + toolchain = toolchain, + target_settings = repository_ctx.attr.target_settings, + toolchain_type = toolchain_type, + target_compatible_with = repository_ctx.attr.target_compatible_with, + exec_compatible_with = repository_ctx.attr.exec_compatible_with, + ) + for name, (toolchain, toolchain_type) in toolchains.items() + ])) if hasattr(repository_ctx, "repo_metadata"): return repository_ctx.repo_metadata(reproducible = True) return None @@ -587,6 +638,16 @@ "exec_compatible_with": attr.string_list( doc = "A list of constraints for the execution platform for this toolchain.", ), + "extra_toolchain_types": attr.string_dict( + doc = "The toolchain types of `extra_toolchains`, keyed the same way.", + ), + "extra_toolchains": attr.string_dict( + doc = ( + "Further toolchain implementation targets to declare with the same constraints and " + + "target settings as `toolchain`, keyed by the name of the `toolchain` target to declare. " + + "Used for the toolchains which accompany a `rust_toolchain` and must resolve with it." + ), + ), "target_compatible_with": attr.string_list( doc = "A list of constraints for the target platform for this toolchain.", ), @@ -594,7 +655,7 @@ doc = "A list of config_settings that must be satisfied by the target configuration in order for this toolchain to be selected during toolchain resolution.", ), "toolchain": attr.string( - doc = "The name of the toolchain implementation target.", + doc = "The name of the toolchain implementation target, declared as `:toolchain`.", mandatory = True, ), "toolchain_type": attr.string( @@ -713,6 +774,8 @@ toolchain_type = toolchain_type, exec_compatible_with = exec_compatible_with, target_compatible_with = target_compatible_with, + extra_toolchains = _companion_toolchain_labels(tools_repo_name), + extra_toolchain_types = COMPANION_TOOLCHAIN_TYPES, ) return { @@ -1131,6 +1194,7 @@ tools_toolchain_label = "@{}//:rust_toolchain".format(tools_repo_name) toolchain_type = "@rules_rust//rust:toolchain" + companion_labels = _companion_toolchain_labels(tools_repo_name) toolchain_repository_proxy( name = toolchain.name, toolchain = tools_toolchain_label, @@ -1138,8 +1202,14 @@ toolchain_type = toolchain_type, exec_compatible_with = tc_exec_compatible_with, target_compatible_with = tc_target_compatible_with, + extra_toolchains = companion_labels, + extra_toolchain_types = COMPANION_TOOLCHAIN_TYPES, ) + # The hub registers repository sets without the channel setting their + # proxy repositories carry: a set which only provides, say, a nightly + # resolves without `--@rules_rust//rust/toolchain/channel=nightly`, + # and modules rely on that. all_toolchain_details[toolchain.name] = { "exec_compatible_with": tc_exec_compatible_with, "name": toolchain.name, @@ -1150,4 +1220,16 @@ "tools_toolchain_label": tools_toolchain_label, } + for companion, companion_type in COMPANION_TOOLCHAIN_TYPES.items(): + companion_name = _companion_hub_name(toolchain.name, companion) + all_toolchain_details[companion_name] = { + "exec_compatible_with": tc_exec_compatible_with, + "name": companion_name, + "target_compatible_with": tc_target_compatible_with, + "target_settings": target_settings, + "toolchain_label": "@{name}//:{companion}".format(name = toolchain.name, companion = companion), + "toolchain_type": companion_type, + "tools_toolchain_label": companion_labels[companion], + } + return all_toolchain_details
diff --git a/rust/private/repository_utils.bzl b/rust/private/repository_utils.bzl index 3e9f915..b6173c9 100644 --- a/rust/private/repository_utils.bzl +++ b/rust/private/repository_utils.bzl
@@ -780,6 +780,40 @@ rustc_lib = rustc_lib, ) +_build_file_for_cargo_toolchain_template = """\ +load("@rules_rust//cargo:cargo_toolchain.bzl", "cargo_toolchain") + +cargo_toolchain( + name = "{name}", + cargo = "{cargo}", + version = "{version}", + channel = "{channel}", + iso_date = {iso_date}, + visibility = ["//visibility:public"], +) +""" + +def BUILD_for_cargo_toolchain(name, cargo, version, channel, iso_date = None): + """Emits a `cargo_toolchain` target. + + Args: + name (str): The name of the toolchain target. + cargo (str): The label of the `cargo` binary. + version (str): The Rust release Cargo comes from, as passed to `rust_toolchain.version`. + channel (str): The release channel, as passed to `rust_toolchain.channel`. + iso_date (str, optional): The ISO date of a nightly or beta release. + + Returns: + str: The contents of a BUILD file + """ + return _build_file_for_cargo_toolchain_template.format( + name = name, + cargo = cargo, + version = version, + channel = channel, + iso_date = repr(iso_date), + ) + def load_rust_stdlib(ctx, target_triple, version, iso_date = None): """Loads a rust standard library and yields corresponding BUILD for it
diff --git a/rust/private/toolchain.bzl b/rust/private/toolchain.bzl index 221a3ba..b71834b 100644 --- a/rust/private/toolchain.bzl +++ b/rust/private/toolchain.bzl
@@ -685,7 +685,12 @@ mandatory = True, ), "cargo": attr.label( - doc = "The location of the `cargo` binary. Can be a direct source or a filegroup containing one item.", + doc = ( + "The location of the `cargo` binary. Can be a direct source or a filegroup containing one item. " + + "**Deprecated**: register a [`cargo_toolchain`](#cargo_toolchain) for " + + "`@rules_rust//cargo:toolchain_type` instead. Rules which run Cargo use that toolchain " + + "when one is registered and fall back to this attribute when none is." + ), allow_single_file = True, cfg = "exec", ),
diff --git a/rust/private/toolchain_utils.bzl b/rust/private/toolchain_utils.bzl index e2ff839..5289986 100644 --- a/rust/private/toolchain_utils.bzl +++ b/rust/private/toolchain_utils.bzl
@@ -1,17 +1,24 @@ """A module defining toolchain utilities""" +# buildifier: disable=bzl-visibility +load("//cargo/private:cargo_toolchain.bzl", "find_cargo") +load("//rust/private:common.bzl", "rust_common") + def _toolchain_files_impl(ctx): toolchain = ctx.toolchains[str(Label("//rust:toolchain_type"))] runfiles = None if ctx.attr.tool == "cargo": - files = depset([toolchain.cargo]) + cargo = find_cargo(ctx, toolchain) + if not cargo.cargo: + fail("{}: neither the registered `cargo_toolchain` nor the `rust_toolchain` provides `cargo`".format(ctx.label)) + files = depset([cargo.cargo]) runfiles = ctx.runfiles( files = [ - toolchain.cargo, + cargo.cargo, toolchain.rustc, ], - transitive_files = toolchain.rustc_lib, + transitive_files = depset(transitive = [cargo.files, toolchain.rustc_lib]), ) elif ctx.attr.tool == "cargo-clippy": files = depset([toolchain.cargo_clippy]) @@ -85,6 +92,7 @@ }, toolchains = [ str(Label("//rust:toolchain_type")), + config_common.toolchain_type(rust_common.cargo_toolchain_type, mandatory = False), ], )
diff --git a/rust/toolchain/BUILD.bazel b/rust/toolchain/BUILD.bazel index 24f8701..f79c923 100644 --- a/rust/toolchain/BUILD.bazel +++ b/rust/toolchain/BUILD.bazel
@@ -5,11 +5,6 @@ package(default_visibility = ["//visibility:public"]) toolchain_files( - name = "current_cargo_files", - tool = "cargo", -) - -toolchain_files( name = "current_clippy_files", tool = "clippy", ) @@ -59,6 +54,12 @@ ) alias( + name = "current_cargo_files", + actual = "//cargo:current_cargo_toolchain", + deprecation = "Instead use `@rules_rust//cargo:current_cargo_toolchain`", +) + +alias( name = "current_rustfmt_files", actual = "current_rustfmt_toolchain", deprecation = "Instead use `@rules_rust//rust/toolchain:current_rustfmt_toolchain",
diff --git a/test/toolchain/BUILD.bazel b/test/toolchain/BUILD.bazel index bc8e011..1ab28a7 100644 --- a/test/toolchain/BUILD.bazel +++ b/test/toolchain/BUILD.bazel
@@ -18,3 +18,19 @@ name = "inspect_build_test", targets = [":inspect"], ) + +genrule( + name = "cargo_version", + outs = ["cargo-version"], + # `current_cargo_toolchain` is a `cargo_toolchain_alias`: it supplies the + # `$(CARGO)` Make variable and the binary as a tool. + cmd = "$(CARGO) --version > $@", + cmd_bat = "$(CARGO) --version > $@", + toolchains = ["@rules_rust//cargo:current_cargo_toolchain"], + tools = ["@rules_rust//cargo:current_cargo_toolchain"], +) + +build_test( + name = "cargo_version_build_test", + targets = [":cargo_version"], +)
diff --git a/tools/upstream_wrapper/BUILD.bazel b/tools/upstream_wrapper/BUILD.bazel index cec76e0..29aa646 100644 --- a/tools/upstream_wrapper/BUILD.bazel +++ b/tools/upstream_wrapper/BUILD.bazel
@@ -1,7 +1,7 @@ load("//rust:defs.bzl", "rust_binary") TOOLS = { - "cargo": "//rust/toolchain:current_cargo_files", + "cargo": "//cargo:current_cargo_toolchain", "cargo_clippy": "//rust/toolchain:current_cargo_clippy_files", "rustc": "//rust/toolchain:current_rustc_files", "rustdoc": "//rust/toolchain:current_rustdoc_files",