fix: Avoid C++ toolchain requirement if possible (#2919)

By making use of the new `launcher_maker_toolchain` in Bazel 9,
rules_python can avoid the requirement for a C++ toolchain targeting the
target platform if that platform isn't Windows.

For example, this makes it possible to cross-compile pure Python targets
from one Unix to another. Since Java targets have a dependency on Python
targets through the `proguard_allowlister`, this also allows Java
targets to be built without any C++ toolchain.
diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl
index d5192ec..b208037 100644
--- a/python/private/internal_config_repo.bzl
+++ b/python/private/internal_config_repo.bzl
@@ -32,6 +32,7 @@
   enable_pystar = True,
   enable_pipstar = {enable_pipstar},
   enable_deprecation_warnings = {enable_deprecation_warnings},
+  bazel_9_or_later = {bazel_9_or_later},
   BuiltinPyInfo = getattr(getattr(native, "legacy_globals", None), "PyInfo", {builtin_py_info_symbol}),
   BuiltinPyRuntimeInfo = getattr(getattr(native, "legacy_globals", None), "PyRuntimeInfo", {builtin_py_runtime_info_symbol}),
   BuiltinPyCcLinkParamsProvider = getattr(getattr(native, "legacy_globals", None), "PyCcLinkParamsProvider", {builtin_py_cc_link_params_provider}),
@@ -87,7 +88,10 @@
 """
 
 def _internal_config_repo_impl(rctx):
-    if not native.bazel_version or int(native.bazel_version.split(".")[0]) >= 8:
+    # An empty version signifies a development build, which is treated as
+    # the latest version.
+    bazel_major_version = int(native.bazel_version.split(".")[0]) if native.bazel_version else 99999
+    if bazel_major_version >= 8:
         builtin_py_info_symbol = "None"
         builtin_py_runtime_info_symbol = "None"
         builtin_py_cc_link_params_provider = "None"
@@ -103,6 +107,7 @@
         builtin_py_info_symbol = builtin_py_info_symbol,
         builtin_py_runtime_info_symbol = builtin_py_runtime_info_symbol,
         builtin_py_cc_link_params_provider = builtin_py_cc_link_params_provider,
+        bazel_9_or_later = str(bazel_major_version >= 9),
     ))
 
     shim_content = _PY_INTERNAL_SHIM
diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl
index 669951e..99a3dff 100644
--- a/python/private/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -18,6 +18,7 @@
 load("@bazel_skylib//lib:structs.bzl", "structs")
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
+load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
 load(":attr_builders.bzl", "attrb")
 load(
     ":attributes.bzl",
@@ -69,6 +70,7 @@
 _py_builtins = py_internal
 _EXTERNAL_PATH_PREFIX = "external"
 _ZIP_RUNFILES_DIRECTORY_NAME = "runfiles"
+_LAUNCHER_MAKER_TOOLCHAIN_TYPE = "@bazel_tools//tools/launcher:launcher_maker_toolchain_type"
 
 # Non-Google-specific attributes for executables
 # These attributes are for rules that accept Python sources.
@@ -228,17 +230,19 @@
                 "@platforms//os:windows",
             ],
         ),
-        "_windows_launcher_maker": lambda: attrb.Label(
-            default = "@bazel_tools//tools/launcher:launcher_maker",
-            cfg = "exec",
-            executable = True,
-        ),
         "_zipper": lambda: attrb.Label(
             cfg = "exec",
             executable = True,
             default = "@bazel_tools//tools/zip:zipper",
         ),
     },
+    {
+        "_windows_launcher_maker": lambda: attrb.Label(
+            default = "@bazel_tools//tools/launcher:launcher_maker",
+            cfg = "exec",
+            executable = True,
+        ),
+    } if not rp_config.bazel_9_or_later else {},
 )
 
 def convert_legacy_create_init_to_int(kwargs):
@@ -777,6 +781,11 @@
         is_executable = True,
     )
 
+def _find_launcher_maker(ctx):
+    if rp_config.bazel_9_or_later:
+        return (ctx.toolchains[_LAUNCHER_MAKER_TOOLCHAIN_TYPE].binary, _LAUNCHER_MAKER_TOOLCHAIN_TYPE)
+    return (ctx.executable._windows_launcher_maker, None)
+
 def _create_windows_exe_launcher(
         ctx,
         *,
@@ -796,8 +805,9 @@
     launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s")
 
     launcher = ctx.attr._launcher[DefaultInfo].files_to_run.executable
+    executable, toolchain = _find_launcher_maker(ctx)
     ctx.actions.run(
-        executable = ctx.executable._windows_launcher_maker,
+        executable = executable,
         arguments = [launcher.path, launch_info, output.path],
         inputs = [launcher],
         outputs = [output],
@@ -805,6 +815,7 @@
         progress_message = "Creating launcher for %{label}",
         # Needed to inherit PATH when using non-MSVC compilers like MinGW
         use_default_shell_env = True,
+        toolchain = toolchain,
     )
 
 def _create_zip_file(ctx, *, output, zip_main, runfiles):
@@ -1838,7 +1849,7 @@
             ruleb.ToolchainType(TOOLCHAIN_TYPE),
             ruleb.ToolchainType(EXEC_TOOLS_TOOLCHAIN_TYPE, mandatory = False),
             ruleb.ToolchainType("@bazel_tools//tools/cpp:toolchain_type", mandatory = False),
-        ],
+        ] + ([ruleb.ToolchainType(_LAUNCHER_MAKER_TOOLCHAIN_TYPE)] if rp_config.bazel_9_or_later else []),
         cfg = dict(
             implementation = _transition_executable_impl,
             inputs = TRANSITION_LABELS + [labels.PYTHON_VERSION],
diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl
index e41bc2c..ed1a550 100644
--- a/tests/base_rules/py_executable_base_tests.bzl
+++ b/tests/base_rules/py_executable_base_tests.bzl
@@ -14,6 +14,7 @@
 """Tests common to py_binary and py_test (executable rules)."""
 
 load("@rules_python//python:py_runtime_info.bzl", RulesPythonPyRuntimeInfo = "PyRuntimeInfo")
+load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
 load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
 load("@rules_testing//lib:truth.bzl", "matching")
 load("@rules_testing//lib:util.bzl", rt_util = "util")
@@ -114,6 +115,29 @@
 
 _tests.append(_test_basic_zip)
 
+def _test_cross_compile_to_unix(name, config):
+    rt_util.helper_target(
+        config.rule,
+        name = name + "_subject",
+        main_module = "dummy",
+    )
+    analysis_test(
+        name = name,
+        impl = _test_cross_compile_to_unix_impl,
+        target = name + "_subject",
+        # Cross-compilation of py_test fails since the default test toolchain
+        # requires an execution platform that matches the target platform.
+        config_settings = {
+            "//command_line_option:platforms": [platform_targets.EXOTIC_UNIX],
+        } if rp_config.bazel_9_or_later and not "py_test" in str(config.rule) else {},
+        expect_failure = True,
+    )
+
+def _test_cross_compile_to_unix_impl(_env, _target):
+    pass
+
+_tests.append(_test_cross_compile_to_unix)
+
 def _test_executable_in_runfiles(name, config):
     rt_util.helper_target(
         config.rule,
diff --git a/tests/support/platforms/BUILD.bazel b/tests/support/platforms/BUILD.bazel
index 41d7936..eeb7ccb 100644
--- a/tests/support/platforms/BUILD.bazel
+++ b/tests/support/platforms/BUILD.bazel
@@ -75,3 +75,11 @@
         "@platforms//cpu:aarch64",
     ],
 )
+
+platform(
+    name = "exotic_unix",
+    constraint_values = [
+        "@platforms//os:linux",
+        "@platforms//cpu:s390x",
+    ],
+)
diff --git a/tests/support/platforms/platforms.bzl b/tests/support/platforms/platforms.bzl
index af049f2..92a1d61 100644
--- a/tests/support/platforms/platforms.bzl
+++ b/tests/support/platforms/platforms.bzl
@@ -10,4 +10,8 @@
     WINDOWS = Label("//tests/support/platforms:windows"),
     WINDOWS_AARCH64 = Label("//tests/support/platforms:windows_aarch64"),
     WINDOWS_X86_64 = Label("//tests/support/platforms:windows_x86_64"),
+
+    # Unspecified Unix platform that is unlikely to be the host platform in CI,
+    # but still provides a Python toolchain.
+    EXOTIC_UNIX = Label("//tests/support/platforms:exotic_unix"),
 )