From b03f74e2f76fb56df1f82b000f9208daa216da70 Mon Sep 17 00:00:00 2001 From: U9G Date: Fri, 25 Sep 2026 04:48:04 -0400 Subject: [PATCH] Read the Rust version from rust-toolchain.toml in rust.toolchain A repository that has a rust-toolchain.toml for rustup and cargo has to write the same version a second time in MODULE.bazel, and nothing keeps the two in step. The new rust_toolchain_file attribute on rust.toolchain reads the file instead, with toml.bzl: channel becomes versions (it must be an exact release, since rules_rust does not resolve stable or 1.85 the way rustup does), targets add to extra_target_triples, and rustc-dev in components enables dev_components. Setting versions as well is an error. Fixes #2753 --- MODULE.bazel | 1 + docs/src/index.md | 10 +++ examples/hello_world_no_cargo/MODULE.bazel | 5 +- .../hello_world_no_cargo/rust-toolchain.toml | 3 + rust/extensions.bzl | 34 +++++++++-- rust/private/BUILD.bazel | 1 + rust/private/rust_toolchain_file.bzl | 42 +++++++++++++ test/unit/rust_toolchain_file/BUILD.bazel | 4 ++ .../rust_toolchain_file_test.bzl | 61 +++++++++++++++++++ 9 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 examples/hello_world_no_cargo/rust-toolchain.toml create mode 100644 rust/private/rust_toolchain_file.bzl create mode 100644 test/unit/rust_toolchain_file/BUILD.bazel create mode 100644 test/unit/rust_toolchain_file/rust_toolchain_file_test.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 7f5526cd2e..3f6c7cff73 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -15,6 +15,7 @@ bazel_dep(name = "platforms", version = "1.1.0") bazel_dep(name = "rules_cc", version = "0.2.4") bazel_dep(name = "rules_license", version = "1.0.0") bazel_dep(name = "rules_shell", version = "0.6.1") +bazel_dep(name = "toml.bzl", version = "0.4.1") bazel_dep(name = "apple_support", version = "1.24.1") internal_deps = use_extension("//rust/private:internal_extensions.bzl", "i") diff --git a/docs/src/index.md b/docs/src/index.md index 07fd8a5ebf..4ea4c382ab 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -42,6 +42,16 @@ rust.toolchain( ) ``` +If the repository has a [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) for rustup and cargo, `rust_toolchain_file` reads the version from it instead, so it is only written once. Its `channel` must be an exact release (`1.85.0`, `nightly-2025-01-01`), as rules_rust downloads a fixed release rather than asking rustup what `stable` is today: + +```python +rust = use_extension("@rules_rust//rust:extensions.bzl", "rust") +rust.toolchain( + edition = "2021", + rust_toolchain_file = "//:rust-toolchain.toml", +) +``` + By default, a `stable` and `nightly` toolchain will be registered if no `toolchain` method is called (and thus no specific versions are registered). However, if only 1 version is passed and it is from the `nightly` or `beta` release channels (i.e. __not__ `stable`), then the following build setting flag must be present, either on the command line or set in the project's `.bazelrc` file: ```text diff --git a/examples/hello_world_no_cargo/MODULE.bazel b/examples/hello_world_no_cargo/MODULE.bazel index 91eba92fee..935005916d 100644 --- a/examples/hello_world_no_cargo/MODULE.bazel +++ b/examples/hello_world_no_cargo/MODULE.bazel @@ -9,7 +9,10 @@ local_path_override( ) rust = use_extension("@rules_rust//rust:extensions.bzl", "rust") -rust.toolchain(edition = "2021") +rust.toolchain( + edition = "2021", + rust_toolchain_file = "//:rust-toolchain.toml", +) use_repo(rust, "rust_toolchains") register_toolchains("@rust_toolchains//:all") diff --git a/examples/hello_world_no_cargo/rust-toolchain.toml b/examples/hello_world_no_cargo/rust-toolchain.toml new file mode 100644 index 0000000000..316581d79e --- /dev/null +++ b/examples/hello_world_no_cargo/rust-toolchain.toml @@ -0,0 +1,3 @@ +# What rustup and cargo read; MODULE.bazel points rules_rust at it too. +[toolchain] +channel = "1.98.0" diff --git a/rust/extensions.bzl b/rust/extensions.bzl index 5e77821146..1179083daf 100644 --- a/rust/extensions.bzl +++ b/rust/extensions.bzl @@ -16,6 +16,7 @@ load( "DEFAULT_NIGHTLY_VERSION", "DEFAULT_STATIC_RUST_URL_TEMPLATES", ) +load("//rust/private:rust_toolchain_file.bzl", "parse_rust_toolchain_file") _RUST_TOOLCHAIN_VERSIONS = [ rust_common.default_version, @@ -120,7 +121,23 @@ def _rust_impl(module_ctx): fail("Cannot define both extra_rustc_flags and extra_rustc_flags_triples") if toolchain.extra_exec_rustc_flags and toolchain.extra_exec_rustc_flags_triples: fail("Cannot define both extra_exec_rustc_flags and extra_exec_rustc_flags_triples") - if len(toolchain.versions) == 0: + + versions = toolchain.versions + extra_target_triples = toolchain.extra_target_triples + dev_components = toolchain.dev_components + if toolchain.rust_toolchain_file: + if toolchain.versions != _RUST_TOOLCHAIN_VERSIONS: + fail("Cannot define both versions and rust_toolchain_file") + toolchain_file = parse_rust_toolchain_file(module_ctx.read(module_ctx.path(toolchain.rust_toolchain_file))) + versions = toolchain_file.versions + extra_target_triples = extra_target_triples + [ + triple + for triple in toolchain_file.extra_target_triples + if triple not in extra_target_triples + ] + dev_components = dev_components or toolchain_file.dev_components + + if len(versions) == 0: # If the root module has asked for rules_rust to not register default # toolchains, an empty repository named `rust_toolchains` is created # so that the `register_toolchains()` in MODULES.bazel is still @@ -132,7 +149,7 @@ def _rust_impl(module_ctx): rust_register_toolchains( hub_name = "rust_toolchains", - dev_components = toolchain.dev_components, + dev_components = dev_components, edition = toolchain.edition, extra_rustc_flags = extra_rustc_flags, extra_exec_rustc_flags = extra_exec_rustc_flags, @@ -141,11 +158,11 @@ def _rust_impl(module_ctx): rustfmt_version = toolchain.rustfmt_version, rust_analyzer_version = toolchain.rust_analyzer_version, sha256s = toolchain.sha256s, - extra_target_triples = toolchain.extra_target_triples, + extra_target_triples = extra_target_triples, opt_level = toolchain.opt_level if toolchain.opt_level else None, strip_level = toolchain.strip_level if toolchain.strip_level else None, urls = toolchain.urls, - versions = toolchain.versions, + versions = versions, compact_windows_names = True, aliases = toolchain.aliases, toolchain_triples = toolchain_triples, @@ -281,6 +298,15 @@ _RUST_TOOLCHAIN_TAG = tag_class( "rust_analyzer_version": attr.string( doc = "The version of Rustc to pair with rust-analyzer.", ), + "rust_toolchain_file": attr.label( + doc = ( + "A `rust-toolchain.toml` file, the one rustup and cargo read, to take the toolchain from " + + "instead of `versions`: its `channel` must be an exact release (`1.85.0`, `nightly-2025-01-01`), " + + "its `targets` are added to `extra_target_triples`, and `rustc-dev` in its `components` " + + "enables `dev_components`." + ), + allow_single_file = True, + ), "rustfmt_toolchain_triples": attr.string_dict( doc = "Like toolchain_triples, but for rustfmt toolchains. Mapping of rust target triple to repository name.", ), diff --git a/rust/private/BUILD.bazel b/rust/private/BUILD.bazel index ce935ba6d0..f50a295f84 100644 --- a/rust/private/BUILD.bazel +++ b/rust/private/BUILD.bazel @@ -28,6 +28,7 @@ bzl_library( "@bazel_skylib//lib:paths", "@bazel_skylib//lib:structs", "@bazel_skylib//rules:common_settings", + "@toml.bzl//:toml", ], ) diff --git a/rust/private/rust_toolchain_file.bzl b/rust/private/rust_toolchain_file.bzl new file mode 100644 index 0000000000..c005f105d8 --- /dev/null +++ b/rust/private/rust_toolchain_file.bzl @@ -0,0 +1,42 @@ +"""Reads the `rust-toolchain.toml` file that rustup and cargo read.""" + +load("@toml.bzl", "toml") + +def parse_rust_toolchain_file(content): + """Turns a `rust-toolchain.toml` file into `rust.toolchain` settings. + + Only what rules_rust can act on without rustup is read: `channel` must name + an exact release (`1.85.0`, `nightly-2025-01-01`, `beta-2025-01-01`), + because resolving `stable` or `1.85` to a release is something rustup does + with the network. `targets` adds to `extra_target_triples`, and + `components` enables `dev_components` if it lists `rustc-dev`. + + Args: + content (str): The contents of a `rust-toolchain.toml` file. + + Returns: + struct: With `versions`, `extra_target_triples` and `dev_components` + fields, in the shape the `rust.toolchain` tag takes. + """ + toolchain = toml.decode(content).get("toolchain", {}) + channel = toolchain.get("channel") + if not channel: + fail("rust-toolchain.toml has no `channel` in its `[toolchain]` table") + + if channel.startswith("nightly-") or channel.startswith("beta-"): + name, iso_date = channel.split("-", 1) + version = "{}/{}".format(name, iso_date) + elif channel[0].isdigit() and channel.count(".") == 2: + version = channel + else: + fail(( + "rust-toolchain.toml channel `{}` is not an exact release: rules_rust " + + "downloads a fixed release, so the channel must be a full version " + + "(`1.85.0`) or a dated nightly or beta (`nightly-2025-01-01`)" + ).format(channel)) + + return struct( + versions = [version], + extra_target_triples = toolchain.get("targets", []), + dev_components = "rustc-dev" in toolchain.get("components", []), + ) diff --git a/test/unit/rust_toolchain_file/BUILD.bazel b/test/unit/rust_toolchain_file/BUILD.bazel new file mode 100644 index 0000000000..afd09dcffb --- /dev/null +++ b/test/unit/rust_toolchain_file/BUILD.bazel @@ -0,0 +1,4 @@ +load(":rust_toolchain_file_test.bzl", "rust_toolchain_file_test_suite") + +############################ UNIT TESTS ############################# +rust_toolchain_file_test_suite(name = "rust_toolchain_file_test_suite") diff --git a/test/unit/rust_toolchain_file/rust_toolchain_file_test.bzl b/test/unit/rust_toolchain_file/rust_toolchain_file_test.bzl new file mode 100644 index 0000000000..18dbfa0137 --- /dev/null +++ b/test/unit/rust_toolchain_file/rust_toolchain_file_test.bzl @@ -0,0 +1,61 @@ +"""Unit tests for rust_toolchain_file.bzl.""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") + +# buildifier: disable=bzl-visibility +load("//rust/private:rust_toolchain_file.bzl", "parse_rust_toolchain_file") + +def _stable_release_test_impl(ctx): + env = unittest.begin(ctx) + parsed = parse_rust_toolchain_file("""\ +# What cargo and rustup read. +[toolchain] +channel = "1.85.0" +components = ["rustfmt", "clippy"] +""") + asserts.equals(env, ["1.85.0"], parsed.versions) + asserts.equals(env, [], parsed.extra_target_triples) + asserts.false(env, parsed.dev_components) + return unittest.end(env) + +def _dated_nightly_test_impl(ctx): + env = unittest.begin(ctx) + parsed = parse_rust_toolchain_file("""\ +[toolchain] +channel = "nightly-2025-01-01" +components = [ + "rustc-dev", + "rustfmt", +] +targets = ["wasm32-unknown-unknown", "aarch64-unknown-linux-gnu"] +""") + asserts.equals(env, ["nightly/2025-01-01"], parsed.versions) + asserts.equals(env, ["wasm32-unknown-unknown", "aarch64-unknown-linux-gnu"], parsed.extra_target_triples) + asserts.true(env, parsed.dev_components) + return unittest.end(env) + +def _dated_beta_test_impl(ctx): + env = unittest.begin(ctx) + parsed = parse_rust_toolchain_file("""\ +[toolchain] +channel = "beta-2025-01-01" +""") + asserts.equals(env, ["beta/2025-01-01"], parsed.versions) + return unittest.end(env) + +stable_release_test = unittest.make(_stable_release_test_impl) +dated_nightly_test = unittest.make(_dated_nightly_test_impl) +dated_beta_test = unittest.make(_dated_beta_test_impl) + +def rust_toolchain_file_test_suite(name): + """Entry-point macro called from the BUILD file. + + Args: + name (str): Name of the macro. + """ + unittest.suite( + name, + stable_release_test, + dated_nightly_test, + dated_beta_test, + )