feat: add runtime_env toolchain suite to replace "autodetecting" toolchain (#2018)

This adds a more comprehensive replacement for the "autodetecting"
toolchain. Specifically, it defines all our toolchain types so that they
take precedence when specified. This prevents the hermetic toolchains
(registered by default) from accidentally being used when undesired.

To keep the behavior backwards compatible, an alias is added for the
autodetecting toolchain with a deprecation notice.

The name `runtime_env` was chosen instead of "autodetecting" so that
it's more clear these
toolchains are not "automatic" or "detecting" anything -- they're just
taking a value
from the runtime environment and using it.

---------

Co-authored-by: Ignas Anikevicius <240938+aignas@users.noreply.github.com>
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 9fd7cae..b778ac4 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -182,6 +182,7 @@
     platform: rbe_ubuntu1604
     test_flags:
       - "--test_tag_filters=-integration-test,-acceptance-test"
+      - "--extra_toolchains=@buildkite_config//config:cc-toolchain"
 
   integration_test_build_file_generation_ubuntu_minimum_supported_workspace:
     <<: *minimum_supported_version
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9dc5ec4..1fe53e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -31,6 +31,9 @@
 * (toolchains) The exec tools toolchain now finds its interpreter by reusing
   the regular interpreter toolchain. This avoids having to duplicate specifying
   where the runtime for the exec tools toolchain is.
+* (toolchains) ({obj}`//python:autodetecting_toolchain`) is deprecated. It is
+  replaced by {obj}`//python/runtime_env_toolchains:all`. The old target will be
+  removed in a future release.
 
 ### Fixed
 * (bzlmod): Targets in `all_requirements` now use the same form as targets returned by the `requirement` macro.
@@ -47,6 +50,10 @@
 * (rules) The first element of the default outputs is now the executable again.
 * (pip) Fixed crash when pypi packages lacked a sha (e.g. yanked packages)
 
+### Added
+* (toolchains) {obj}`//python/runtime_env_toolchains:all`, which is a drop-in
+  replacement for the "autodetecting" toolchain.
+
 ### Removed
 * (pip): Removes the `entrypoint` macro that was replaced by `py_console_script_binary` in 0.26.0.
 
diff --git a/docs/sphinx/api/python/index.md b/docs/sphinx/api/python/index.md
index 494e7b4..6c79447 100644
--- a/docs/sphinx/api/python/index.md
+++ b/docs/sphinx/api/python/index.md
@@ -26,18 +26,11 @@
 
 ::::{target} autodetecting_toolchain
 
-A simple toolchain that simply uses `python3` from the runtime environment.
+Legacy toolchain; despite its name, it doesn't autodetect anything.
 
-Note that this toolchain provides no build-time information, which makes it of
-limited utility.
+:::{deprecated} 0.34.0
 
-This is only provided to aid migration off the builtin Bazel toolchain 
-(`@bazel_tools//python:autodetecting_toolchain`), and is largely only applicable
-to WORKSPACE builds.
-
-:::{deprecated} unspecified
-
-Switch to using a hermetic toolchain or manual toolchain configuration instead.
+Use {obj}`@rules_python//python/runtime_env_toolchain:all` instead.
 :::
-
 ::::
+
diff --git a/docs/sphinx/api/python/runtime_env_toolchains/index.md b/docs/sphinx/api/python/runtime_env_toolchains/index.md
new file mode 100644
index 0000000..ef31f08
--- /dev/null
+++ b/docs/sphinx/api/python/runtime_env_toolchains/index.md
@@ -0,0 +1,38 @@
+:::{default-domain} bzl
+:::
+:::{bzl:currentfile} //python/runtime_env_toolchain:BUILD.bazel
+:::
+
+# //python/runtime_env_toolchain
+
+::::{target} all
+
+A set of toolchains that invoke `python3` from the runtime environment.
+
+Note that this toolchain provides no build-time information, which makes it of
+limited utility. This is because the invocation of `python3` is done when a
+program is run, not at build time.
+
+This is only provided to aid migration off the builtin Bazel toolchain 
+(`@bazel_tools//python:autodetecting_toolchain`), and is largely only applicable
+to WORKSPACE builds.
+
+To use this target, register it as a toolchain in WORKSPACE or MODULE.bazel:
+
+:::
+register_toolchains("@rules_python//python/runtime_env_toolchains:all")
+:::
+
+The benefit of this target over the legacy targets is this defines additional
+toolchain types that rules_python needs. This prevents toolchain resolution from
+continuing to search elsewhere (e.g. potentially incurring a download of the
+hermetic runtimes when they won't be used).
+
+:::{deprecated} 0.34.0
+
+Switch to using a hermetic toolchain or manual toolchain configuration instead.
+:::
+
+:::{versionadded} 0.34.0
+:::
+::::
diff --git a/docs/sphinx/toolchains.md b/docs/sphinx/toolchains.md
index 26557ca..fac1bfc 100644
--- a/docs/sphinx/toolchains.md
+++ b/docs/sphinx/toolchains.md
@@ -240,5 +240,5 @@
 there is a toolchain misconfiguration somewhere.
 
 To aid migration off the Bazel-builtin toolchain, rules_python provides
-{obj}`@rules_python//python:autodetecting_toolchain`. This is an equivalent
+{obj}`@rules_python//python/runtime_env_toolchains:all`. This is an equivalent
 toolchain, but is implemented using rules_python's objects.
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index 96b2282..7a69ac8 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -24,7 +24,6 @@
 """
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
-load("//python/private:autodetecting_toolchain.bzl", "define_autodetecting_toolchain")
 load(":current_py_toolchain.bzl", "current_py_toolchain")
 
 package(default_visibility = ["//visibility:public"])
@@ -320,11 +319,16 @@
 # safe if you know for a fact that your build is completely compatible with the
 # version of the `python` command installed on the target platform.
 
-define_autodetecting_toolchain(name = "autodetecting_toolchain")
+alias(
+    name = "autodetecting_toolchain",
+    actual = "//python/runtime_env_toolchains:runtime_env_toolchain",
+    deprecation = "Use //python/runtime_env_toolchains:all instead",
+)
 
 alias(
     name = "autodetecting_toolchain_nonstrict",
-    actual = ":autodetecting_toolchain",
+    actual = "//python/runtime_env_toolchains:runtime_env_toolchain",
+    deprecation = "Use //python/runtime_env_toolchains:all instead",
 )
 
 # ========= Packaging rules =========
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index ccc6acd..9c759cb 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("@bazel_skylib//rules:common_settings.bzl", "bool_setting")
 load("//python:py_binary.bzl", "py_binary")
 load("//python:py_library.bzl", "py_library")
 load("//python:versions.bzl", "print_toolchains_checksums")
@@ -58,9 +59,10 @@
 )
 
 bzl_library(
-    name = "autodetecting_toolchain_bzl",
-    srcs = ["autodetecting_toolchain.bzl"],
+    name = "runtime_env_toolchain_bzl",
+    srcs = ["runtime_env_toolchain.bzl"],
     deps = [
+        ":py_exec_tools_toolchain_bzl",
         ":toolchain_types_bzl",
         "//python:py_runtime_bzl",
         "//python:py_runtime_pair_bzl",
@@ -140,6 +142,7 @@
         ":py_cc_toolchain_info_bzl",
         ":rules_cc_srcs_bzl",
         ":util_bzl",
+        "@bazel_skylib//rules:common_settings",
     ],
 )
 
@@ -164,7 +167,11 @@
 bzl_library(
     name = "py_exec_tools_toolchain_bzl",
     srcs = ["py_exec_tools_toolchain.bzl"],
-    deps = ["//python/private/common:providers_bzl"],
+    deps = [
+        ":toolchain_types_bzl",
+        "//python/private/common:providers_bzl",
+        "@bazel_skylib//rules:common_settings",
+    ],
 )
 
 bzl_library(
@@ -192,6 +199,7 @@
     deps = [
         "//python:py_runtime_bzl",
         "//python:py_runtime_info_bzl",
+        "@bazel_skylib//rules:common_settings",
     ],
 )
 
@@ -365,6 +373,15 @@
     },
 )
 
+# This should only be set by analysis tests to expose additional metadata to
+# aid testing, so a setting instead of a flag.
+bool_setting(
+    name = "visible_for_testing",
+    build_setting_default = False,
+    # This is only because it is an implicit dependency by the toolchains.
+    visibility = ["//visibility:public"],
+)
+
 print_toolchains_checksums(name = "print_toolchains_checksums")
 
 # Used for py_console_script_gen rule
diff --git a/python/private/autodetecting_toolchain.bzl b/python/private/autodetecting_toolchain.bzl
deleted file mode 100644
index 174136e..0000000
--- a/python/private/autodetecting_toolchain.bzl
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright 2019 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.
-
-"""Definitions related to the Python toolchain."""
-
-load("//python:py_runtime.bzl", "py_runtime")
-load("//python:py_runtime_pair.bzl", "py_runtime_pair")
-load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
-
-def define_autodetecting_toolchain(name):
-    """Defines the autodetecting Python toolchain.
-
-    Args:
-        name: The name of the toolchain to introduce. Must have value
-            "autodetecting_toolchain". This param is present only to make the
-            BUILD file more readable.
-    """
-    if name != "autodetecting_toolchain":
-        fail("Python autodetecting toolchain must be named " +
-             "'autodetecting_toolchain'")
-
-    # buildifier: disable=native-py
-    py_runtime(
-        name = "_autodetecting_py3_runtime",
-        interpreter = "//python/private:autodetecting_toolchain_interpreter.sh",
-        python_version = "PY3",
-        stub_shebang = "#!/usr/bin/env python3",
-        visibility = ["//visibility:private"],
-    )
-
-    # This is a dummy runtime whose interpreter_path triggers the native rule
-    # logic to use the legacy behavior on Windows.
-    # TODO(#7844): Remove this target.
-    # buildifier: disable=native-py
-    py_runtime(
-        name = "_magic_sentinel_runtime",
-        interpreter_path = "/_magic_pyruntime_sentinel_do_not_use",
-        python_version = "PY3",
-        visibility = ["//visibility:private"],
-    )
-
-    py_runtime_pair(
-        name = "_autodetecting_py_runtime_pair",
-        py3_runtime = select({
-            # If we're on windows, inject the sentinel to tell native rule logic
-            # that we attempted to use the autodetecting toolchain and need to
-            # switch back to legacy behavior.
-            # TODO(#7844): Remove this hack.
-            "@platforms//os:windows": ":_magic_sentinel_runtime",
-            "//conditions:default": ":_autodetecting_py3_runtime",
-        }),
-        visibility = ["//visibility:public"],
-    )
-
-    native.toolchain(
-        name = name,
-        toolchain = ":_autodetecting_py_runtime_pair",
-        toolchain_type = TARGET_TOOLCHAIN_TYPE,
-        visibility = ["//visibility:public"],
-    )
diff --git a/python/private/py_cc_toolchain_rule.bzl b/python/private/py_cc_toolchain_rule.bzl
index 5d3debb..1599415 100644
--- a/python/private/py_cc_toolchain_rule.bzl
+++ b/python/private/py_cc_toolchain_rule.bzl
@@ -18,6 +18,7 @@
 https://github.com/bazelbuild/rules_python/issues/824 is considered done.
 """
 
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("@rules_cc//cc:defs.bzl", "CcInfo")
 load(":py_cc_toolchain_info.bzl", "PyCcToolchainInfo")
 
@@ -37,8 +38,12 @@
         ),
         python_version = ctx.attr.python_version,
     )
+    extra_kwargs = {}
+    if ctx.attr._visible_for_testing[BuildSettingInfo].value:
+        extra_kwargs["toolchain_label"] = ctx.label
     return [platform_common.ToolchainInfo(
         py_cc_toolchain = py_cc_toolchain,
+        **extra_kwargs
     )]
 
 py_cc_toolchain = rule(
@@ -60,6 +65,9 @@
             doc = "The Major.minor Python version, e.g. 3.11",
             mandatory = True,
         ),
+        "_visible_for_testing": attr.label(
+            default = "//python/private:visible_for_testing",
+        ),
     },
     doc = """\
 A toolchain for a Python runtime's C/C++ information (e.g. headers)
diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl
index 5c17b89..b3d0fb2 100644
--- a/python/private/py_exec_tools_toolchain.bzl
+++ b/python/private/py_exec_tools_toolchain.bzl
@@ -14,14 +14,22 @@
 
 """Rule that defines a toolchain for build tools."""
 
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
 load(":py_exec_tools_info.bzl", "PyExecToolsInfo")
 
 def _py_exec_tools_toolchain_impl(ctx):
-    return [platform_common.ToolchainInfo(exec_tools = PyExecToolsInfo(
-        exec_interpreter = ctx.attr.exec_interpreter,
-        precompiler = ctx.attr.precompiler,
-    ))]
+    extra_kwargs = {}
+    if ctx.attr._visible_for_testing[BuildSettingInfo].value:
+        extra_kwargs["toolchain_label"] = ctx.label
+
+    return [platform_common.ToolchainInfo(
+        exec_tools = PyExecToolsInfo(
+            exec_interpreter = ctx.attr.exec_interpreter,
+            precompiler = ctx.attr.precompiler,
+        ),
+        **extra_kwargs
+    )]
 
 py_exec_tools_toolchain = rule(
     implementation = _py_exec_tools_toolchain_impl,
@@ -36,6 +44,9 @@
             cfg = "exec",
             doc = "See PyExecToolsInfo.precompiler",
         ),
+        "_visible_for_testing": attr.label(
+            default = "//python/private:visible_for_testing",
+        ),
     },
 )
 
diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl
index 02f9a5b..eb91413 100644
--- a/python/private/py_runtime_pair_rule.bzl
+++ b/python/private/py_runtime_pair_rule.bzl
@@ -14,6 +14,7 @@
 
 """Implementation of py_runtime_pair."""
 
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("//python:py_runtime_info.bzl", "PyRuntimeInfo")
 load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
 load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")
@@ -40,9 +41,14 @@
     #     fail("Using Python 2 is not supported and disabled; see " +
     #          "https://github.com/bazelbuild/bazel/issues/15684")
 
+    extra_kwargs = {}
+    if ctx.attr._visible_for_testing[BuildSettingInfo].value:
+        extra_kwargs["toolchain_label"] = ctx.label
+
     return [platform_common.ToolchainInfo(
         py2_runtime = py2_runtime,
         py3_runtime = py3_runtime,
+        **extra_kwargs
     )]
 
 def _get_py_runtime_info(target):
@@ -85,6 +91,9 @@
 `PY3`.
 """,
         ),
+        "_visible_for_testing": attr.label(
+            default = "//python/private:visible_for_testing",
+        ),
     },
     fragments = ["py"],
     doc = """\
diff --git a/python/private/runtime_env_toolchain.bzl b/python/private/runtime_env_toolchain.bzl
new file mode 100644
index 0000000..1601926
--- /dev/null
+++ b/python/private/runtime_env_toolchain.bzl
@@ -0,0 +1,112 @@
+# Copyright 2019 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.
+"""Definitions related to the Python toolchain."""
+
+load("@rules_cc//cc:defs.bzl", "cc_library")
+load("//python:py_runtime.bzl", "py_runtime")
+load("//python:py_runtime_pair.bzl", "py_runtime_pair")
+load("//python/cc:py_cc_toolchain.bzl", "py_cc_toolchain")
+load(":py_exec_tools_toolchain.bzl", "py_exec_tools_toolchain")
+load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "PY_CC_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE")
+
+_IS_EXEC_TOOLCHAIN_ENABLED = Label("//python/config_settings:is_exec_tools_toolchain_enabled")
+
+def define_runtime_env_toolchain(name):
+    """Defines the runtime_env Python toolchain.
+
+    This is a minimal suite of toolchains that provided limited functionality.
+    They're mostly only useful to aid migration off the builtin
+    `@bazel_tools//tools/python:autodetecting_toolchain` toolchain.
+
+    NOTE: This was previously called the "autodetecting" toolchain, but was
+    renamed to better reflect its behavior, since it doesn't autodetect
+    anything.
+
+    Args:
+        name: The name of the toolchain to introduce.
+    """
+    base_name = name.replace("_toolchain", "")
+
+    py_runtime(
+        name = "_runtime_env_py3_runtime",
+        interpreter = "//python/private:runtime_env_toolchain_interpreter.sh",
+        python_version = "PY3",
+        stub_shebang = "#!/usr/bin/env python3",
+        visibility = ["//visibility:private"],
+        tags = ["manual"],
+    )
+
+    # This is a dummy runtime whose interpreter_path triggers the native rule
+    # logic to use the legacy behavior on Windows.
+    # TODO(#7844): Remove this target.
+    py_runtime(
+        name = "_magic_sentinel_runtime",
+        interpreter_path = "/_magic_pyruntime_sentinel_do_not_use",
+        python_version = "PY3",
+        visibility = ["//visibility:private"],
+        tags = ["manual"],
+    )
+
+    py_runtime_pair(
+        name = "_runtime_env_py_runtime_pair",
+        py3_runtime = select({
+            # If we're on windows, inject the sentinel to tell native rule logic
+            # that we attempted to use the runtime_env toolchain and need to
+            # switch back to legacy behavior.
+            # TODO(#7844): Remove this hack.
+            "@platforms//os:windows": ":_magic_sentinel_runtime",
+            "//conditions:default": ":_runtime_env_py3_runtime",
+        }),
+        visibility = ["//visibility:public"],
+        tags = ["manual"],
+    )
+
+    native.toolchain(
+        name = name,
+        toolchain = ":_runtime_env_py_runtime_pair",
+        toolchain_type = TARGET_TOOLCHAIN_TYPE,
+        visibility = ["//visibility:public"],
+    )
+
+    py_exec_tools_toolchain(
+        name = "_runtime_env_py_exec_tools_toolchain_impl",
+        precompiler = Label("//tools/precompiler:precompiler"),
+        visibility = ["//visibility:private"],
+        tags = ["manual"],
+    )
+    native.toolchain(
+        name = base_name + "_py_exec_tools_toolchain",
+        toolchain = "_runtime_env_py_exec_tools_toolchain_impl",
+        toolchain_type = EXEC_TOOLS_TOOLCHAIN_TYPE,
+        target_settings = [_IS_EXEC_TOOLCHAIN_ENABLED],
+        visibility = ["//visibility:public"],
+    )
+    cc_library(
+        name = "_empty_cc_lib",
+        visibility = ["//visibility:private"],
+        tags = ["manual"],
+    )
+    py_cc_toolchain(
+        name = "_runtime_env_py_cc_toolchain_impl",
+        headers = ":_empty_cc_lib",
+        libs = ":_empty_cc_lib",
+        python_version = "0.0",
+        tags = ["manual"],
+    )
+    native.toolchain(
+        name = base_name + "_py_cc_toolchain",
+        toolchain = ":_runtime_env_py_cc_toolchain_impl",
+        toolchain_type = PY_CC_TOOLCHAIN_TYPE,
+        visibility = ["//visibility:public"],
+    )
diff --git a/python/private/autodetecting_toolchain_interpreter.sh b/python/private/runtime_env_toolchain_interpreter.sh
similarity index 93%
rename from python/private/autodetecting_toolchain_interpreter.sh
rename to python/private/runtime_env_toolchain_interpreter.sh
index 5c8a10d..2cb7cc7 100644
--- a/python/private/autodetecting_toolchain_interpreter.sh
+++ b/python/private/runtime_env_toolchain_interpreter.sh
@@ -8,8 +8,8 @@
 # We do lose the ability to set -o pipefail.
 
 FAILURE_HEADER="\
-Error occurred while attempting to use the default Python toolchain \
-(@rules_python//python:autodetecting_toolchain)."
+Error occurred while attempting to use the deprecated Python toolchain \
+(@rules_python//python/runtime_env_toolchain:all)."
 
 die() {
   echo "$FAILURE_HEADER" 1>&2
diff --git a/python/runtime_env_toolchains/BUILD.bazel b/python/runtime_env_toolchains/BUILD.bazel
new file mode 100644
index 0000000..21355ac
--- /dev/null
+++ b/python/runtime_env_toolchains/BUILD.bazel
@@ -0,0 +1,19 @@
+# Copyright 2024 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.
+
+load("//python/private:runtime_env_toolchain.bzl", "define_runtime_env_toolchain")
+
+package(default_visibility = ["//:__subpackages__"])
+
+define_runtime_env_toolchain(name = "runtime_env_toolchain")
diff --git a/tests/base_rules/precompile/precompile_tests.bzl b/tests/base_rules/precompile/precompile_tests.bzl
index 58bdafe..5599f61 100644
--- a/tests/base_rules/precompile/precompile_tests.bzl
+++ b/tests/base_rules/precompile/precompile_tests.bzl
@@ -122,6 +122,7 @@
             "//command_line_option:extra_toolchains": _TEST_TOOLCHAINS,
             ##PRECOMPILE_SOURCE_RETENTION: "omit_source",
             EXEC_TOOLS_TOOLCHAIN: "enabled",
+            PRECOMPILE: "enabled",
         },
         target = name + "_subject",
     )
diff --git a/tests/runtime_env_toolchain/BUILD.bazel b/tests/runtime_env_toolchain/BUILD.bazel
new file mode 100644
index 0000000..ebcdbaf
--- /dev/null
+++ b/tests/runtime_env_toolchain/BUILD.bazel
@@ -0,0 +1,17 @@
+# Copyright 2024 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.
+
+load(":runtime_env_toolchain_tests.bzl", "runtime_env_toolchain_test_suite")
+
+runtime_env_toolchain_test_suite(name = "runtime_env_toolchain_tests")
diff --git a/tests/runtime_env_toolchain/runtime_env_toolchain_tests.bzl b/tests/runtime_env_toolchain/runtime_env_toolchain_tests.bzl
new file mode 100644
index 0000000..9885a1e
--- /dev/null
+++ b/tests/runtime_env_toolchain/runtime_env_toolchain_tests.bzl
@@ -0,0 +1,101 @@
+# Copyright 2024 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.
+
+"""Starlark tests for py_runtime rule."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load(
+    "//python/private:toolchain_types.bzl",
+    "EXEC_TOOLS_TOOLCHAIN_TYPE",
+    "PY_CC_TOOLCHAIN_TYPE",
+    "TARGET_TOOLCHAIN_TYPE",
+)  # buildifier: disable=bzl-visibility
+load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")  # buildifier: disable=bzl-visibility
+load("//tests/support:support.bzl", "CC_TOOLCHAIN", "EXEC_TOOLS_TOOLCHAIN", "VISIBLE_FOR_TESTING")
+
+_LookupInfo = provider()  # buildifier: disable=provider-params
+
+def _use_toolchains_impl(ctx):
+    return [
+        _LookupInfo(
+            target = ctx.toolchains[TARGET_TOOLCHAIN_TYPE],
+            exec = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE],
+            cc = ctx.toolchains[PY_CC_TOOLCHAIN_TYPE],
+        ),
+    ]
+
+_use_toolchains = rule(
+    implementation = _use_toolchains_impl,
+    toolchains = [
+        TARGET_TOOLCHAIN_TYPE,
+        EXEC_TOOLS_TOOLCHAIN_TYPE,
+        PY_CC_TOOLCHAIN_TYPE,
+    ],
+)
+
+_tests = []
+
+def _test_runtime_env_toolchain_matches(name):
+    rt_util.helper_target(
+        _use_toolchains,
+        name = name + "_subject",
+    )
+    extra_toolchains = [
+        str(Label("//python/runtime_env_toolchains:all")),
+    ]
+
+    # We have to add a cc toolchain because py_cc toolchain depends on it.
+    # However, that package also defines a different fake py_cc toolchain we
+    # don't want to use, so we need to ensure the runtime_env toolchain has
+    # higher precendence.
+    # However, Bazel 6 and Bazel 7 process --extra_toolchains in different
+    # orders:
+    #  * Bazel 6 goes left to right
+    #  * Bazel 7 goes right to left
+    # We could just put our preferred toolchain before *and* after
+    # the undesired toolchain...
+    # However, Bazel 7 has a bug where *duplicate* entries are ignored,
+    # and only the *first* entry is respected.
+    if IS_BAZEL_7_OR_HIGHER:
+        extra_toolchains.insert(0, CC_TOOLCHAIN)
+    else:
+        extra_toolchains.append(CC_TOOLCHAIN)
+    analysis_test(
+        name = name,
+        impl = _test_runtime_env_toolchain_matches_impl,
+        target = name + "_subject",
+        config_settings = {
+            "//command_line_option:extra_toolchains": extra_toolchains,
+            EXEC_TOOLS_TOOLCHAIN: "enabled",
+            VISIBLE_FOR_TESTING: True,
+        },
+    )
+
+def _test_runtime_env_toolchain_matches_impl(env, target):
+    env.expect.that_str(
+        str(target[_LookupInfo].target.toolchain_label),
+    ).contains("runtime_env_py_runtime_pair")
+    env.expect.that_str(
+        str(target[_LookupInfo].exec.toolchain_label),
+    ).contains("runtime_env_py_exec_tools")
+    env.expect.that_str(
+        str(target[_LookupInfo].cc.toolchain_label),
+    ).contains("runtime_env_py_cc")
+
+_tests.append(_test_runtime_env_toolchain_matches)
+
+def runtime_env_toolchain_test_suite(name):
+    test_suite(name = name, tests = _tests)
diff --git a/tests/support/support.bzl b/tests/support/support.bzl
index 2e58203..a74346d 100644
--- a/tests/support/support.bzl
+++ b/tests/support/support.bzl
@@ -37,3 +37,4 @@
 PRECOMPILE_SOURCE_RETENTION = str(Label("//python/config_settings:precompile_source_retention"))
 PYC_COLLECTION = str(Label("//python/config_settings:pyc_collection"))
 PYTHON_VERSION = str(Label("//python/config_settings:python_version"))
+VISIBLE_FOR_TESTING = str(Label("//python/private:visible_for_testing"))