Add bzlmod support for system_python.

#test-continuous

This creates a module extension for system python so that it can be used in bzlmod builds.  The pip_parse setup (dynamically building a new repo) doesn't work in the new bzlmod model, so we use the regular pip extension pointed at system_python's interpreter.

Remaining work to get off WORKSPACE:
* The pip extension quickly fails in the case where there's no python available, unlike our system_python setup
* Our windows upb tests were never actually running python, and system_python doesn't work (and breaks under bzlmod due to the above issue)
* Some of the upb wheel tests still don't work on bzlmod yet because they depend on some very custom http_archive dependencies.

Closes #18750
Closes #23313
Closes #23307

PiperOrigin-RevId: 810995270
diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml
index 40e49cf..420482b 100644
--- a/.github/workflows/test_python.yml
+++ b/.github/workflows/test_python.yml
@@ -29,8 +29,6 @@
       matrix:
         type: [ Pure, C++]
         version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
-        # TODO: Enable bzlmod once python headers are supported for python dist.
-        bzlmod: [--noenable_bzlmod]
         include:
           - type: Pure
             targets: //python/... //python:python_version_test
@@ -38,9 +36,12 @@
           - type: C++
             targets: //python/... //python:python_version_test
             flags: --define=use_fast_cpp_protos=true
+            # Test using WORKSPACE with our oldest support Python version.
           - version: "3.9"
+            nobzlmod: true
+            # Our 3.9 image has non-hermetic issues and can't be rebuilt.  Use the old version.
+            image: us-docker.pkg.dev/protobuf-build/containers/test/linux/python:7.6.1-3.9-12e21b8dda91028bc14212a3ab582c7c4d149fac
           - version: "3.10"
-            continuous-only: true
           - version: "3.11"
             continuous-only: true
           - version: "3.12"
@@ -51,10 +52,9 @@
             targets: //python/... //python:aarch64_test
             # TODO Enable this once conformance tests are fixed.
             flags: --define=use_fast_cpp_protos=true --test_tag_filters=-conformance
-            bzlmod: --noenable_bzlmod
             image: us-docker.pkg.dev/protobuf-build/containers/test/linux/emulation:7.6.1-aarch64-f0d1e209ed9369f69d93ce418990ecff3aa08d6f
 
-    name: ${{ matrix.continuous-only && inputs.continuous-prefix || '' }} Linux ${{ matrix.type }} ${{ matrix.version }}
+    name: ${{ matrix.continuous-only && inputs.continuous-prefix || '' }} Linux ${{ matrix.type }} ${{ matrix.version }} ${{ matrix.nobzlmod && 'No Bzlmod' || '' }}
     runs-on: ubuntu-latest
     steps:
       - name: Checkout pending changes
@@ -66,10 +66,10 @@
         if: ${{ !matrix.continuous-only || inputs.continuous-run }}
         uses: protocolbuffers/protobuf-ci/bazel-docker@v4
         with:
-          image: ${{ matrix.image || format('us-docker.pkg.dev/protobuf-build/containers/test/linux/python:7.6.1-{0}-12e21b8dda91028bc14212a3ab582c7c4d149fac', matrix.version) }}
+          image: ${{ matrix.image || format('us-docker.pkg.dev/protobuf-build/containers/test/linux/python:7.6.1-{0}-e1c2fe666ffa9b941b126541a26340d0ea787cea', matrix.version) }}
           credentials: ${{ secrets.GAR_SERVICE_ACCOUNT }}
           bazel-cache: python_linux/${{ matrix.type }}_${{ matrix.version }}
-          bazel: test ${{ matrix.targets }} ${{ matrix.flags }} ${{ matrix.bzlmod }} --test_env=KOKORO_PYTHON_VERSION
+          bazel: test ${{ matrix.targets }} ${{ matrix.flags }} ${{ matrix.nobzlmod && '--noenable_bzlmod' || '' }} --test_env=KOKORO_PYTHON_VERSION
 
 
   macos:
@@ -78,8 +78,6 @@
       matrix:
         type: [ Pure, C++]
         version: [ "3.12", "3.13" ]
-        # TODO: Enable bzlmod once python headers are supported for python dist.
-        bzlmod: [--noenable_bzlmod]
         include:
           - type: Pure
             targets: //python/... //python:python_version_test
@@ -121,6 +119,6 @@
           credentials: ${{ secrets.GAR_SERVICE_ACCOUNT }}
           bazel-cache: python_macos/${{ matrix.type }}_${{ matrix.version }}
           bazel: >-
-            test ${{ matrix.targets }} ${{ matrix.flags }} ${{ matrix.bzlmod }}
+            test ${{ matrix.targets }} ${{ matrix.flags }}
             --test_env=KOKORO_PYTHON_VERSION=${{ matrix.version }}
             --macos_minimum_os=11.0
diff --git a/.github/workflows/test_upb.yml b/.github/workflows/test_upb.yml
index 52d942d..3f65de6 100644
--- a/.github/workflows/test_upb.yml
+++ b/.github/workflows/test_upb.yml
@@ -52,8 +52,7 @@
           image: us-docker.pkg.dev/protobuf-build/containers/test/linux/sanitize:${{ matrix.config.bazel_version || '7.6.1' }}-12e21b8dda91028bc14212a3ab582c7c4d149fac
           credentials: ${{ secrets.GAR_SERVICE_ACCOUNT }}
           bazel-cache: upb-bazel
-          # TODO: Enable bzlmod once python headers are supported for python dist.
-          bazel: test --noenable_bzlmod //bazel/... //benchmarks/... //lua/... //python/... //upb/... //upb_generator/... ${{ matrix.config.flags }}
+          bazel: test //bazel/... //benchmarks/... //lua/... //python/... //upb/... //upb_generator/... ${{ matrix.config.flags }}
           exclude-targets: ${{ matrix.config.exclude-targets }}
 
   linux-gcc:
@@ -73,7 +72,7 @@
           credentials: ${{ secrets.GAR_SERVICE_ACCOUNT }}
           bazel-cache: "upb-bazel-gcc"
           bazel: >-
-            test --noenable_bzlmod -c opt
+            test -c opt
             --copt="-Wno-error=maybe-uninitialized" --java_runtime_version=remotejdk_11
             //bazel/... //benchmarks/... //lua/... //python/... //upb/... //upb_generator/...
 
@@ -92,7 +91,7 @@
         with:
           credentials: ${{ secrets.GAR_SERVICE_ACCOUNT }}
           bazel-cache: "upb-bazel-windows"
-          bazel: test --noenable_bzlmod //upb/... //upb_generator/... //python/...
+          bazel: test //upb/... //upb_generator/... //python/... --noenable_bzlmod
           version: 7.6.1
           exclude-targets: -//python:conformance_test -//upb/reflection:def_builder_test
 
@@ -121,7 +120,7 @@
         with:
           credentials: ${{ secrets.GAR_SERVICE_ACCOUNT }}
           bazel-cache: "upb-bazel-macos"
-          bazel: ${{ matrix.config.bazel-command }} --noenable_bzlmod ${{ matrix.config.flags }} //bazel/... //benchmarks/... //lua/... //python/... //upb/... //upb_generator/...
+          bazel: ${{ matrix.config.bazel-command }} ${{ matrix.config.flags }} //bazel/... //benchmarks/... //lua/... //python/... //upb/... //upb_generator/...
           version: 7.6.1
 
   no-python:
diff --git a/MODULE.bazel b/MODULE.bazel
index 322a304..827c35a 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -123,29 +123,25 @@
     "3.10",
     "3.11",
     "3.12",
+    "3.13",
 ]
 
-# TODO: Support hermetic / system python in bzlmod.
-python = use_extension("@rules_python//python/extensions:python.bzl", "python")
-
-[
-    python.toolchain(
-        python_version = python_version,
-    )
-    for python_version in SUPPORTED_PYTHON_VERSIONS
-]
-
-python.defaults(python_version = SUPPORTED_PYTHON_VERSIONS[-1])
-use_repo(
-    python,
-    system_python = "python_{}".format(SUPPORTED_PYTHON_VERSIONS[-1].replace(".", "_")),
+# TODO: Replace system_python with hermetic_python.
+# TODO: Remove dev_dependency once system_python is no longer used and we support
+# python/upb in Bazel.
+system_python = use_extension("//python/dist:system_python.bzl", "system_python_extension", dev_dependency = True)
+system_python.find(
+    name = "system_python",
+    minimum = "3.9",
 )
+use_repo(system_python, "system_python")
 
 pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True)
 
 [
     pip.parse(
         hub_name = "protobuf_pip_deps",
+        python_interpreter_target = "@system_python//:interpreter",
         python_version = python_version,
         requirements_lock = "//python:requirements.txt",
     )
diff --git a/cmake/dependencies_generator.py b/cmake/dependencies_generator.py
index 875a7f0..22a9e53 100644
--- a/cmake/dependencies_generator.py
+++ b/cmake/dependencies_generator.py
@@ -46,6 +46,9 @@
   def bundle_fetch(self, *args, **kwargs):
     pass
 
+  def find(self, *args, **kwargs):
+    pass
+
 
 def empty_func(*args, **kwargs):
   pass
diff --git a/python/dist/system_python.bzl b/python/dist/system_python.bzl
index 5f0fdf5..2a1c564 100644
--- a/python/dist/system_python.bzl
+++ b/python/dist/system_python.bzl
@@ -69,8 +69,10 @@
 """
 
 _build_file = """
+load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
 load("@bazel_skylib//lib:selects.bzl", "selects")
 load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
+load("@rules_python//python:py_runtime.bzl", "py_runtime")
 load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair")
 
 cc_library(
@@ -269,3 +271,23 @@
         "minimum_python_version": attr.string(default = "3.9"),
     },
 )
+
+def _system_python_extension(ctx):
+    for mod in ctx.modules:
+        for py in mod.tags.find:
+            system_python(
+                name = py.name,
+                minimum_python_version = py.minimum,
+            )
+
+find = tag_class(attrs = {
+    "name": attr.string(doc = "Supported versions of python to find"),
+    "minimum": attr.string(),
+})
+
+system_python_extension = module_extension(
+    implementation = _system_python_extension,
+    tag_classes = {
+        "find": find,
+    },
+)