diff --git a/.bazelignore b/.bazelignore index 0b25ffe44..8aaeb6a16 100644 --- a/.bazelignore +++ b/.bazelignore @@ -5,3 +5,4 @@ crate_universe/private/bootstrap test/bzlmod_repo_mapping test/cc_common_link test/no_std +.direnv \ No newline at end of file diff --git a/docs/flatten.md b/docs/flatten.md index 334b50708..e9a222cc8 100644 --- a/docs/flatten.md +++ b/docs/flatten.md @@ -1132,7 +1132,7 @@ Run the test with `bazel test //hello_lib:greeting_test`. ## rust_toolchain
-rust_toolchain(name, allocator_library, binary_ext, cargo, clippy_driver, debug_info,
+rust_toolchain(name, allocator_library, binary_ext, cargo, cargo_clippy, clippy_driver, debug_info,
                default_edition, dylib_ext, env, exec_triple, experimental_link_std_dylib,
                experimental_use_cc_common_link, extra_exec_rustc_flags, extra_rustc_flags,
                extra_rustc_flags_for_crate_types, global_allocator_library, llvm_cov, llvm_profdata,
@@ -1192,6 +1192,7 @@ See `@rules_rust//rust:repositories.bzl` for examples of defining the `@rust_cpu
 | allocator_library |  Target that provides allocator functions when rust_library targets are embedded in a cc_binary.   | Label | optional |  `"@rules_rust//ffi/cc/allocator_library"`  |
 | binary_ext |  The extension for binaries created from rustc.   | String | required |  |
 | cargo |  The location of the `cargo` binary. Can be a direct source or a filegroup containing one item.   | Label | optional |  `None`  |
+| cargo_clippy |  The location of the `cargo_clippy` binary. Can be a direct source or a filegroup containing one item.   | Label | optional |  `None`  |
 | clippy_driver |  The location of the `clippy-driver` binary. Can be a direct source or a filegroup containing one item.   | Label | optional |  `None`  |
 | debug_info |  Rustc debug info levels per opt level   | Dictionary: String -> String | optional |  `{"dbg": "2", "fastbuild": "0", "opt": "0"}`  |
 | default_edition |  The edition to use for rust_* rules that don't specify an edition. If absent, every rule is required to specify its `edition` attribute.   | String | optional |  `""`  |
diff --git a/docs/rust_repositories.md b/docs/rust_repositories.md
index 729e4ba1e..f815779ef 100644
--- a/docs/rust_repositories.md
+++ b/docs/rust_repositories.md
@@ -36,7 +36,7 @@ A dedicated filegroup-like rule for Rust stdlib artifacts.
 ## rust_toolchain
 
 
-rust_toolchain(name, allocator_library, binary_ext, cargo, clippy_driver, debug_info,
+rust_toolchain(name, allocator_library, binary_ext, cargo, cargo_clippy, clippy_driver, debug_info,
                default_edition, dylib_ext, env, exec_triple, experimental_link_std_dylib,
                experimental_use_cc_common_link, extra_exec_rustc_flags, extra_rustc_flags,
                extra_rustc_flags_for_crate_types, global_allocator_library, llvm_cov, llvm_profdata,
@@ -96,6 +96,7 @@ See `@rules_rust//rust:repositories.bzl` for examples of defining the `@rust_cpu
 | allocator_library |  Target that provides allocator functions when rust_library targets are embedded in a cc_binary.   | Label | optional |  `"@rules_rust//ffi/cc/allocator_library"`  |
 | binary_ext |  The extension for binaries created from rustc.   | String | required |  |
 | cargo |  The location of the `cargo` binary. Can be a direct source or a filegroup containing one item.   | Label | optional |  `None`  |
+| cargo_clippy |  The location of the `cargo_clippy` binary. Can be a direct source or a filegroup containing one item.   | Label | optional |  `None`  |
 | clippy_driver |  The location of the `clippy-driver` binary. Can be a direct source or a filegroup containing one item.   | Label | optional |  `None`  |
 | debug_info |  Rustc debug info levels per opt level   | Dictionary: String -> String | optional |  `{"dbg": "2", "fastbuild": "0", "opt": "0"}`  |
 | default_edition |  The edition to use for rust_* rules that don't specify an edition. If absent, every rule is required to specify its `edition` attribute.   | String | optional |  `""`  |
diff --git a/examples/.bazelignore b/examples/.bazelignore
index e8cae6593..5c00b61cb 100644
--- a/examples/.bazelignore
+++ b/examples/.bazelignore
@@ -1,4 +1,5 @@
 android
+bazel_env
 bzlmod
 cargo_manifest_dir/external_crate
 crate_universe
diff --git a/examples/bazel_env/.bazelignore b/examples/bazel_env/.bazelignore
new file mode 100644
index 000000000..ff51edffc
--- /dev/null
+++ b/examples/bazel_env/.bazelignore
@@ -0,0 +1 @@
+.direnv
\ No newline at end of file
diff --git a/examples/bazel_env/.bazelrc b/examples/bazel_env/.bazelrc
new file mode 100644
index 000000000..f5a104e0d
--- /dev/null
+++ b/examples/bazel_env/.bazelrc
@@ -0,0 +1,15 @@
+# Required on windows
+common --enable_platform_specific_config
+startup --windows_enable_symlinks
+build:windows --enable_runfiles
+
+build --experimental_enable_bzlmod
+
+# This isn't currently the defaut in Bazel, but we enable it to test we'll be ready if/when it flips.
+build --incompatible_disallow_empty_glob
+
+# Required for cargo_build_script support before Bazel 7
+build --incompatible_merge_fixed_and_default_shell_env
+
+# Do not import the PATH etc. from the host environment
+common --incompatible_strict_action_env
diff --git a/examples/bazel_env/.envrc b/examples/bazel_env/.envrc
new file mode 100644
index 000000000..4722fbe14
--- /dev/null
+++ b/examples/bazel_env/.envrc
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# ^^^ make IDEs happy
+
+watch_file bazel-out/bazel_env-opt/bin/env/env/bin
+PATH_add bazel-out/bazel_env-opt/bin/env/env/bin
+if [[ ! -d bazel-out/bazel_env-opt/bin/env/env/bin ]]; then
+  log_error "ERROR[bazel_env.bzl]: Run 'bazel run //env:env' to regenerate bazel-out/bazel_env-opt/bin/env/env/bin"
+fi
\ No newline at end of file
diff --git a/examples/bazel_env/.gitignore b/examples/bazel_env/.gitignore
new file mode 100644
index 000000000..a6ef824c1
--- /dev/null
+++ b/examples/bazel_env/.gitignore
@@ -0,0 +1 @@
+/bazel-*
diff --git a/examples/bazel_env/BUILD.bazel b/examples/bazel_env/BUILD.bazel
new file mode 100644
index 000000000..0d1b02883
--- /dev/null
+++ b/examples/bazel_env/BUILD.bazel
@@ -0,0 +1,50 @@
+"Tests for upstream wrappers."
+
+sh_test(
+    name = "upstream_cargo_test",
+    size = "small",
+    srcs = ["cargo_test.sh"],
+    args = [
+        "$(rlocationpath @rules_rust//tools/upstream_wrapper:cargo)",
+    ],
+    data = [
+        "Cargo.lock",
+        "Cargo.toml",
+        "//rust/hello_world:Cargo.toml",
+        "//rust/hello_world:src/main.rs",
+        "@rules_rust//tools/upstream_wrapper:cargo",
+    ],
+    deps = [
+        "@bazel_tools//tools/bash/runfiles",
+    ],
+)
+
+sh_test(
+    name = "upstream_rustc_test",
+    size = "small",
+    srcs = ["rustc_test.sh"],
+    args = [
+        "$(rlocationpath @rules_rust//tools/upstream_wrapper:rustc)",
+    ],
+    data = [
+        "@rules_rust//tools/upstream_wrapper:rustc",
+    ],
+    deps = [
+        "@bazel_tools//tools/bash/runfiles",
+    ],
+)
+
+sh_test(
+    name = "upstream_rustfmt_test",
+    size = "small",
+    srcs = ["rustfmt_test.sh"],
+    args = [
+        "$(rlocationpath @rules_rust//tools/upstream_wrapper:rustfmt)",
+    ],
+    data = [
+        "@rules_rust//tools/upstream_wrapper:rustfmt",
+    ],
+    deps = [
+        "@bazel_tools//tools/bash/runfiles",
+    ],
+)
diff --git a/examples/bazel_env/Cargo.lock b/examples/bazel_env/Cargo.lock
new file mode 100644
index 000000000..c2e8cbe09
--- /dev/null
+++ b/examples/bazel_env/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "hello_world"
+version = "0.0.0"
diff --git a/examples/bazel_env/Cargo.toml b/examples/bazel_env/Cargo.toml
new file mode 100644
index 000000000..35e6b18f1
--- /dev/null
+++ b/examples/bazel_env/Cargo.toml
@@ -0,0 +1,5 @@
+[workspace]
+resolver = "2"
+members = [
+    "rust/hello_world"
+]
\ No newline at end of file
diff --git a/examples/bazel_env/MODULE.bazel b/examples/bazel_env/MODULE.bazel
new file mode 100644
index 000000000..e4e934075
--- /dev/null
+++ b/examples/bazel_env/MODULE.bazel
@@ -0,0 +1,44 @@
+"""bazelbuild/rules_rust - bazel_env/bzlmod example
+
+See https://github.com/buildbuddy-io/bazel_env.bzl.
+"""
+
+module(
+    name = "all_crate_deps_bzlmod_example",
+    version = "0.0.0",
+)
+
+bazel_dep(name = "platforms", version = "0.0.9")
+bazel_dep(
+    name = "rules_rust",
+    version = "0.0.0",
+)
+local_path_override(
+    module_name = "rules_rust",
+    path = "../..",
+)
+
+rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
+rust.toolchain(edition = "2021")
+use_repo(
+    rust,
+    "rust_toolchains",
+)
+
+register_toolchains("@rust_toolchains//:all")
+
+crate = use_extension(
+    "@rules_rust//crate_universe:extension.bzl",
+    "crate",
+)
+crate.from_cargo(
+    name = "crates",
+    cargo_lockfile = "//:Cargo.lock",
+    manifests = [
+        "//:Cargo.toml",
+        "//rust/hello_world:Cargo.toml",
+    ],
+)
+use_repo(crate, "crates")
+
+bazel_dep(name = "bazel_env.bzl", version = "0.1.1")
diff --git a/examples/bazel_env/README.md b/examples/bazel_env/README.md
new file mode 100644
index 000000000..34550116e
--- /dev/null
+++ b/examples/bazel_env/README.md
@@ -0,0 +1,11 @@
+# rules_rust with bazel_env
+
+This example uses [bazel_env.bzl](https://github.com/buildbuddy-io/bazel_env.bzl) to
+provide `cargo`, `cargo-clippy`, `rustc` and `rustfmt` from the bazel toolchains
+to the user. They will be available directly in the PATH when the user
+enters this directory and has [direnv](https://direnv.net/) set up.
+
+Advantages:
+
+- The user doesn't have to install the toolchains themselves.
+- The tool versions will always match exactly those that bazel uses.
diff --git a/examples/bazel_env/WORKSPACE.bazel b/examples/bazel_env/WORKSPACE.bazel
new file mode 100644
index 000000000..b543b79e9
--- /dev/null
+++ b/examples/bazel_env/WORKSPACE.bazel
@@ -0,0 +1 @@
+# Intentionally blank; using bzlmod
diff --git a/examples/bazel_env/WORKSPACE.bzlmod b/examples/bazel_env/WORKSPACE.bzlmod
new file mode 100644
index 000000000..8e081c0b5
--- /dev/null
+++ b/examples/bazel_env/WORKSPACE.bzlmod
@@ -0,0 +1 @@
+# Intentionally blank; enable strict mode for bzlmod
diff --git a/examples/bazel_env/cargo_test.sh b/examples/bazel_env/cargo_test.sh
new file mode 100755
index 000000000..664067aea
--- /dev/null
+++ b/examples/bazel_env/cargo_test.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+
+# --- begin runfiles.bash initialization v3 ---
+# Copy-pasted from the Bazel Bash runfiles library v3.
+set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
+source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
+  source "$0.runfiles/$f" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+  { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
+# --- end runfiles.bash initialization v3 ---
+
+
+set -euo pipefail
+
+# MARK - Functions
+
+fail() {
+  echo >&2 "$@"
+  exit 1
+}
+
+# MARK - Args
+
+if [[ "$#" -lt 1 ]]; then
+  fail "Usage: $0 /path/to/bin "
+fi
+CARGO="$(rlocation "$1")"
+
+# MARK - Test
+
+# simulate bazel run with setting BUILD_WORKING_DIRECTORY
+export BUILD_WORKING_DIRECTORY="$PWD" 
+
+# Run hello_world, invoking rustc
+OUTPUT="$("${CARGO}" run)"
+
+EXPECTED_OUTPUT="Hello, world!"
+[[ "${OUTPUT}" == "${EXPECTED_OUTPUT}" ]] ||
+  fail 'Expected "'"${EXPECTED_OUTPUT}"'", but was' "${OUTPUT}"
+
+# Crash tests, no error
+
+${CARGO} clippy ||
+  fail "couldn't execute \"cargo clippy\""
diff --git a/examples/bazel_env/env/BUILD.bazel b/examples/bazel_env/env/BUILD.bazel
new file mode 100644
index 000000000..0c64a5883
--- /dev/null
+++ b/examples/bazel_env/env/BUILD.bazel
@@ -0,0 +1,13 @@
+load("@bazel_env.bzl", "bazel_env")
+
+package(default_visibility = ["//visibility:public"])
+
+bazel_env(
+    name = "env",
+    tools = {
+        "cargo": "@rules_rust//tools/upstream_wrapper:cargo",
+        "cargo-clippy": "@rules_rust//tools/upstream_wrapper:cargo_clippy",
+        "rustc": "@rules_rust//tools/upstream_wrapper:rustc",
+        "rustfmt": "@rules_rust//tools/upstream_wrapper:rustfmt",
+    },
+)
diff --git a/examples/bazel_env/hello b/examples/bazel_env/hello
new file mode 100755
index 000000000..182a8addb
Binary files /dev/null and b/examples/bazel_env/hello differ
diff --git a/examples/bazel_env/rust/hello_world/BUILD.bazel b/examples/bazel_env/rust/hello_world/BUILD.bazel
new file mode 100644
index 000000000..0f5efa03f
--- /dev/null
+++ b/examples/bazel_env/rust/hello_world/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@crates//:defs.bzl", "all_crate_deps")
+load("@rules_rust//rust:defs.bzl", "rust_binary")
+
+package(default_visibility = ["//visibility:public"])
+
+exports_files([
+    "Cargo.toml",
+    "src/main.rs",
+])
+
+rust_binary(
+    name = "hello_world",
+    srcs = ["src/main.rs"],
+    deps = all_crate_deps(normal = True),
+)
diff --git a/examples/bazel_env/rust/hello_world/Cargo.toml b/examples/bazel_env/rust/hello_world/Cargo.toml
new file mode 100644
index 000000000..755a8dd14
--- /dev/null
+++ b/examples/bazel_env/rust/hello_world/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "hello_world"
+version = "0.0.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "/dev/null"
diff --git a/examples/bazel_env/rust/hello_world/src/main.rs b/examples/bazel_env/rust/hello_world/src/main.rs
new file mode 100644
index 000000000..317f56458
--- /dev/null
+++ b/examples/bazel_env/rust/hello_world/src/main.rs
@@ -0,0 +1,17 @@
+// Copyright 2015 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+fn main() {
+    println!("Hello, world!");
+}
diff --git a/examples/bazel_env/rustc_test.sh b/examples/bazel_env/rustc_test.sh
new file mode 100755
index 000000000..ecef60919
--- /dev/null
+++ b/examples/bazel_env/rustc_test.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+
+# --- begin runfiles.bash initialization v3 ---
+# Copy-pasted from the Bazel Bash runfiles library v3.
+set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
+source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
+  source "$0.runfiles/$f" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+  { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
+# --- end runfiles.bash initialization v3 ---
+
+
+set -euo pipefail
+
+# MARK - Functions
+
+fail() {
+  echo >&2 "$@"
+  exit 1
+}
+
+# MARK - Args
+
+if [[ "$#" -lt 1 ]]; then
+  fail "Usage: $0 /path/to/bin "
+fi
+RUSTC="$(rlocation "$1")"
+
+# MARK - Test
+DIR=$(mktemp -d "rustc-test-XXXXXXX")
+
+set -x
+echo "fn main() {}" >"$DIR/main.rs"
+cd "$DIR"
+# simulate bazel run with setting BUILD_WORKING_DIRECTORY
+export BUILD_WORKING_DIRECTORY="$PWD" 
+"$RUSTC" "main.rs" || 
+  fail "Couldn't compile main.rs"
diff --git a/examples/bazel_env/rustfmt_test.sh b/examples/bazel_env/rustfmt_test.sh
new file mode 100755
index 000000000..177201077
--- /dev/null
+++ b/examples/bazel_env/rustfmt_test.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+# --- begin runfiles.bash initialization v3 ---
+# Copy-pasted from the Bazel Bash runfiles library v3.
+set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
+source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
+  source "$0.runfiles/$f" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+  source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
+  { echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
+# --- end runfiles.bash initialization v3 ---
+
+
+set -euo pipefail
+
+# MARK - Functions
+
+fail() {
+  echo >&2 "$@"
+  exit 1
+}
+
+# MARK - Args
+
+if [[ "$#" -lt 1 ]]; then
+  fail "Usage: $0 /path/to/bin "
+fi
+RUSTFMT="$(rlocation "$1")"
+
+# MARK - Test
+
+# simulate bazel run with setting BUILD_WORKING_DIRECTORY
+export BUILD_WORKING_DIRECTORY="$PWD" 
+
+OUTPUT="$(echo -e "fn main() {\n\n}" | "$RUSTFMT")"
+
+# without newlines in body
+EXPECTED_OUTPUT="fn main() {}"
+[[ "${OUTPUT}" == "${EXPECTED_OUTPUT}" ]] ||
+  fail 'Expected "'"${EXPECTED_OUTPUT}"'", but was' "${OUTPUT}"
\ No newline at end of file
diff --git a/rust/private/repository_utils.bzl b/rust/private/repository_utils.bzl
index 67f1cd0aa..15d11e683 100644
--- a/rust/private/repository_utils.bzl
+++ b/rust/private/repository_utils.bzl
@@ -122,14 +122,6 @@ def BUILD_for_rustfmt(target_triple):
         binary_ext = system_to_binary_ext(target_triple.system),
     )
 
-_build_file_for_clippy_template = """\
-filegroup(
-    name = "clippy_driver_bin",
-    srcs = ["bin/clippy-driver{binary_ext}"],
-    visibility = ["//visibility:public"],
-)
-"""
-
 _build_file_for_rust_analyzer_proc_macro_srv = """\
 filegroup(
    name = "rust_analyzer_proc_macro_srv",
@@ -150,6 +142,19 @@ def BUILD_for_rust_analyzer_proc_macro_srv(exec_triple):
         binary_ext = system_to_binary_ext(exec_triple.system),
     )
 
+_build_file_for_clippy_template = """\
+filegroup(
+    name = "clippy_driver_bin",
+    srcs = ["bin/clippy-driver{binary_ext}"],
+    visibility = ["//visibility:public"],
+)
+filegroup(
+    name = "cargo_clippy_bin",
+    srcs = ["bin/cargo-clippy{binary_ext}"],
+    visibility = ["//visibility:public"],
+)
+"""
+
 def BUILD_for_clippy(target_triple):
     """Emits a BUILD file the clippy archive.
 
@@ -244,6 +249,7 @@ rust_toolchain(
     rustfmt = {rustfmt_label},
     cargo = "//:cargo",
     clippy_driver = "//:clippy_driver_bin",
+    cargo_clippy = "//:cargo_clippy_bin",
     llvm_cov = {llvm_cov_label},
     llvm_profdata = {llvm_profdata_label},
     rustc_lib = "//:rustc_lib",
diff --git a/rust/private/toolchain_utils.bzl b/rust/private/toolchain_utils.bzl
index 64f759f50..078e6f34c 100644
--- a/rust/private/toolchain_utils.bzl
+++ b/rust/private/toolchain_utils.bzl
@@ -13,6 +13,16 @@ def _toolchain_files_impl(ctx):
             ],
             transitive_files = toolchain.rustc_lib,
         )
+    elif ctx.attr.tool == "cargo-clippy":
+        files = depset([toolchain.cargo_clippy])
+        runfiles = ctx.runfiles(
+            files = [
+                toolchain.cargo_clippy,
+                toolchain.clippy_driver,
+                toolchain.rustc,
+            ],
+            transitive_files = toolchain.rustc_lib,
+        )
     elif ctx.attr.tool == "clippy":
         files = depset([toolchain.clippy_driver])
         runfiles = ctx.runfiles(
@@ -60,6 +70,7 @@ toolchain_files = rule(
             doc = "The desired tool to get form the current rust_toolchain",
             values = [
                 "cargo",
+                "cargo-clippy",
                 "clippy",
                 "rust_lib",
                 "rust_std",
diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl
index b10ddb1c8..ae3fcefeb 100644
--- a/rust/toolchain.bzl
+++ b/rust/toolchain.bzl
@@ -377,6 +377,7 @@ def _generate_sysroot(
         rustc_lib,
         cargo = None,
         clippy = None,
+        cargo_clippy = None,
         llvm_tools = None,
         rust_std = None,
         rustfmt = None):
@@ -388,6 +389,7 @@ def _generate_sysroot(
         rustdoc (File): The path to a `rustdoc` executable.
         rustc_lib (Target): A collection of Files containing dependencies of `rustc`.
         cargo (File, optional): The path to a `cargo` executable.
+        cargo_clippy (File, optional): The path to a `cargo-clippy` executable.
         clippy (File, optional): The path to a `clippy-driver` executable.
         llvm_tools (Target, optional): A collection of llvm tools used by `rustc`.
         rust_std (Target, optional): A collection of Files containing Rust standard library components.
@@ -428,6 +430,12 @@ def _generate_sysroot(
         sysroot_cargo = _symlink_sysroot_bin(ctx, name, "bin", cargo)
         direct_files.extend([sysroot_cargo])
 
+    # Cargo-clippy
+    sysroot_cargo_clippy = None
+    if cargo_clippy:
+        sysroot_cargo_clippy = _symlink_sysroot_bin(ctx, name, "bin", cargo_clippy)
+        direct_files.extend([sysroot_cargo_clippy])
+
     # Rustfmt
     sysroot_rustfmt = None
     if rustfmt:
@@ -453,6 +461,7 @@ def _generate_sysroot(
         content = "\n".join([
             "cargo: {}".format(cargo),
             "clippy: {}".format(clippy),
+            "cargo-clippy: {}".format(cargo_clippy),
             "llvm_tools: {}".format(llvm_tools),
             "rust_std: {}".format(rust_std),
             "rustc_lib: {}".format(rustc_lib),
@@ -469,6 +478,7 @@ def _generate_sysroot(
         all_files = all_files,
         cargo = sysroot_cargo,
         clippy = sysroot_clippy,
+        cargo_clippy = sysroot_cargo_clippy,
         rust_std = sysroot_rust_std,
         rustc = sysroot_rustc,
         rustc_lib = sysroot_rustc_lib,
@@ -529,6 +539,7 @@ def _rust_toolchain_impl(ctx):
         rustfmt = ctx.file.rustfmt,
         clippy = ctx.file.clippy_driver,
         cargo = ctx.file.cargo,
+        cargo_clippy = ctx.file.cargo_clippy,
         llvm_tools = ctx.attr.llvm_tools,
     )
 
@@ -571,6 +582,7 @@ def _rust_toolchain_impl(ctx):
         "RUSTDOC": sysroot.rustdoc.path,
         "RUST_DEFAULT_EDITION": ctx.attr.default_edition or "",
         "RUST_SYSROOT": sysroot_path,
+        "RUST_SYSROOT_SHORT": sysroot_short_path,
     }
 
     if sysroot.cargo:
@@ -631,6 +643,7 @@ def _rust_toolchain_impl(ctx):
         binary_ext = ctx.attr.binary_ext,
         cargo = sysroot.cargo,
         clippy_driver = sysroot.clippy,
+        cargo_clippy = sysroot.cargo_clippy,
         compilation_mode_opts = compilation_mode_opts,
         crosstool_files = ctx.files._cc_toolchain,
         default_edition = ctx.attr.default_edition,
@@ -702,6 +715,11 @@ rust_toolchain = rule(
             allow_single_file = True,
             cfg = "exec",
         ),
+        "cargo_clippy": attr.label(
+            doc = "The location of the `cargo_clippy` binary. Can be a direct source or a filegroup containing one item.",
+            allow_single_file = True,
+            cfg = "exec",
+        ),
         "clippy_driver": attr.label(
             doc = "The location of the `clippy-driver` binary. Can be a direct source or a filegroup containing one item.",
             allow_single_file = True,
diff --git a/rust/toolchain/BUILD.bazel b/rust/toolchain/BUILD.bazel
index b5e57af79..c839c9136 100644
--- a/rust/toolchain/BUILD.bazel
+++ b/rust/toolchain/BUILD.bazel
@@ -13,6 +13,11 @@ toolchain_files(
     tool = "clippy",
 )
 
+toolchain_files(
+    name = "current_cargo_clippy_files",
+    tool = "cargo-clippy",
+)
+
 toolchain_files(
     name = "current_rustc_files",
     tool = "rustc",
diff --git a/tools/rustfmt/BUILD.bazel b/tools/rustfmt/BUILD.bazel
index c8aeb81a6..ecbc1a9d2 100644
--- a/tools/rustfmt/BUILD.bazel
+++ b/tools/rustfmt/BUILD.bazel
@@ -36,7 +36,7 @@ rust_library(
 alias(
     name = "rustfmt",
     actual = ":target_aware_rustfmt",
-    deprecation = "Prefer //tools/rustfmt:target_aware_rustfmt",
+    deprecation = "Prefer //tools/upstream_wrapper:rustfmt",
     visibility = ["//visibility:public"],
 )
 
@@ -85,20 +85,10 @@ rust_clippy(
     ],
 )
 
-# This is a small wrapper around the raw upstream rustfmt binary which can be `bazel run`.
-rust_binary(
+# Deprecated but present for compatibility.
+alias(
     name = "upstream_rustfmt",
-    srcs = [
-        "src/upstream_rustfmt_wrapper.rs",
-    ],
-    data = ["//rust/toolchain:current_rustfmt_toolchain_for_target"],
-    edition = "2018",
-    rustc_env = {
-        "RUSTFMT": "$(rlocationpath //rust/toolchain:current_rustfmt_toolchain_for_target)",
-    },
-    toolchains = ["@rules_rust//rust/toolchain:current_rust_toolchain"],
+    actual = "//tools/upstream_wrapper:rustfmt",
+    deprecation = "Prefer //tools/upstream_wrapper:rustfmt",
     visibility = ["//visibility:public"],
-    deps = [
-        "//tools/runfiles",
-    ],
 )
diff --git a/tools/rustfmt/src/upstream_rustfmt_wrapper.rs b/tools/rustfmt/src/upstream_rustfmt_wrapper.rs
deleted file mode 100644
index 33c091aa1..000000000
--- a/tools/rustfmt/src/upstream_rustfmt_wrapper.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use std::path::PathBuf;
-use std::process::{exit, Command};
-
-fn main() {
-    let runfiles = runfiles::Runfiles::create().unwrap();
-
-    let rustfmt = runfiles::rlocation!(runfiles, env!("RUSTFMT"));
-    if !rustfmt.exists() {
-        panic!("rustfmt does not exist at: {}", rustfmt.display());
-    }
-
-    let working_directory = std::env::var_os("BUILD_WORKING_DIRECTORY")
-        .map(PathBuf::from)
-        .unwrap_or_else(|| std::env::current_dir().expect("Failed to get working directory"));
-
-    let status = Command::new(rustfmt)
-        .current_dir(&working_directory)
-        .args(std::env::args_os().skip(1))
-        .status()
-        .expect("Failed to run rustfmt");
-    if let Some(exit_code) = status.code() {
-        exit(exit_code);
-    }
-    exit_for_signal(&status);
-    panic!("Child rustfmt process didn't exit or get a signal - don't know how to handle it");
-}
-
-#[cfg(target_family = "unix")]
-fn exit_for_signal(status: &std::process::ExitStatus) {
-    use std::os::unix::process::ExitStatusExt;
-    if let Some(signal) = status.signal() {
-        exit(signal);
-    }
-}
-
-#[cfg(not(target_family = "unix"))]
-#[allow(unused)]
-fn exit_for_signal(status: &std::process::ExitStatus) {}
diff --git a/tools/upstream_wrapper/BUILD.bazel b/tools/upstream_wrapper/BUILD.bazel
new file mode 100644
index 000000000..ad9ab7eaf
--- /dev/null
+++ b/tools/upstream_wrapper/BUILD.bazel
@@ -0,0 +1,33 @@
+load("//rust:defs.bzl", "rust_binary")
+
+tools = {
+    "cargo": "//rust/toolchain:current_cargo_files",
+    "cargo_clippy": "//rust/toolchain:current_cargo_clippy_files",
+    "rustc": "//rust/toolchain:current_rustc_files",
+    "rustfmt": "//rust/toolchain:current_rustfmt_toolchain_for_target",
+}
+
+all_tools = [target for target in tools.values()]
+
+[
+    rust_binary(
+        name = tool_name,
+        srcs = [
+            "src/main.rs",
+        ],
+        # Cargo calls out to the other tools.
+        # Make sure that they are included in the runfiles.
+        data = all_tools if tool_name == "cargo" else [target],
+        edition = "2018",
+        rustc_env = {
+            "WRAPPED_TOOL_NAME": tool_name,
+            "WRAPPED_TOOL_TARGET": "$(rlocationpath {})".format(target),
+        },
+        toolchains = ["@rules_rust//rust/toolchain:current_rust_toolchain"],
+        visibility = ["//visibility:public"],
+        deps = [
+            "//tools/runfiles",
+        ],
+    )
+    for (tool_name, target) in tools.items()
+]
diff --git a/tools/upstream_wrapper/README.md b/tools/upstream_wrapper/README.md
new file mode 100644
index 000000000..3b1c8975c
--- /dev/null
+++ b/tools/upstream_wrapper/README.md
@@ -0,0 +1,15 @@
+# upstream_wrapper
+
+Wrap the binaries from the current toolchain so that
+they can be easily invoked with `bazel run`:
+
+```bash
+bazel run @rules_rust//tools/upstream_wrapper:cargo
+bazel run @rules_rust//tools/upstream_wrapper:cargo -- clippy
+bazel run @rules_rust//tools/upstream_wrapper:cargo -- fmt
+bazel run @rules_rust//tools/upstream_wrapper:rustc -- main.rs
+bazel run @rules_rust//tools/upstream_wrapper:rustfmt
+```
+
+Alternatively, look at the [bazel_env example](../../examples/bazel_env/)
+to include them in the users path with direnv.
diff --git a/tools/upstream_wrapper/src/main.rs b/tools/upstream_wrapper/src/main.rs
new file mode 100644
index 000000000..6d6e167c2
--- /dev/null
+++ b/tools/upstream_wrapper/src/main.rs
@@ -0,0 +1,59 @@
+use std::ffi::OsString;
+use std::path::PathBuf;
+use std::process::{exit, Command};
+
+const WRAPPED_TOOL_NAME: &str = env!("WRAPPED_TOOL_NAME");
+const WRAPPED_TOOL_TARGET: &str = env!("WRAPPED_TOOL_TARGET");
+
+#[cfg(not(target_os = "windows"))]
+const PATH_SEPARATOR: &str = ":";
+#[cfg(target_os = "windows")]
+const PATH_SEPARATOR: &str = ";";
+
+fn main() {
+    let runfiles = runfiles::Runfiles::create().unwrap();
+
+    let wrapped_tool_path = runfiles::rlocation!(runfiles, WRAPPED_TOOL_TARGET);
+    if !wrapped_tool_path.exists() {
+        panic!(
+            "{WRAPPED_TOOL_NAME} does not exist at: {}",
+            wrapped_tool_path.display()
+        );
+    }
+
+    let tool_directory = wrapped_tool_path
+        .parent()
+        .expect("parent directory of tool binary");
+    let old_path = std::env::var_os("PATH").unwrap_or_default();
+    let mut new_path = OsString::from(tool_directory);
+    new_path.push(PATH_SEPARATOR);
+    new_path.push(&old_path);
+
+    let working_directory = std::env::var_os("BUILD_WORKING_DIRECTORY")
+        .map(PathBuf::from)
+        .unwrap_or_else(|| std::env::current_dir().expect("Failed to get working directory"));
+
+    let status = Command::new(wrapped_tool_path)
+        .current_dir(&working_directory)
+        .args(std::env::args_os().skip(1))
+        .env("PATH", new_path)
+        .status()
+        .unwrap_or_else(|e| panic!("Failed to run {WRAPPED_TOOL_NAME} {:#}", e));
+    if let Some(exit_code) = status.code() {
+        exit(exit_code);
+    }
+    exit_for_signal(&status);
+    panic!("Child rustfmt process didn't exit or get a signal - don't know how to handle it");
+}
+
+#[cfg(target_family = "unix")]
+fn exit_for_signal(status: &std::process::ExitStatus) {
+    use std::os::unix::process::ExitStatusExt;
+    if let Some(signal) = status.signal() {
+        exit(signal);
+    }
+}
+
+#[cfg(not(target_family = "unix"))]
+#[allow(unused)]
+fn exit_for_signal(status: &std::process::ExitStatus) {}