tests: add integration test for prebuilt exe_zip_maker toolchain (#4309)

Verify that the prebuilt toolchains infrastructure resolves and uses a
prebuilt exe_zip_maker binary when rules_python is consumed in release
mode (`TOOL_DEPS_DEV_ONLY = True`).

The test stages a release-like copy of rules_python with a populated
`artifacts_manifest.json` pointing to a locally built `exe_zip_maker`
binary, then runs a nested Bzlmod workspace with
`--override_module=rules_python` to confirm `py_binary` zipapps build
and execute using the prebuilt toolchain.

Work towards #4250
Work towards #4216
diff --git a/.bazelignore b/.bazelignore
index 88a4c32..8a3cbf9 100644
--- a/.bazelignore
+++ b/.bazelignore
@@ -33,6 +33,7 @@
 tests/integration/bzlmod_lockfile/bazel-bzlmod_lockfile
 tests/integration/compile_pip_requirements/bazel-compile_pip_requirements
 tests/integration/local_toolchains/bazel-local_toolchains
+tests/integration/prebuilt_toolchain/bazel-prebuilt_toolchain
 tests/integration/py_cc_toolchain_registered/bazel-py_cc_toolchain_registered
 tests/integration/toolchain_target_settings/bazel-module_under_test
 tests/integration/unified_pypi/bazel-unified_pypi
diff --git a/.bazelrc.deleted_packages b/.bazelrc.deleted_packages
index 3eec282..d0c8d40 100644
--- a/.bazelrc.deleted_packages
+++ b/.bazelrc.deleted_packages
@@ -38,6 +38,7 @@
 common --deleted_packages=tests/integration/pip_parse
 common --deleted_packages=tests/integration/pip_parse/empty
 common --deleted_packages=tests/integration/pip_parse_isolated
+common --deleted_packages=tests/integration/prebuilt_toolchain
 common --deleted_packages=tests/integration/py_cc_toolchain_registered
 common --deleted_packages=tests/integration/runtime_manifests
 common --deleted_packages=tests/integration/toolchain_target_settings
diff --git a/tests/integration/BUILD.bazel b/tests/integration/BUILD.bazel
index 2eae8df..6c5d248 100644
--- a/tests/integration/BUILD.bazel
+++ b/tests/integration/BUILD.bazel
@@ -15,7 +15,7 @@
 load("@rules_bazel_integration_test//bazel_integration_test:defs.bzl", "default_test_runner")
 load("//python:py_binary.bzl", "py_binary")
 load("//python:py_library.bzl", "py_library")
-load("//tests/support:support.bzl", "NOT_WINDOWS")
+load("//tests/support:support.bzl", "NOT_WINDOWS", "SUPPORTS_BZLMOD")
 load(":integration_test.bzl", "rules_python_integration_test")
 
 licenses(["notice"])
@@ -116,6 +116,14 @@
 )
 
 rules_python_integration_test(
+    name = "prebuilt_toolchain_test",
+    py_data = ["//crates/exe_zip_maker"],
+    py_deps = ["//python/runfiles"],
+    py_main = "prebuilt_toolchain_test.py",
+    target_compatible_with = SUPPORTS_BZLMOD + NOT_WINDOWS,
+)
+
+rules_python_integration_test(
     name = "toolchain_target_settings_test",
     py_main = "toolchain_target_settings_test.py",
 )
diff --git a/tests/integration/integration_test.bzl b/tests/integration/integration_test.bzl
index f3d5cb6..fec4a89 100644
--- a/tests/integration/integration_test.bzl
+++ b/tests/integration/integration_test.bzl
@@ -21,13 +21,14 @@
 )
 load("//python:py_test.bzl", "py_test")
 
-def _test_runner(*, name, bazel_version, py_main, bzlmod, py_deps):
+def _test_runner(*, name, bazel_version, py_main, bzlmod, py_deps, py_data):
     if py_main:
         test_runner = "{}_bazel_{}_py_runner".format(name, bazel_version)
         py_test(
             name = test_runner,
             srcs = [py_main],
             main = py_main,
+            data = py_data,
             deps = [":runner_lib"] + py_deps,
             # Hide from ... patterns; should only be run as part
             # of the bazel integration test
@@ -47,6 +48,7 @@
         tags = None,
         py_main = None,
         py_deps = None,
+        py_data = None,
         bazel_versions = None,
         **kwargs):
     """Runs a bazel-in-bazel integration test.
@@ -62,11 +64,15 @@
             python based test runner is used, and this source file is the main
             entry point and responsible for executing tests.
         py_deps: Optional test runner deps to use for setup.
+        py_data: Optional test runner data files to use for setup.
         bazel_versions: `list[str] | None`, the bazel versions to test. I
             not specified, defaults to all configured bazel versions.
         **kwargs: Passed to the upstream `bazel_integration_tests` rule.
     """
     workspace_path = workspace_path or name.removesuffix("_test")
+    py_deps = py_deps or []
+    py_data = py_data or []
+    tags = tags or []
 
     # Because glob expansion happens at loading time, the bazel-* symlinks
     # in the workspaces can recursively expand to tens-of-thousands of entries,
@@ -93,7 +99,8 @@
             name = name,
             bazel_version = bazel_version,
             py_main = py_main,
-            py_deps = py_deps or [],
+            py_deps = py_deps,
+            py_data = py_data,
             bzlmod = bzlmod,
         )
         bazel_integration_test(
@@ -103,7 +110,7 @@
             bazel_version = bazel_version,
             workspace_files = [name + "_workspace_files"],
             # Override the tags so that the `manual` tag isn't applied.
-            tags = (tags or []) + [
+            tags = tags + [
                 # These tests are very heavy weight, so much so that only a couple
                 # can be run in parallel without harming their reliability,
                 # overall runtime, and the system's stability. Unfortunately,
diff --git a/tests/integration/prebuilt_toolchain/.bazelrc b/tests/integration/prebuilt_toolchain/.bazelrc
new file mode 100644
index 0000000..e1df951
--- /dev/null
+++ b/tests/integration/prebuilt_toolchain/.bazelrc
@@ -0,0 +1,3 @@
+common --lockfile_mode=off
+test --test_output=errors
+build --enable_runfiles
diff --git a/tests/integration/prebuilt_toolchain/BUILD.bazel b/tests/integration/prebuilt_toolchain/BUILD.bazel
new file mode 100644
index 0000000..18c2f0d
--- /dev/null
+++ b/tests/integration/prebuilt_toolchain/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@rules_python//python:py_binary.bzl", "py_binary")
+load("@rules_python//python/zipapp:py_zipapp_test.bzl", "py_zipapp_test")
+
+py_binary(
+    name = "main",
+    srcs = ["main.py"],
+    config_settings = {
+        "@rules_python//python/config_settings:bootstrap_impl": "script",
+    },
+)
+
+py_zipapp_test(
+    name = "zipapp_test",
+    binary = ":main",
+)
diff --git a/tests/integration/prebuilt_toolchain/MODULE.bazel b/tests/integration/prebuilt_toolchain/MODULE.bazel
new file mode 100644
index 0000000..edf2283
--- /dev/null
+++ b/tests/integration/prebuilt_toolchain/MODULE.bazel
@@ -0,0 +1,10 @@
+module(name = "prebuilt_toolchain")
+
+bazel_dep(name = "rules_python", version = "0.0.0")
+local_path_override(
+    module_name = "rules_python",
+    path = "../../..",
+)
+
+python = use_extension("@rules_python//python/extensions:python.bzl", "python")
+python.toolchain(python_version = "3.13")
diff --git a/tests/integration/prebuilt_toolchain/WORKSPACE b/tests/integration/prebuilt_toolchain/WORKSPACE
new file mode 100644
index 0000000..d7be0c9
--- /dev/null
+++ b/tests/integration/prebuilt_toolchain/WORKSPACE
@@ -0,0 +1 @@
+# Intentionally blank; bzlmod is used.
diff --git a/tests/integration/prebuilt_toolchain/main.py b/tests/integration/prebuilt_toolchain/main.py
new file mode 100644
index 0000000..6359ecc
--- /dev/null
+++ b/tests/integration/prebuilt_toolchain/main.py
@@ -0,0 +1 @@
+print("Hello from prebuilt exe_zip_maker zipapp")
diff --git a/tests/integration/prebuilt_toolchain_test.py b/tests/integration/prebuilt_toolchain_test.py
new file mode 100644
index 0000000..ab12a9c
--- /dev/null
+++ b/tests/integration/prebuilt_toolchain_test.py
@@ -0,0 +1,98 @@
+"""Integration test for prebuilt tool toolchain configuration."""
+
+import hashlib
+import json
+import shutil
+import unittest
+
+from python.runfiles import runfiles
+from tests.integration import runner
+
+_USE_RUST_FLAG = "--@rules_python//python/private/toolchains:use_rust_exe_zip_maker=yes"
+
+
+class PrebuiltToolchainTest(runner.TestCase):
+    def setUp(self):
+        super().setUp()
+        rf = runfiles.CreateOrRaise()
+        exe_zip_maker = (
+            rf.root() / "rules_python/crates/exe_zip_maker/exe_zip_maker"
+        ).resolve()
+        self.assertTrue(
+            exe_zip_maker.is_file(),
+            f"Expected exe_zip_maker binary at {exe_zip_maker}",
+        )
+        sha256 = hashlib.sha256(exe_zip_maker.read_bytes()).hexdigest()
+
+        orig_rules_python = self.repo_root.parent.parent.parent
+        self.rules_python_dir = self.test_tmp_dir / "rules_python"
+        shutil.copytree(
+            orig_rules_python,
+            self.rules_python_dir,
+            ignore=shutil.ignore_patterns("tests", "bazel-*"),
+        )
+
+        # Simulate a release archive where tool dependencies (such as
+        # rules_rust) are dev-only and prebuilt toolchains are configured via
+        # artifacts_manifest.json.
+        module_bazel = self.rules_python_dir / "MODULE.bazel"
+        module_content = module_bazel.read_text(encoding="utf-8")
+        self.assertIn("\nTOOL_DEPS_DEV_ONLY = False\n", module_content)
+        module_bazel.write_text(
+            module_content.replace(
+                "\nTOOL_DEPS_DEV_ONLY = False\n",
+                "\nTOOL_DEPS_DEV_ONLY = True\n",
+            ),
+            encoding="utf-8",
+        )
+
+        manifest = {
+            "artifacts": {
+                "exe_zip_maker": [
+                    {
+                        "exec_compatible_with": [
+                            "@platforms//os:windows",
+                        ],
+                        "name": "exe_zip_maker_unmatched",
+                        "sha256": "0" * 64,
+                        "urls": ["https://invalid.example.com/should-not-be-fetched"],
+                    },
+                    {
+                        "exec_compatible_with": [],
+                        "name": "exe_zip_maker_prebuilt",
+                        "sha256": sha256,
+                        "urls": [exe_zip_maker.as_uri()],
+                    },
+                ],
+            },
+        }
+        manifest_path = self.rules_python_dir / "python/private/artifacts_manifest.json"
+        manifest_path.write_text(
+            json.dumps(manifest, indent=2) + "\n",
+            encoding="utf-8",
+        )
+
+    def test_prebuilt_exe_zip_maker_toolchain(self):
+        override_flag = f"--override_module=rules_python={self.rules_python_dir}"
+
+        self.run_bazel(
+            "test",
+            override_flag,
+            _USE_RUST_FLAG,
+            "//:zipapp_test",
+        )
+
+        aquery_res = self.run_bazel(
+            "aquery",
+            override_flag,
+            _USE_RUST_FLAG,
+            "mnemonic(PyZipAppCreateExecutableZip, //:zipapp_test)",
+        )
+        self.assert_result_matches(
+            aquery_res,
+            r"rules_python_exe_zip_maker_prebuilt_file/file/downloaded",
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()