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",