Upgrade bazelbuild-rules_python to 0.37.0 am: 8c622fcfa1

Original change: https://android-review.googlesource.com/c/platform/external/bazelbuild-rules_python/+/3403662

Change-Id: If1040443d02f4107742f38e0bb7795b2d7eb52fd
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
deleted file mode 100644
index d37121b..0000000
--- a/.github/workflows/stale.yml
+++ /dev/null
@@ -1,73 +0,0 @@
-# Copyright 2023 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.
-
-# See https://github.com/marketplace/actions/close-stale-issues
-
-name: Mark stale issues and pull requests
-
-on:
-  schedule:
-  # run at 22:45 UTC daily
-  - cron: "45 22 * * *"
-
-jobs:
-  stale:
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/stale@v9
-      with:
-        repo-token: ${{ secrets.GITHUB_TOKEN }}
-
-        # NB: We start with very long duration while trimming existing issues,
-        # with the hope to reduce when/if we get better at keeping up with user support.
-
-        # The number of days old an issue can be before marking it stale.
-        days-before-stale: 180
-        # Number of days of inactivity before a stale issue is closed
-        days-before-close: 30
-
-        # If an issue/PR is assigned, trust the assignee to stay involved
-        # Can revisit if these get stale
-        exempt-all-assignees: true
-        # Issues with these labels will never be considered stale
-        exempt-issue-labels: "need: discussion,cleanup"
-
-        # Label to use when marking an issue as stale
-        stale-issue-label: 'Can Close?'
-        stale-pr-label: 'Can Close?'
-
-        stale-issue-message: >
-          This issue has been automatically marked as stale because it has not had
-          any activity for 180 days.
-          It will be closed if no further activity occurs in 30 days.
-
-          Collaborators can add an assignee to keep this open indefinitely.
-          Thanks for your contributions to rules_python!
-
-        stale-pr-message: >
-          This Pull Request has been automatically marked as stale because it has not had
-          any activity for 180 days.
-          It will be closed if no further activity occurs in 30 days.
-
-          Collaborators can add an assignee to keep this open indefinitely.
-          Thanks for your contributions to rules_python!
-
-        close-issue-message: >
-          This issue was automatically closed because it went 30 days without a reply
-          since it was labeled "Can Close?"
-
-        close-pr-message: >
-          This PR was automatically closed because it went 30 days without a reply
-          since it was labeled "Can Close?"
diff --git a/BUILD.bazel b/BUILD.bazel
index 038b56a..5a58422 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
-load(":version.bzl", "BAZEL_VERSION")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -25,6 +24,11 @@
 ])
 
 exports_files(
+    [".bazelversion"],
+    visibility = ["//tests:__subpackages__"],
+)
+
+exports_files(
     glob(["*.md"]),
     visibility = ["//docs:__subpackages__"],
 )
@@ -69,29 +73,3 @@
     ],
     visibility = ["//visibility:public"],
 )
-
-genrule(
-    name = "assert_bazelversion",
-    srcs = [".bazelversion"],
-    outs = ["assert_bazelversion_test.sh"],
-    cmd = """\
-set -o errexit -o nounset -o pipefail
-current=$$(cat "$(execpath .bazelversion)")
-cat > "$@" <<EOF
-#!/usr/bin/env bash
-set -o errexit -o nounset -o pipefail
-if [[ \"$${{current}}\" != \"{expected}\" ]]; then
-    >&2 echo "ERROR: current bazel version '$${{current}}' is not the expected '{expected}'"
-    exit 1
-fi
-EOF
-""".format(
-        expected = BAZEL_VERSION,
-    ),
-    executable = True,
-)
-
-sh_test(
-    name = "assert_bazelversion_test",
-    srcs = [":assert_bazelversion_test.sh"],
-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ea9565..e428fa3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,19 +22,97 @@
 
 ## Unreleased
 
-[x.x.x]: https://github.com/bazelbuild/rules_python/releases/tag/x.x.x
-
 ### Changed
-* Nothing yet
+- Nothing yet
 
 ### Fixed
-* Nothing yet
+- Nothing yet
 
 ### Added
-* Nothing yet
+- Nothing yet
 
 ### Removed
-* Nothing yet
+- Nothing yet
+
+## [0.37.0] - 2024-10-18
+
+[x.x.x]: https://github.com/bazelbuild/rules_python/releases/tag/0.37.0
+
+### Changed
+* **BREAKING** `py_library` no longer puts its source files or generated pyc
+  files in runfiles; it's the responsibility of consumers (e.g. binaries) to
+  populate runfiles with the necessary files. Adding source files to runfiles
+  can be temporarily restored by setting {obj}`--add_srcs_to_runfiles=enabled`,
+  but this flag will be removed in a subsequent releases.
+* {obj}`PyInfo.transitive_sources` is now added to runfiles. These files are
+  `.py` files that are required to be added to runfiles by downstream binaries
+  (or equivalent).
+* (toolchains) `py_runtime.implementation_name` now defaults to `cpython`
+  (previously it defaulted to None).
+* (toolchains) The exec tools toolchain is enabled by default. It can be
+  disabled by setting
+  {obj}`--@rules_python//python/config_settings:exec_tools_toolchain=disabled`.
+* (deps) stardoc 0.6.2 added as dependency.
+
+### Fixed
+* (bzlmod) The `python.override(minor_mapping)` now merges the default and the
+  overridden versions ensuring that the resultant `minor_mapping` will always
+  have all of the python versions.
+* (bzlmod) The default value for the {obj}`--python_version` flag will now be
+  always set to the default python toolchain version value.
+* (bzlmod) correctly wire the {attr}`pip.parse.extra_pip_args` all the
+  way to {obj}`whl_library`. What is more we will pass the `extra_pip_args` to
+  {obj}`whl_library` for `sdist` distributions when using
+  {attr}`pip.parse.experimental_index_url`. See
+  [#2239](https://github.com/bazelbuild/rules_python/issues/2239).
+* (whl_filegroup): Provide per default also the `RECORD` file
+* (py_wheel): `RECORD` file entry elements are now quoted if necessary when a
+  wheel is created
+* (whl_library) truncate progress messages from the repo rule to better handle
+  case where a requirement has many `--hash=sha256:...` flags
+* (rules) `compile_pip_requirements` passes `env` to the `X.update` target (and
+  not only to the `X_test` target, a bug introduced in
+  [#1067](https://github.com/bazelbuild/rules_python/pull/1067)).
+* (bzlmod) In hybrid bzlmod with WORKSPACE builds,
+  `python_register_toolchains(register_toolchains=True)` is respected
+  ([#1675](https://github.com/bazelbuild/rules_python/issues/1675)).
+* (precompiling) The {obj}`pyc_collection` attribute now correctly
+  enables (or disables) using pyc files from targets transitively
+* (pip) Skip patching wheels not matching `pip.override`'s `file`
+  ([#2294](https://github.com/bazelbuild/rules_python/pull/2294)).
+* (chore): Add a `rules_shell` dev dependency and moved a `sh_test` target
+  outside of the `//:BUILD.bazel` file.
+  Fixes [#2299](https://github.com/bazelbuild/rules_python/issues/2299).
+
+### Added
+* (py_wheel) Now supports `compress = (True|False)` to allow disabling
+  compression to speed up development.
+* (toolchains): A public `//python/config_settings:python_version_major_minor` has
+  been exposed for users to be able to match on the `X.Y` version of a Python
+  interpreter.
+* (api) Added {obj}`merge_py_infos()` so user rules can merge and propagate
+  `PyInfo` without losing information.
+* (toolchains) New Python versions available: 3.13.0 using the [20241008] release.
+* (toolchains): Bump default toolchain versions to:
+    * `3.8 -> 3.8.20`
+    * `3.9 -> 3.9.20`
+    * `3.10 -> 3.10.15`
+    * `3.11 -> 3.11.10`
+    * `3.12 -> 3.12.7`
+* (coverage) Add support for python 3.13 and bump `coverage.py` to 7.6.1.
+* (bzlmod) Add support for `download_only` flag to disable usage of `sdists`
+  when {bzl:attr}`pip.parse.experimental_index_url` is set.
+* (api) PyInfo fields: {obj}`PyInfo.transitive_implicit_pyc_files`,
+  {obj}`PyInfo.transitive_implicit_pyc_source_files`.
+
+[20241008]: https://github.com/indygreg/python-build-standalone/releases/tag/20241008
+
+### Removed
+* (precompiling) {obj}`--precompile_add_to_runfiles` has been removed.
+* (precompiling) {obj}`--pyc_collection` has been removed. The `pyc_collection`
+  attribute now bases its default on {obj}`--precompile`.
+* (precompiling) The {obj}`precompile=if_generated_source` value has been removed.
+* (precompiling) The {obj}`precompile_source_retention=omit_if_generated_source` value has been removed.
 
 ## [0.36.0] - 2024-09-24
 
@@ -83,7 +161,6 @@
 * (toolchain) The {bzl:obj}`gen_python_config_settings` has been fixed to include
   the flag_values from the platform definitions.
 
-
 ### Added
 * (bzlmod): Toolchain overrides can now be done using the new
   {bzl:obj}`python.override`, {bzl:obj}`python.single_version_override` and
diff --git a/DEVELOPING.md b/DEVELOPING.md
index a70d3b1..0601be5 100644
--- a/DEVELOPING.md
+++ b/DEVELOPING.md
@@ -9,6 +9,8 @@
 1. Bump the coverage dependencies using the script using:
    ```
    bazel run //tools/private/update_deps:update_coverage_deps <VERSION>
+   # for example:
+   # bazel run //tools/private/update_deps:update_coverage_deps 7.6.1
    ```
 
 ## Releasing
diff --git a/METADATA b/METADATA
index 1d87185..7f9ccc1 100644
--- a/METADATA
+++ b/METADATA
@@ -8,12 +8,12 @@
   license_type: NOTICE
   last_upgrade_date {
     year: 2024
-    month: 11
-    day: 18
+    month: 12
+    day: 10
   }
   identifier {
     type: "Git"
     value: "https://github.com/bazelbuild/rules_python"
-    version: "0.36.0"
+    version: "0.37.0"
   }
 }
diff --git a/MODULE.bazel b/MODULE.bazel
index 58c7ae2..0cbae38 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -66,10 +66,13 @@
 )
 use_repo(pip, "rules_python_publish_deps")
 
+# Not a dev dependency to allow usage of //sphinxdocs code, which refers to stardoc repos.
+bazel_dep(name = "stardoc", version = "0.6.2", repo_name = "io_bazel_stardoc")
+
 # ===== DEV ONLY DEPS AND SETUP BELOW HERE =====
-bazel_dep(name = "stardoc", version = "0.6.2", dev_dependency = True, repo_name = "io_bazel_stardoc")
 bazel_dep(name = "rules_bazel_integration_test", version = "0.20.0", dev_dependency = True)
 bazel_dep(name = "rules_testing", version = "0.6.0", dev_dependency = True)
+bazel_dep(name = "rules_shell", version = "0.2.0", dev_dependency = True)
 
 # Extra gazelle plugin deps so that WORKSPACE.bzlmod can continue including it for e2e tests.
 # We use `WORKSPACE.bzlmod` because it is impossible to have dev-only local overrides.
@@ -91,6 +94,7 @@
     dev_dependency = True,
 )
 dev_pip.parse(
+    download_only = True,  # this will not add the `sdist` values to the transitive closures at all.
     hub_name = "dev_pip",
     python_version = "3.11",
     requirements_lock = "//docs:requirements.txt",
diff --git a/WORKSPACE b/WORKSPACE
index 02b3b6e..33ab37f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -41,14 +41,14 @@
 
 rules_python_internal_setup()
 
+load("@pythons_hub//:versions.bzl", "MINOR_MAPPING", "PYTHON_VERSIONS")
 load("//python:repositories.bzl", "python_register_multi_toolchains")
-load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
 
 python_register_multi_toolchains(
     name = "python",
-    default_version = MINOR_MAPPING.values()[-2],
+    default_version = MINOR_MAPPING.values()[-3],  # Use 3.11.10
     # Integration tests verify each version, so register all of them.
-    python_versions = TOOL_VERSIONS.keys(),
+    python_versions = PYTHON_VERSIONS,
 )
 
 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel
index 149e2c5..33d93fd 100644
--- a/docs/BUILD.bazel
+++ b/docs/BUILD.bazel
@@ -93,14 +93,16 @@
         "//python:py_runtime_info_bzl",
         "//python:py_test_bzl",
         "//python:repositories_bzl",
+        "//python/api:api_bzl",
         "//python/cc:py_cc_toolchain_bzl",
         "//python/cc:py_cc_toolchain_info_bzl",
         "//python/entry_points:py_console_script_binary_bzl",
+        "//python/private:py_binary_rule_bazel_bzl",
         "//python/private:py_cc_toolchain_rule_bzl",
-        "//python/private/common:py_binary_rule_bazel_bzl",
-        "//python/private/common:py_library_rule_bazel_bzl",
-        "//python/private/common:py_runtime_rule_bzl",
-        "//python/private/common:py_test_rule_bazel_bzl",
+        "//python/private:py_library_rule_bazel_bzl",
+        "//python/private:py_runtime_rule_bzl",
+        "//python/private:py_test_rule_bazel_bzl",
+        "//python/private/api:py_common_api_bzl",
     ] + ([
         # Bazel 6 + Stardoc isn't able to parse something about the python bzlmod extension
         "//python/extensions:python_bzl",
diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md
index e102baa..511a218 100644
--- a/docs/api/rules_python/python/config_settings/index.md
+++ b/docs/api/rules_python/python/config_settings/index.md
@@ -5,11 +5,34 @@
 
 # //python/config_settings
 
+:::{bzl:flag} add_srcs_to_runfiles
+Determines if the `srcs` of targets are added to their runfiles.
+
+More specifically, the sources added to runfiles are the `.py` files in `srcs`.
+If precompiling is performed, it is the `.py` files that are kept according
+to {obj}`precompile_source_retention`.
+
+Values:
+* `auto`: (default) Automatically decide the effective value; the current
+  behavior is `disabled`.
+* `disabled`: Don't add `srcs` to a target's runfiles.
+* `enabled`:  Add `srcs` to a target's runfiles.
+::::{versionadded} 0.37.0
+::::
+::::{deprecated} 0.37.0
+This is a transition flag and will be removed in a subsequent release.
+::::
+:::
+
 :::{bzl:flag} python_version
 Determines the default hermetic Python toolchain version. This can be set to
 one of the values that `rules_python` maintains.
 :::
 
+:::{bzl:target} python_version_major_minor
+Parses the value of the `python_version` and transforms it into a `X.Y` value.
+:::
+
 ::::{bzl:flag} exec_tools_toolchain
 Determines if the {obj}`exec_tools_toolchain_type` toolchain is enabled.
 
@@ -38,12 +61,8 @@
 
 * `auto`: (default) Automatically decide the effective value based on environment,
   target platform, etc.
-* `enabled`: Compile Python source files at build time. Note that
-  {bzl:obj}`--precompile_add_to_runfiles` affects how the compiled files are included into
-  a downstream binary.
+* `enabled`: Compile Python source files at build time.
 * `disabled`: Don't compile Python source files at build time.
-* `if_generated_source`: Compile Python source files, but only if they're a
-  generated file.
 * `force_enabled`: Like `enabled`, except overrides target-level setting. This
   is mostly useful for development, testing enabling precompilation more
   broadly, or as an escape hatch if build-time compiling is not available.
@@ -52,6 +71,9 @@
   broadly, or as an escape hatch if build-time compiling is not available.
 :::{versionadded} 0.33.0
 :::
+:::{versionchanged} 0.37.0
+The `if_generated_source` value was removed
+:::
 ::::
 
 ::::{bzl:flag} precompile_source_retention
@@ -69,45 +91,14 @@
   target platform, etc.
 * `keep_source`: Include the original Python source.
 * `omit_source`: Don't include the orignal py source.
-* `omit_if_generated_source`: Keep the original source if it's a regular source
-  file, but omit it if it's a generated file.
 
 :::{versionadded} 0.33.0
 :::
 :::{versionadded} 0.36.0
 The `auto` value
 :::
-::::
-
-::::{bzl:flag} precompile_add_to_runfiles
-Determines if a target adds its compiled files to its runfiles.
-
-When a target compiles its files, but doesn't add them to its own runfiles, it
-relies on a downstream target to retrieve them from
-{bzl:obj}`PyInfo.transitive_pyc_files`
-
-Values:
-* `always`: Always include the compiled files in the target's runfiles.
-* `decided_elsewhere`: Don't include the compiled files in the target's
-  runfiles; they are still added to {bzl:obj}`PyInfo.transitive_pyc_files`. See
-  also: {bzl:obj}`py_binary.pyc_collection` attribute. This is useful for allowing
-  incrementally enabling precompilation on a per-binary basis.
-:::{versionadded} 0.33.0
-:::
-::::
-
-::::{bzl:flag} pyc_collection
-Determine if `py_binary` collects transitive pyc files.
-
-:::{note}
-This flag is overridden by the target level `pyc_collection` attribute.
-:::
-
-Values:
-* `include_pyc`: Include `PyInfo.transitive_pyc_files` as part of the binary.
-* `disabled`: Don't include `PyInfo.transitive_pyc_files` as part of the binary.
-:::{versionadded} 0.33.0
-:::
+:::{versionchanged} 0.37.0
+The `omit_if_generated_source` value was removed
 ::::
 
 ::::{bzl:flag} py_linux_libc
diff --git a/docs/api/rules_python/python/runtime_env_toolchains/index.md b/docs/api/rules_python/python/runtime_env_toolchains/index.md
index 7d6e1fb..5ced89b 100644
--- a/docs/api/rules_python/python/runtime_env_toolchains/index.md
+++ b/docs/api/rules_python/python/runtime_env_toolchains/index.md
@@ -7,11 +7,17 @@
 
 ::::{target} all
 
-A set of toolchains that invoke `python3` from the runtime environment.
+A set of toolchains that invoke `python3` from the runtime environment (i.e
+after building).
 
-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.
+:::{note}
+These toolchains do not provide any build-time information, including but not
+limited to the Python version or C headers. As such, they cannot be used
+for e.g. precompiling, building Python C extension modules, or anything else
+that requires information about the Python runtime at build time. Under the
+hood, these simply create a fake "interpreter" that calls `python3` that
+built programs use to run themselves.
+:::
 
 This is only provided to aid migration off the builtin Bazel toolchain 
 (`@bazel_tools//python:autodetecting_toolchain`), and is largely only applicable
diff --git a/docs/conf.py b/docs/conf.py
index d65d5b5..9d33782 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -59,7 +59,7 @@
 autodoc2_docstring_parser_regexes = [
     (".*", "myst"),
 ]
- 
+
 # NOTE: The redirects generation will clobber existing files.
 redirects = {
     "api/tools/precompiler/index": "/api/rules_python/tools/precompiler/index.html",
@@ -69,10 +69,10 @@
     "api/python/defs": "/api/rules_python/python/defs.html",
     "api/python/index": "/api/rules_python/python/index.html",
     "api/python/py_runtime_info": "/api/rules_python/python/py_runtime_info.html",
-    "api/python/private/common/py_library_rule_bazel": "/api/rules_python/python/private/common/py_library_rule_bazel.html",
-    "api/python/private/common/py_test_rule_bazel": "/api/rules_python/python/private/common/py_test_rule_bazel.html",
-    "api/python/private/common/py_binary_rule_bazel": "/api/rules_python/python/private/common/py_binary_rule_bazel.html",
-    "api/python/private/common/py_runtime_rule": "/api/rules_python/python/private/common/py_runtime_rule.html",
+    "api/python/private/common/py_library_rule_bazel": "/api/rules_python/python/private/py_library_rule_bazel.html",
+    "api/python/private/common/py_test_rule_bazel": "/api/rules_python/python/private/py_test_rule_bazel.html",
+    "api/python/private/common/py_binary_rule_bazel": "/api/rules_python/python/private/py_binary_rule_bazel.html",
+    "api/python/private/common/py_runtime_rule": "/api/rules_python/python/private/py_runtime_rule.html",
     "api/python/extensions/pip": "/api/rules_python/python/extensions/pip.html",
     "api/python/extensions/python": "/api/rules_python/python/extensions/python.html",
     "api/python/entry_points/py_console_script_binary": "/api/rules_python/python/entry_points/py_console_script_binary.html",
diff --git a/docs/precompiling.md b/docs/precompiling.md
index 52678e6..6eadc40 100644
--- a/docs/precompiling.md
+++ b/docs/precompiling.md
@@ -20,24 +20,24 @@
 
 ## Binary-level opt-in
 
-Because of the costs of precompiling, it may not be feasible to globally enable it
-for your repo for everything. For example, some binaries may be
-particularly large, and doubling the number of runfiles isn't doable.
+Binary-level opt-in allows enabling precompiling on a per-target basic. This is
+useful for situations such as:
 
-If this is the case, there's an alternative way to more selectively and
-incrementally control precompiling on a per-binry basis.
+* Globally enabling precompiling in your `.bazelrc` isn't feasible. This may
+  be because some targets don't work with precompiling, e.g. because they're too
+  big.
+* Enabling precompiling for build tools (exec config targets) separately from
+  target-config programs.
 
-To use this approach, the two basic steps are:
-1. Disable pyc files from being automatically added to runfiles:
-   {bzl:obj}`--@rules_python//python/config_settings:precompile_add_to_runfiles=decided_elsewhere`,
-2. Set the `pyc_collection` attribute on the binaries/tests that should or should
-   not use precompiling.
+To use this approach, set the {bzl:attr}`pyc_collection` attribute on the
+binaries/tests that should or should not use precompiling. Then change the
+{bzl:flag}`--precompile` default.
 
-The default for the `pyc_collection` attribute is controlled by the flag
-{bzl:obj}`--@rules_python//python/config_settings:pyc_collection`, so you
+The default for the {bzl:attr}`pyc_collection` attribute is controlled by the flag
+{bzl:obj}`--@rules_python//python/config_settings:precompile`, so you
 can use an opt-in or opt-out approach by setting its value:
-* targets must opt-out: `--@rules_python//python/config_settings:pyc_collection=include_pyc`
-* targets must opt-in: `--@rules_python//python/config_settings:pyc_collection=disabled`
+* targets must opt-out: `--@rules_python//python/config_settings:precompile=enabled`
+* targets must opt-in: `--@rules_python//python/config_settings:precompile=disabled`
 
 ## Advanced precompiler customization
 
@@ -48,7 +48,7 @@
 mechanisms are available:
 
 * The exec tools toolchain allows customizing the precompiler binary used with
-  the `precompiler` attribute. Arbitrary binaries are supported.
+  the {bzl:attr}`precompiler` attribute. Arbitrary binaries are supported.
 * The execution requirements can be customized using
   `--@rules_python//tools/precompiler:execution_requirements`. This is a list
   flag that can be repeated. Each entry is a key=value that is added to the
@@ -92,3 +92,9 @@
   `foo.cpython-39.opt-2.pyc`). This works fine (it's all byte code), but also
   means the interpreter `-O` argument can't be used -- doing so will cause the
   interpreter to look for the non-existent `opt-N` named files.
+* Targets with the same source files and different exec properites will result
+  in action conflicts. This most commonly occurs when a `py_binary` and
+  `py_library` have the same source files. To fix, modify both targets so
+  they have the same exec properties. If this is difficult because unsupported
+  exec groups end up being passed to the Python rules, please file an issue
+  to have those exec groups added to the Python rules.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 7b5681d..dcaa74e 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -125,9 +125,9 @@
     #   myst-parser
     #   sphinx
     #   sphinx-rtd-theme
-idna==3.8 \
-    --hash=sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac \
-    --hash=sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603
+idna==3.10 \
+    --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
+    --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
     # via requests
 imagesize==1.4.1 \
     --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
@@ -146,67 +146,68 @@
     # via
     #   mdit-py-plugins
     #   myst-parser
-markupsafe==2.1.5 \
-    --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \
-    --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \
-    --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \
-    --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \
-    --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \
-    --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \
-    --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \
-    --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \
-    --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \
-    --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \
-    --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \
-    --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \
-    --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \
-    --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \
-    --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \
-    --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \
-    --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \
-    --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \
-    --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \
-    --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \
-    --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \
-    --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \
-    --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \
-    --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \
-    --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \
-    --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \
-    --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \
-    --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \
-    --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \
-    --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \
-    --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \
-    --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \
-    --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \
-    --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \
-    --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \
-    --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \
-    --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \
-    --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \
-    --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \
-    --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \
-    --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \
-    --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \
-    --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \
-    --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \
-    --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \
-    --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \
-    --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \
-    --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \
-    --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \
-    --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \
-    --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \
-    --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \
-    --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \
-    --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \
-    --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \
-    --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \
-    --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \
-    --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \
-    --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \
-    --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68
+markupsafe==3.0.1 \
+    --hash=sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396 \
+    --hash=sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38 \
+    --hash=sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a \
+    --hash=sha256:242d6860f1fd9191aef5fae22b51c5c19767f93fb9ead4d21924e0bcb17619d8 \
+    --hash=sha256:244dbe463d5fb6d7ce161301a03a6fe744dac9072328ba9fc82289238582697b \
+    --hash=sha256:26627785a54a947f6d7336ce5963569b5d75614619e75193bdb4e06e21d447ad \
+    --hash=sha256:2a4b34a8d14649315c4bc26bbfa352663eb51d146e35eef231dd739d54a5430a \
+    --hash=sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a \
+    --hash=sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da \
+    --hash=sha256:3341c043c37d78cc5ae6e3e305e988532b072329639007fd408a476642a89fd6 \
+    --hash=sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8 \
+    --hash=sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344 \
+    --hash=sha256:3ffb4a8e7d46ed96ae48805746755fadd0909fea2306f93d5d8233ba23dda12a \
+    --hash=sha256:40621d60d0e58aa573b68ac5e2d6b20d44392878e0bfc159012a5787c4e35bc8 \
+    --hash=sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5 \
+    --hash=sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7 \
+    --hash=sha256:48488d999ed50ba8d38c581d67e496f955821dc183883550a6fbc7f1aefdc170 \
+    --hash=sha256:4935dd7883f1d50e2ffecca0aa33dc1946a94c8f3fdafb8df5c330e48f71b132 \
+    --hash=sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9 \
+    --hash=sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd \
+    --hash=sha256:4ffaaac913c3f7345579db4f33b0020db693f302ca5137f106060316761beea9 \
+    --hash=sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346 \
+    --hash=sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc \
+    --hash=sha256:67c519635a4f64e495c50e3107d9b4075aec33634272b5db1cde839e07367589 \
+    --hash=sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5 \
+    --hash=sha256:7044312a928a66a4c2a22644147bc61a199c1709712069a344a3fb5cfcf16915 \
+    --hash=sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295 \
+    --hash=sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453 \
+    --hash=sha256:80fcbf3add8790caddfab6764bde258b5d09aefbe9169c183f88a7410f0f6dea \
+    --hash=sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b \
+    --hash=sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d \
+    --hash=sha256:8ad4ad1429cd4f315f32ef263c1342166695fad76c100c5d979c45d5570ed58b \
+    --hash=sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4 \
+    --hash=sha256:93e8248d650e7e9d49e8251f883eed60ecbc0e8ffd6349e18550925e31bd029b \
+    --hash=sha256:973a371a55ce9ed333a3a0f8e0bcfae9e0d637711534bcb11e130af2ab9334e7 \
+    --hash=sha256:9ba25a71ebf05b9bb0e2ae99f8bc08a07ee8e98c612175087112656ca0f5c8bf \
+    --hash=sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f \
+    --hash=sha256:a4792d3b3a6dfafefdf8e937f14906a51bd27025a36f4b188728a73382231d91 \
+    --hash=sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd \
+    --hash=sha256:ad91738f14eb8da0ff82f2acd0098b6257621410dcbd4df20aaa5b4233d75a50 \
+    --hash=sha256:b6a387d61fe41cdf7ea95b38e9af11cfb1a63499af2759444b99185c4ab33f5b \
+    --hash=sha256:b954093679d5750495725ea6f88409946d69cfb25ea7b4c846eef5044194f583 \
+    --hash=sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a \
+    --hash=sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984 \
+    --hash=sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c \
+    --hash=sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c \
+    --hash=sha256:cb53e2a99df28eee3b5f4fea166020d3ef9116fdc5764bc5117486e6d1211b25 \
+    --hash=sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa \
+    --hash=sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4 \
+    --hash=sha256:d98e66a24497637dd31ccab090b34392dddb1f2f811c4b4cd80c230205c074a3 \
+    --hash=sha256:db15ce28e1e127a0013dfb8ac243a8e392db8c61eae113337536edb28bdc1f97 \
+    --hash=sha256:db842712984e91707437461930e6011e60b39136c7331e971952bb30465bc1a1 \
+    --hash=sha256:e24bfe89c6ac4c31792793ad9f861b8f6dc4546ac6dc8f1c9083c7c4f2b335cd \
+    --hash=sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772 \
+    --hash=sha256:e9393357f19954248b00bed7c56f29a25c930593a77630c719653d51e7669c2a \
+    --hash=sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729 \
+    --hash=sha256:f31ae06f1328595d762c9a2bf29dafd8621c7d3adc130cbb46278079758779ca \
+    --hash=sha256:f94190df587738280d544971500b9cafc9b950d32efcb1fba9ac10d84e6aa4e6 \
+    --hash=sha256:fa7d686ed9883f3d664d39d5a8e74d3c5f63e603c2e3ff0abcba23eac6542635 \
+    --hash=sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b \
+    --hash=sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f
     # via jinja2
 mdit-py-plugins==0.4.2 \
     --hash=sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636 \
@@ -316,9 +317,9 @@
     --hash=sha256:444ae1438fba4418242ca76d6a6de3eaee82aaf0d8f2b0cac71a15d32ce6eba2 \
     --hash=sha256:cfa753b441020a22708ce8eb17d4fd553a28fc87a609330092917ada2a6da0d8
     # via rules-python-docs (docs/pyproject.toml)
-sphinx-rtd-theme==2.0.0 \
-    --hash=sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b \
-    --hash=sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586
+sphinx-rtd-theme==3.0.1 \
+    --hash=sha256:921c0ece75e90633ee876bd7b148cfaad136b481907ad154ac3669b6fc957916 \
+    --hash=sha256:a4c5745d1b06dfcb80b7704fe532eb765b44065a8fad9851e4258c8804140703
     # via rules-python-docs (docs/pyproject.toml)
 sphinxcontrib-applehelp==2.0.0 \
     --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \
@@ -354,7 +355,7 @@
     # via
     #   rules-python-docs (docs/pyproject.toml)
     #   sphinx-autodoc2
-urllib3==2.2.2 \
-    --hash=sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472 \
-    --hash=sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168
+urllib3==2.2.3 \
+    --hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \
+    --hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9
     # via requests
diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel
index 4ac8191..e9d69c5 100644
--- a/examples/bzlmod/MODULE.bazel
+++ b/examples/bzlmod/MODULE.bazel
@@ -42,17 +42,16 @@
 python.override(
     available_python_versions = [
         "3.10.9",
+        "3.9.18",
         "3.9.19",
         # The following is used by the `other_module` and we need to include it here
         # as well.
         "3.11.8",
     ],
     # Also override the `minor_mapping` so that the root module,
-    # instead of rules_python's defaults, controls what full version
-    # is used when `3.x` is requested.
+    # instead of rules_python's defaulting to the latest available version,
+    # controls what full version is used when `3.x` is requested.
     minor_mapping = {
-        "3.10": "3.10.9",
-        "3.11": "3.11.8",
         "3.9": "3.9.19",
     },
 )
@@ -90,7 +89,7 @@
 # See the tests folder for various examples on using multiple Python versions.
 # The names "python_3_9" and "python_3_10" are autmatically created by the repo
 # rules based on the `python_version` arg values.
-use_repo(python, "python_3_10", "python_3_9", "python_versions")
+use_repo(python, "python_3_10", "python_3_9", "python_versions", "pythons_hub")
 
 # EXPERIMENTAL: This is experimental and may be removed without notice
 uv = use_extension("@rules_python//python/uv:extensions.bzl", "uv")
@@ -257,3 +256,6 @@
     module_name = "other_module",
     path = "other_module",
 )
+
+# example test dependencies
+bazel_dep(name = "rules_shell", version = "0.2.0", dev_dependency = True)
diff --git a/examples/bzlmod/MODULE.bazel.lock b/examples/bzlmod/MODULE.bazel.lock
index cb8fbe2..8c66a13 100644
--- a/examples/bzlmod/MODULE.bazel.lock
+++ b/examples/bzlmod/MODULE.bazel.lock
@@ -10,13 +10,15 @@
     "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef",
     "https://bcr.bazel.build/modules/apple_support/1.5.0/source.json": "eb98a7627c0bc486b57f598ad8da50f6625d974c8f723e9ea71bd39f709c9862",
     "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
-    "https://bcr.bazel.build/modules/bazel_features/1.11.0/source.json": "c9320aa53cd1c441d24bd6b716da087ad7e4ff0d9742a9884587596edfe53015",
+    "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
+    "https://bcr.bazel.build/modules/bazel_features/1.18.0/source.json": "cde886d88c8164b50b9b97dba7c0a64ca24d257b72ca3a2fcb06bee1fdb47ee4",
     "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
     "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
     "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
     "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
     "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
     "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
+    "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
     "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
     "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
     "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/source.json": "082ed5f9837901fada8c68c2f3ddc958bb22b6d654f71dd73f3df30d45d4b749",
@@ -25,13 +27,14 @@
     "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
     "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
     "https://bcr.bazel.build/modules/googletest/1.14.0/source.json": "2478949479000fdd7de9a3d0107ba2c85bb5f961c3ecb1aa448f52549ce310b5",
+    "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
+    "https://bcr.bazel.build/modules/platforms/0.0.10/source.json": "f22828ff4cf021a6b577f1bf6341cb9dcd7965092a439f64fc1bb3b7a5ae4bd5",
     "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
     "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
     "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
     "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
     "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d",
     "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc",
-    "https://bcr.bazel.build/modules/platforms/0.0.9/source.json": "cd74d854bf16a9e002fb2ca7b1a421f4403cda29f824a765acd3a8c56f8d43e6",
     "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
     "https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a",
     "https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12",
@@ -45,12 +48,14 @@
     "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
     "https://bcr.bazel.build/modules/rules_cc/0.0.9/source.json": "1f1ba6fea244b616de4a554a0f4983c91a9301640c8fe0dd1d410254115c8430",
     "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
+    "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963",
     "https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64",
     "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1",
     "https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d",
     "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
     "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
-    "https://bcr.bazel.build/modules/rules_jvm_external/5.1/source.json": "5abb45cc9beb27b77aec6a65a11855ef2b55d95dfdc358e9f312b78ae0ba32d5",
+    "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
+    "https://bcr.bazel.build/modules/rules_jvm_external/5.2/source.json": "10572111995bc349ce31c78f74b3c147f6b3233975c7fa5eff9211f6db0d34d9",
     "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
     "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
     "https://bcr.bazel.build/modules/rules_license/0.0.7/source.json": "355cc5737a0f294e560d52b1b7a6492d4fff2caf0bef1a315df5a298fca2d34a",
@@ -60,9 +65,12 @@
     "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
     "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
     "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/source.json": "8d8448e71706df7450ced227ca6b3812407ff5e2ccad74a43a9fbe79c84e34e0",
+    "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
+    "https://bcr.bazel.build/modules/rules_shell/0.2.0/source.json": "7f27af3c28037d9701487c4744b5448d26537cc66cdef0d8df7ae85411f8de95",
     "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
     "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
-    "https://bcr.bazel.build/modules/stardoc/0.5.3/source.json": "cd53fe968dc8cd98197c052db3db6d82562960c87b61e7a90ee96f8e4e0dda97",
+    "https://bcr.bazel.build/modules/stardoc/0.6.2/MODULE.bazel": "7060193196395f5dd668eda046ccbeacebfd98efc77fed418dbe2b82ffaa39fd",
+    "https://bcr.bazel.build/modules/stardoc/0.6.2/source.json": "d2ff8063b63b4a85e65fe595c4290f99717434fa9f95b4748a79a7d04dfed349",
     "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
     "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9",
     "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/source.json": "b2150404947339e8b947c6b16baa39fa75657f4ddec5e37272c7b11c7ab533bc",
@@ -104,7 +112,7 @@
     "@@platforms//host:extension.bzl%host_platform": {
       "general": {
         "bzlTransitiveDigest": "xelQcPZH8+tmuOHVjL9vDxMnnQNMlwj0SlvgoqBkm4U=",
-        "usagesDigest": "meSzxn3DUCcYEhq4HQwExWkWtU4EjriRBQLsZN+Q0SU=",
+        "usagesDigest": "V1R2Y2oMxKNfx2WCWpSCaUV1WefW1o8HZGm3v1vHgY4=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -149,9 +157,10 @@
     },
     "@@rules_jvm_external~//:extensions.bzl%maven": {
       "general": {
-        "bzlTransitiveDigest": "4ijz6uc3T4E+d+U8LQv4EAt+8OqZNVY/lzvhLx3y1yg=",
-        "usagesDigest": "WfVTcbopbu3jyxPgDWx1iqIv1QV6L/T7utvDxAj5k84=",
+        "bzlTransitiveDigest": "U98JuBYMWVrcyiXT1L6KAYSAA0chnjRZZloIUmNmZ7M=",
+        "usagesDigest": "1L6xElvJScwRWKMMza2Jyew+Iuz6EPOkfBMQmHYuNIk=",
         "recordedFileInputs": {
+          "@@stardoc~//maven_install.json": "de0bfa778b4ed6aebb77509362dd87ab8d20fc7c7c18d2a7429cdfee03949a21",
           "@@rules_jvm_external~//rules_jvm_external_deps_install.json": "3ab1f67b0de4815df110bc72ccd6c77882b3b21d3d1e0a84445847b6ce3235a3"
         },
         "recordedDirentsInputs": {},
@@ -199,8 +208,7 @@
             "attributes": {
               "sha256": "a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26",
               "urls": [
-                "https://repo1.maven.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar",
-                "https://maven.google.com/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar"
+                "https://repo1.maven.org/maven2/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar"
               ],
               "downloaded_file_path": "com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar"
             }
@@ -241,6 +249,42 @@
               "downloaded_file_path": "com/google/cloud/google-cloud-storage/1.113.4/google-cloud-storage-1.113.4.jar"
             }
           },
+          "unpinned_stardoc_maven": {
+            "bzlFile": "@@rules_jvm_external~//:coursier.bzl",
+            "ruleClassName": "coursier_fetch",
+            "attributes": {
+              "repositories": [
+                "{ \"repo_url\": \"https://repo1.maven.org/maven2\" }"
+              ],
+              "artifacts": [
+                "{ \"group\": \"com.beust\", \"artifact\": \"jcommander\", \"version\": \"1.82\" }",
+                "{ \"group\": \"com.google.escapevelocity\", \"artifact\": \"escapevelocity\", \"version\": \"1.1\" }",
+                "{ \"group\": \"com.google.guava\", \"artifact\": \"guava\", \"version\": \"31.1-jre\" }",
+                "{ \"group\": \"com.google.truth\", \"artifact\": \"truth\", \"version\": \"1.1.3\" }",
+                "{ \"group\": \"junit\", \"artifact\": \"junit\", \"version\": \"4.13.2\" }"
+              ],
+              "fail_on_missing_checksum": true,
+              "fetch_sources": true,
+              "fetch_javadoc": false,
+              "excluded_artifacts": [],
+              "generate_compat_repositories": false,
+              "version_conflict_policy": "default",
+              "override_targets": {},
+              "strict_visibility": true,
+              "strict_visibility_value": [
+                "@@//visibility:private"
+              ],
+              "maven_install_json": "@@stardoc~//:maven_install.json",
+              "resolve_timeout": 600,
+              "jetify": false,
+              "jetify_include_list": [
+                "*"
+              ],
+              "use_starlark_android_rules": false,
+              "aar_import_bzl_label": "@build_bazel_rules_android//android:rules.bzl",
+              "duplicate_version_warning": "warn"
+            }
+          },
           "io_grpc_grpc_context_1_33_1": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -331,8 +375,7 @@
             "attributes": {
               "sha256": "21af30c92267bd6122c0e0b4d20cccb6641a37eaf956c6540ec471d584e64a7b",
               "urls": [
-                "https://repo1.maven.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar",
-                "https://maven.google.com/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar"
+                "https://repo1.maven.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar"
               ],
               "downloaded_file_path": "com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar"
             }
@@ -349,6 +392,17 @@
               "downloaded_file_path": "software/amazon/awssdk/metrics-spi/2.17.183/metrics-spi-2.17.183.jar"
             }
           },
+          "com_google_escapevelocity_escapevelocity_1_1": {
+            "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
+            "ruleClassName": "http_file",
+            "attributes": {
+              "sha256": "37e76e4466836dedb864fb82355cd01c3bd21325ab642d89a0f759291b171231",
+              "urls": [
+                "https://repo1.maven.org/maven2/com/google/escapevelocity/escapevelocity/1.1/escapevelocity-1.1.jar"
+              ],
+              "downloaded_file_path": "com/google/escapevelocity/escapevelocity/1.1/escapevelocity-1.1.jar"
+            }
+          },
           "org_reactivestreams_reactive_streams_1_0_3": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -385,6 +439,17 @@
               "downloaded_file_path": "io/netty/netty-transport/4.1.72.Final/netty-transport-4.1.72.Final.jar"
             }
           },
+          "com_beust_jcommander_1_82": {
+            "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
+            "ruleClassName": "http_file",
+            "attributes": {
+              "sha256": "deeac157c8de6822878d85d0c7bc8467a19cc8484d37788f7804f039dde280b1",
+              "urls": [
+                "https://repo1.maven.org/maven2/com/beust/jcommander/1.82/jcommander-1.82.jar"
+              ],
+              "downloaded_file_path": "com/beust/jcommander/1.82/jcommander-1.82.jar"
+            }
+          },
           "io_netty_netty_codec_http2_4_1_72_Final": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -588,8 +653,7 @@
             "attributes": {
               "sha256": "a42edc9cab792e39fe39bb94f3fca655ed157ff87a8af78e1d6ba5b07c4a00ab",
               "urls": [
-                "https://repo1.maven.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar",
-                "https://maven.google.com/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar"
+                "https://repo1.maven.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar"
               ],
               "downloaded_file_path": "com/google/guava/guava/31.1-jre/guava-31.1-jre.jar"
             }
@@ -618,6 +682,17 @@
               "downloaded_file_path": "io/netty/netty-transport-native-unix-common/4.1.72.Final/netty-transport-native-unix-common-4.1.72.Final.jar"
             }
           },
+          "junit_junit_4_13_2": {
+            "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
+            "ruleClassName": "http_file",
+            "attributes": {
+              "sha256": "8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3",
+              "urls": [
+                "https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar"
+              ],
+              "downloaded_file_path": "junit/junit/4.13.2/junit-4.13.2.jar"
+            }
+          },
           "io_opencensus_opencensus_contrib_http_util_0_24_0": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -726,6 +801,17 @@
               "downloaded_file_path": "org/checkerframework/checker-qual/3.12.0/checker-qual-3.12.0.jar"
             }
           },
+          "org_hamcrest_hamcrest_core_1_3": {
+            "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
+            "ruleClassName": "http_file",
+            "attributes": {
+              "sha256": "66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9",
+              "urls": [
+                "https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar"
+              ],
+              "downloaded_file_path": "org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar"
+            }
+          },
           "com_google_cloud_google_cloud_core_http_1_93_10": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -832,8 +918,7 @@
             "attributes": {
               "sha256": "721cb91842b46fa056847d104d5225c8b8e1e8b62263b993051e1e5a0137b7ec",
               "urls": [
-                "https://repo1.maven.org/maven2/com/google/errorprone/error_prone_annotations/2.11.0/error_prone_annotations-2.11.0.jar",
-                "https://maven.google.com/com/google/errorprone/error_prone_annotations/2.11.0/error_prone_annotations-2.11.0.jar"
+                "https://repo1.maven.org/maven2/com/google/errorprone/error_prone_annotations/2.11.0/error_prone_annotations-2.11.0.jar"
               ],
               "downloaded_file_path": "com/google/errorprone/error_prone_annotations/2.11.0/error_prone_annotations-2.11.0.jar"
             }
@@ -910,6 +995,51 @@
               "downloaded_file_path": "software/amazon/awssdk/protocol-core/2.17.183/protocol-core-2.17.183.jar"
             }
           },
+          "stardoc_maven": {
+            "bzlFile": "@@rules_jvm_external~//:coursier.bzl",
+            "ruleClassName": "pinned_coursier_fetch",
+            "attributes": {
+              "repositories": [
+                "{ \"repo_url\": \"https://repo1.maven.org/maven2\" }"
+              ],
+              "artifacts": [
+                "{ \"group\": \"com.beust\", \"artifact\": \"jcommander\", \"version\": \"1.82\" }",
+                "{ \"group\": \"com.google.escapevelocity\", \"artifact\": \"escapevelocity\", \"version\": \"1.1\" }",
+                "{ \"group\": \"com.google.guava\", \"artifact\": \"guava\", \"version\": \"31.1-jre\" }",
+                "{ \"group\": \"com.google.truth\", \"artifact\": \"truth\", \"version\": \"1.1.3\" }",
+                "{ \"group\": \"junit\", \"artifact\": \"junit\", \"version\": \"4.13.2\" }"
+              ],
+              "fetch_sources": true,
+              "fetch_javadoc": false,
+              "generate_compat_repositories": false,
+              "maven_install_json": "@@stardoc~//:maven_install.json",
+              "override_targets": {},
+              "strict_visibility": true,
+              "strict_visibility_value": [
+                "@@//visibility:private"
+              ],
+              "jetify": false,
+              "jetify_include_list": [
+                "*"
+              ],
+              "additional_netrc_lines": [],
+              "fail_if_repin_required": true,
+              "use_starlark_android_rules": false,
+              "aar_import_bzl_label": "@build_bazel_rules_android//android:rules.bzl",
+              "duplicate_version_warning": "warn"
+            }
+          },
+          "com_google_truth_truth_1_1_3": {
+            "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
+            "ruleClassName": "http_file",
+            "attributes": {
+              "sha256": "fc0b67782289a2aabfddfdf99eff1dcd5edc890d49143fcd489214b107b8f4f3",
+              "urls": [
+                "https://repo1.maven.org/maven2/com/google/truth/truth/1.1.3/truth-1.1.3.jar"
+              ],
+              "downloaded_file_path": "com/google/truth/truth/1.1.3/truth-1.1.3.jar"
+            }
+          },
           "org_checkerframework_checker_compat_qual_2_5_5": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -934,6 +1064,17 @@
               "downloaded_file_path": "com/google/apis/google-api-services-storage/v1-rev20200927-1.30.10/google-api-services-storage-v1-rev20200927-1.30.10.jar"
             }
           },
+          "org_ow2_asm_asm_9_1": {
+            "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
+            "ruleClassName": "http_file",
+            "attributes": {
+              "sha256": "cda4de455fab48ff0bcb7c48b4639447d4de859a7afc30a094a986f0936beba2",
+              "urls": [
+                "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.1/asm-9.1.jar"
+              ],
+              "downloaded_file_path": "org/ow2/asm/asm/9.1/asm-9.1.jar"
+            }
+          },
           "com_google_api_client_google_api_client_1_30_11": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -1000,8 +1141,7 @@
             "attributes": {
               "sha256": "b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99",
               "urls": [
-                "https://repo1.maven.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar",
-                "https://maven.google.com/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar"
+                "https://repo1.maven.org/maven2/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar"
               ],
               "downloaded_file_path": "com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar"
             }
@@ -1042,6 +1182,17 @@
               "downloaded_file_path": "software/amazon/awssdk/arns/2.17.183/arns-2.17.183.jar"
             }
           },
+          "com_google_auto_value_auto_value_annotations_1_8_1": {
+            "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
+            "ruleClassName": "http_file",
+            "attributes": {
+              "sha256": "37ec09b47d7ed35a99d13927db5c86fc9071f620f943ead5d757144698310852",
+              "urls": [
+                "https://repo1.maven.org/maven2/com/google/auto/value/auto-value-annotations/1.8.1/auto-value-annotations-1.8.1.jar"
+              ],
+              "downloaded_file_path": "com/google/auto/value/auto-value-annotations/1.8.1/auto-value-annotations-1.8.1.jar"
+            }
+          },
           "com_google_code_gson_gson_2_9_0": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -1072,8 +1223,7 @@
             "attributes": {
               "sha256": "766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7",
               "urls": [
-                "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar",
-                "https://maven.google.com/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar"
+                "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar"
               ],
               "downloaded_file_path": "com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar"
             }
@@ -1126,6 +1276,17 @@
               "downloaded_file_path": "org/codehaus/plexus/plexus-utils/3.3.1/plexus-utils-3.3.1.jar"
             }
           },
+          "org_checkerframework_checker_qual_3_13_0": {
+            "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
+            "ruleClassName": "http_file",
+            "attributes": {
+              "sha256": "3ea0dcd73b4d6cb2fb34bd7ed4dad6db327a01ebad7db05eb7894076b3d64491",
+              "urls": [
+                "https://repo1.maven.org/maven2/org/checkerframework/checker-qual/3.13.0/checker-qual-3.13.0.jar"
+              ],
+              "downloaded_file_path": "org/checkerframework/checker-qual/3.13.0/checker-qual-3.13.0.jar"
+            }
+          },
           "com_google_protobuf_protobuf_java_util_3_13_0": {
             "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
             "ruleClassName": "http_file",
@@ -1204,7 +1365,7 @@
     "@@rules_jvm_external~//:non-module-deps.bzl%non_module_deps": {
       "general": {
         "bzlTransitiveDigest": "l6SlNloqPvd60dcuPdWiJNi3g3jfK76fcZc0i/Yr0dQ=",
-        "usagesDigest": "pX61d12AFioOtqChQDmxvlNGDYT69e5MrKT2E/S6TeQ=",
+        "usagesDigest": "hiuzyio8ny4T3UoEFpHaxXzNFc6OGUFvx5DDVLBBUmU=",
         "recordedFileInputs": {},
         "recordedDirentsInputs": {},
         "envVariables": {},
@@ -1231,7 +1392,7 @@
     },
     "@@rules_python~//python/extensions:pip.bzl%pip": {
       "general": {
-        "bzlTransitiveDigest": "ED3oUrLQz/MTptq8JOZ03sjD7HZ3naUeFS3XFpxz4tg=",
+        "bzlTransitiveDigest": "iikkSIkMsBiM/vadkEf9xEoVbaxZqrkUg08hiHr/LKk=",
         "usagesDigest": "MChlcSw99EuW3K7OOoMcXQIdcJnEh6YmfyjJm+9mxIg=",
         "recordedFileInputs": {
           "@@other_module~//requirements_lock_3_11.txt": "a7d0061366569043d5efcf80e34a32c732679367cb3c831c4cdc606adc36d314",
@@ -1242,7 +1403,7 @@
           "@@//requirements_lock_3_9.txt": "6a4990586366467d1e7d56d9f2ec9bafdd7e17fb29dc959aa5a6b0395c22eac7",
           "@@rules_python~~internal_deps~pypi__packaging//packaging-24.0.dist-info/RECORD": "be1aea790359b4c2c9ea83d153c1a57c407742a35b95ee36d00723509f5ed5dd",
           "@@//requirements_windows_3_10.txt": "c79f04bfaca147b8330275911a3328b81fc80828b9050a6bebdb15477627dabc",
-          "@@rules_python~//BUILD.bazel": "c421a2c2f3f428d2685a16eb9cc3fb8662605aba4ef151a87a356678bb7e866d",
+          "@@rules_python~//BUILD.bazel": "140002ce7e68de2fbf064bcdc37f854d4fa5b5d611a5fece6eb6cf19b8822bc4",
           "@@rules_python~~python~python_3_9_host//BUILD.bazel": "cf97d5763b728ce5ba8fdc3243350b967658ba4e3879734504aee002cec0d2b3",
           "@@rules_python~//python/private/pypi/requirements_parser/resolve_target_platforms.py": "42bf51980528302373529bcdfddb8014e485182d6bc9d2f7d3bbe1f11d8d923d"
         },
@@ -2955,7 +3116,6 @@
               "whl_map": {
                 "absl_py": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":null,\"repo\":\"other_module_pip_311_absl_py\",\"target_platforms\":null,\"version\":\"3.11\"}]"
               },
-              "default_version": "3.9",
               "packages": [],
               "groups": {}
             }
@@ -4267,7 +4427,6 @@
                 "sphinxcontrib_applehelp": "[{\"config_setting\":\"//_config:is_python_3.9\",\"filename\":\"sphinxcontrib_applehelp-1.0.7-py3-none-any.whl\",\"repo\":\"pip_39_sphinxcontrib_applehelp_py3_none_any_094c4d56\",\"target_platforms\":null,\"version\":\"3.9\"},{\"config_setting\":\"//_config:is_python_3.9\",\"filename\":\"sphinxcontrib_applehelp-1.0.7.tar.gz\",\"repo\":\"pip_39_sphinxcontrib_applehelp_sdist_39fdc8d7\",\"target_platforms\":null,\"version\":\"3.9\"},{\"config_setting\":\"//_config:is_python_3.10\",\"filename\":null,\"repo\":\"pip_310_sphinxcontrib_applehelp\",\"target_platforms\":null,\"version\":\"3.10\"}]",
                 "sphinxcontrib_serializinghtml": "[{\"config_setting\":\"//_config:is_python_3.9\",\"filename\":\"sphinxcontrib_serializinghtml-1.1.9-py3-none-any.whl\",\"repo\":\"pip_39_sphinxcontrib_serializinghtml_py3_none_any_9b36e503\",\"target_platforms\":null,\"version\":\"3.9\"},{\"config_setting\":\"//_config:is_python_3.9\",\"filename\":\"sphinxcontrib_serializinghtml-1.1.9.tar.gz\",\"repo\":\"pip_39_sphinxcontrib_serializinghtml_sdist_0c64ff89\",\"target_platforms\":null,\"version\":\"3.9\"},{\"config_setting\":\"//_config:is_python_3.10\",\"filename\":null,\"repo\":\"pip_310_sphinxcontrib_serializinghtml\",\"target_platforms\":null,\"version\":\"3.10\"}]"
               },
-              "default_version": "3.9",
               "packages": [
                 "alabaster",
                 "astroid",
@@ -6140,12 +6299,12 @@
     },
     "@@rules_python~//python/private/pypi:pip.bzl%pip_internal": {
       "general": {
-        "bzlTransitiveDigest": "vEOIMpxlh8qbHkABunGFRr+IDbabjCM/hUF0V3GGTus=",
+        "bzlTransitiveDigest": "WPfU9gogl29lCI8A/N2aYn7RAhsCpZikVU1Hw7nMtAc=",
         "usagesDigest": "Y8ihY+R57BAFhalrVLVdJFrpwlbsiKz9JPJ99ljF7HA=",
         "recordedFileInputs": {
           "@@rules_python~//tools/publish/requirements.txt": "031e35d03dde03ae6305fe4b3d1f58ad7bdad857379752deede0f93649991b8a",
-          "@@rules_python~//tools/publish/requirements_windows.txt": "15472d5a28e068d31ba9e2dc389459698afaff366e9db06e15890283a3ea252e",
-          "@@rules_python~//tools/publish/requirements_darwin.txt": "61cf602ff33b58c5f42a6cee30112985e9b502209605314e313157f8aad679f9"
+          "@@rules_python~//tools/publish/requirements_windows.txt": "27831a1477549ad865043f17a9c1dd9a19566d460ba1f68cd8dfded642accbca",
+          "@@rules_python~//tools/publish/requirements_darwin.txt": "91df49ab0079887f6b7ee4035f9e2a686036c749e7ce82837a4a74b471e4a9aa"
         },
         "recordedDirentsInputs": {},
         "envVariables": {
@@ -6335,46 +6494,6 @@
               ]
             }
           },
-          "rules_python_publish_deps_311_idna_py3_none_any_82fee1fc": {
-            "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
-            "ruleClassName": "whl_library",
-            "attributes": {
-              "dep_template": "@rules_python_publish_deps//{name}:{target}",
-              "experimental_target_platforms": [
-                "cp311_osx_aarch64",
-                "cp311_osx_x86_64",
-                "cp311_windows_x86_64"
-              ],
-              "filename": "idna-3.7-py3-none-any.whl",
-              "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
-              "repo": "rules_python_publish_deps_311",
-              "requirement": "idna==3.7",
-              "sha256": "82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0",
-              "urls": [
-                "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl"
-              ]
-            }
-          },
-          "rules_python_publish_deps_311_certifi_py3_none_any_c198e21b": {
-            "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
-            "ruleClassName": "whl_library",
-            "attributes": {
-              "dep_template": "@rules_python_publish_deps//{name}:{target}",
-              "experimental_target_platforms": [
-                "cp311_osx_aarch64",
-                "cp311_osx_x86_64",
-                "cp311_windows_x86_64"
-              ],
-              "filename": "certifi-2024.7.4-py3-none-any.whl",
-              "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
-              "repo": "rules_python_publish_deps_311",
-              "requirement": "certifi==2024.7.4",
-              "sha256": "c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90",
-              "urls": [
-                "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl"
-              ]
-            }
-          },
           "rules_python_publish_deps_311_requests_toolbelt_py2_none_any_18565aa5": {
             "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
             "ruleClassName": "whl_library",
@@ -6721,6 +6840,26 @@
               ]
             }
           },
+          "rules_python_publish_deps_311_idna_sdist_12f65c9b": {
+            "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
+            "ruleClassName": "whl_library",
+            "attributes": {
+              "dep_template": "@rules_python_publish_deps//{name}:{target}",
+              "experimental_target_platforms": [
+                "cp311_osx_aarch64",
+                "cp311_osx_x86_64",
+                "cp311_windows_x86_64"
+              ],
+              "filename": "idna-3.10.tar.gz",
+              "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
+              "repo": "rules_python_publish_deps_311",
+              "requirement": "idna==3.10",
+              "sha256": "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9",
+              "urls": [
+                "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz"
+              ]
+            }
+          },
           "rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_aarch64_887623fe": {
             "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
             "ruleClassName": "whl_library",
@@ -7104,6 +7243,26 @@
               ]
             }
           },
+          "rules_python_publish_deps_311_docutils_sdist_3a6b1873": {
+            "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
+            "ruleClassName": "whl_library",
+            "attributes": {
+              "dep_template": "@rules_python_publish_deps//{name}:{target}",
+              "experimental_target_platforms": [
+                "cp311_osx_aarch64",
+                "cp311_osx_x86_64",
+                "cp311_windows_x86_64"
+              ],
+              "filename": "docutils-0.21.2.tar.gz",
+              "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
+              "repo": "rules_python_publish_deps_311",
+              "requirement": "docutils==0.21.2",
+              "sha256": "3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f",
+              "urls": [
+                "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz"
+              ]
+            }
+          },
           "rules_python_publish_deps_311_cryptography_sdist_831a4b37": {
             "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
             "ruleClassName": "whl_library",
@@ -7146,6 +7305,26 @@
               ]
             }
           },
+          "rules_python_publish_deps_311_certifi_py3_none_any_922820b5": {
+            "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
+            "ruleClassName": "whl_library",
+            "attributes": {
+              "dep_template": "@rules_python_publish_deps//{name}:{target}",
+              "experimental_target_platforms": [
+                "cp311_osx_aarch64",
+                "cp311_osx_x86_64",
+                "cp311_windows_x86_64"
+              ],
+              "filename": "certifi-2024.8.30-py3-none-any.whl",
+              "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
+              "repo": "rules_python_publish_deps_311",
+              "requirement": "certifi==2024.8.30",
+              "sha256": "922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8",
+              "urls": [
+                "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl"
+              ]
+            }
+          },
           "rules_python_publish_deps_311_zipp_sdist_bf1dcf64": {
             "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
             "ruleClassName": "whl_library",
@@ -7191,6 +7370,26 @@
               ]
             }
           },
+          "rules_python_publish_deps_311_certifi_sdist_bec941d2": {
+            "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
+            "ruleClassName": "whl_library",
+            "attributes": {
+              "dep_template": "@rules_python_publish_deps//{name}:{target}",
+              "experimental_target_platforms": [
+                "cp311_osx_aarch64",
+                "cp311_osx_x86_64",
+                "cp311_windows_x86_64"
+              ],
+              "filename": "certifi-2024.8.30.tar.gz",
+              "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
+              "repo": "rules_python_publish_deps_311",
+              "requirement": "certifi==2024.8.30",
+              "sha256": "bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9",
+              "urls": [
+                "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz"
+              ]
+            }
+          },
           "rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_x86_64_ce8613be": {
             "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
             "ruleClassName": "whl_library",
@@ -7497,7 +7696,7 @@
               ]
             }
           },
-          "rules_python_publish_deps_311_certifi_sdist_5a1e7645": {
+          "rules_python_publish_deps_311_idna_py3_none_any_946d195a": {
             "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
             "ruleClassName": "whl_library",
             "attributes": {
@@ -7507,13 +7706,13 @@
                 "cp311_osx_x86_64",
                 "cp311_windows_x86_64"
               ],
-              "filename": "certifi-2024.7.4.tar.gz",
+              "filename": "idna-3.10-py3-none-any.whl",
               "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
               "repo": "rules_python_publish_deps_311",
-              "requirement": "certifi==2024.7.4",
-              "sha256": "5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b",
+              "requirement": "idna==3.10",
+              "sha256": "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3",
               "urls": [
-                "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz"
+                "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl"
               ]
             }
           },
@@ -7850,10 +8049,7 @@
                 "cp311_linux_arm",
                 "cp311_linux_ppc",
                 "cp311_linux_s390x",
-                "cp311_linux_x86_64",
-                "cp311_osx_aarch64",
-                "cp311_osx_x86_64",
-                "cp311_windows_x86_64"
+                "cp311_linux_x86_64"
               ],
               "filename": "docutils-0.19-py3-none-any.whl",
               "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
@@ -7875,10 +8071,7 @@
                 "cp311_linux_arm",
                 "cp311_linux_ppc",
                 "cp311_linux_s390x",
-                "cp311_linux_x86_64",
-                "cp311_osx_aarch64",
-                "cp311_osx_x86_64",
-                "cp311_windows_x86_64"
+                "cp311_linux_x86_64"
               ],
               "filename": "docutils-0.19.tar.gz",
               "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
@@ -7890,6 +8083,26 @@
               ]
             }
           },
+          "rules_python_publish_deps_311_docutils_py3_none_any_dafca5b9": {
+            "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
+            "ruleClassName": "whl_library",
+            "attributes": {
+              "dep_template": "@rules_python_publish_deps//{name}:{target}",
+              "experimental_target_platforms": [
+                "cp311_osx_aarch64",
+                "cp311_osx_x86_64",
+                "cp311_windows_x86_64"
+              ],
+              "filename": "docutils-0.21.2-py3-none-any.whl",
+              "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
+              "repo": "rules_python_publish_deps_311",
+              "requirement": "docutils==0.21.2",
+              "sha256": "dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2",
+              "urls": [
+                "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl"
+              ]
+            }
+          },
           "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_win_amd64_66394663": {
             "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
             "ruleClassName": "whl_library",
@@ -7910,26 +8123,6 @@
               ]
             }
           },
-          "rules_python_publish_deps_311_idna_sdist_028ff3aa": {
-            "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
-            "ruleClassName": "whl_library",
-            "attributes": {
-              "dep_template": "@rules_python_publish_deps//{name}:{target}",
-              "experimental_target_platforms": [
-                "cp311_osx_aarch64",
-                "cp311_osx_x86_64",
-                "cp311_windows_x86_64"
-              ],
-              "filename": "idna-3.7.tar.gz",
-              "python_interpreter_target": "@@rules_python~~python~python_3_11_host//:python",
-              "repo": "rules_python_publish_deps_311",
-              "requirement": "idna==3.7",
-              "sha256": "028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
-              "urls": [
-                "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz"
-              ]
-            }
-          },
           "rules_python_publish_deps_311_jeepney_sdist_5efe48d2": {
             "bzlFile": "@@rules_python~//python/private/pypi:whl_library.bzl",
             "ruleClassName": "whl_library",
@@ -8077,19 +8270,19 @@
               "whl_map": {
                 "six": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"six-1.16.0-py2.py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_six_py2_none_any_8abb2f1d\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"six-1.16.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_six_sdist_1e61c374\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "cffi": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_aarch64_3548db28\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_ppc64le_91fc98ad\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_x86_64_94411f22\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_x86_64_cc4d65ae\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"cffi-1.15.1.tar.gz\",\"repo\":\"rules_python_publish_deps_311_cffi_sdist_d400bfb9\",\"target_platforms\":null,\"version\":\"3.11\"}]",
-                "idna": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"idna-3.4-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_idna_py3_none_any_90b77e79\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"idna-3.4.tar.gz\",\"repo\":\"rules_python_publish_deps_311_idna_sdist_814f528e\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"idna-3.7-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_idna_py3_none_any_82fee1fc\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"idna-3.7.tar.gz\",\"repo\":\"rules_python_publish_deps_311_idna_sdist_028ff3aa\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"}]",
+                "idna": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"idna-3.10-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_idna_py3_none_any_946d195a\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"idna-3.10.tar.gz\",\"repo\":\"rules_python_publish_deps_311_idna_sdist_12f65c9b\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"idna-3.4-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_idna_py3_none_any_90b77e79\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"idna-3.4.tar.gz\",\"repo\":\"rules_python_publish_deps_311_idna_sdist_814f528e\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"}]",
                 "rich": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"rich-13.2.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_rich_py3_none_any_7c963f0d\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"rich-13.2.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_rich_sdist_f1a00cdd\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "zipp": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"zipp-3.11.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_zipp_py3_none_any_83a28fcb\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"zipp-3.11.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_zipp_sdist_a7a22e05\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"zipp-3.19.2-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_zipp_py3_none_any_f091755f\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"zipp-3.19.2.tar.gz\",\"repo\":\"rules_python_publish_deps_311_zipp_sdist_bf1dcf64\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"}]",
                 "mdurl": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"mdurl-0.1.2-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_mdurl_py3_none_any_84008a41\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"mdurl-0.1.2.tar.gz\",\"repo\":\"rules_python_publish_deps_311_mdurl_sdist_bb413d29\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "twine": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"twine-4.0.2-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_twine_py3_none_any_929bc3c2\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"twine-4.0.2.tar.gz\",\"repo\":\"rules_python_publish_deps_311_twine_sdist_9e102ef5\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "bleach": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"bleach-6.0.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_bleach_py3_none_any_33c16e33\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"bleach-6.0.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_bleach_sdist_1a1a85c1\",\"target_platforms\":null,\"version\":\"3.11\"}]",
-                "certifi": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"certifi-2022.12.7-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_certifi_py3_none_any_4ad3232f\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"certifi-2022.12.7.tar.gz\",\"repo\":\"rules_python_publish_deps_311_certifi_sdist_35824b4c\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"certifi-2024.7.4-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_certifi_py3_none_any_c198e21b\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"certifi-2024.7.4.tar.gz\",\"repo\":\"rules_python_publish_deps_311_certifi_sdist_5a1e7645\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"}]",
+                "certifi": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"certifi-2022.12.7-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_certifi_py3_none_any_4ad3232f\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"certifi-2022.12.7.tar.gz\",\"repo\":\"rules_python_publish_deps_311_certifi_sdist_35824b4c\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"certifi-2024.8.30-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_certifi_py3_none_any_922820b5\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"certifi-2024.8.30.tar.gz\",\"repo\":\"rules_python_publish_deps_311_certifi_sdist_bec941d2\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"}]",
                 "jeepney": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"jeepney-0.8.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_jeepney_py3_none_any_c0a454ad\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"jeepney-0.8.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_jeepney_sdist_5efe48d2\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "keyring": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"keyring-23.13.1-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_keyring_py3_none_any_771ed2a9\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"keyring-23.13.1.tar.gz\",\"repo\":\"rules_python_publish_deps_311_keyring_sdist_ba2e15a9\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "pkginfo": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"pkginfo-1.9.6-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_pkginfo_py3_none_any_4b7a555a\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"pkginfo-1.9.6.tar.gz\",\"repo\":\"rules_python_publish_deps_311_pkginfo_sdist_8fd5896e\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "rfc3986": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"rfc3986-2.0.0-py2.py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_rfc3986_py2_none_any_50b1502b\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"rfc3986-2.0.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_rfc3986_sdist_97aacf9d\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "urllib3": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"urllib3-1.26.14-py2.py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_urllib3_py2_none_any_75edcdc2\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"urllib3-1.26.14.tar.gz\",\"repo\":\"rules_python_publish_deps_311_urllib3_sdist_076907bf\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"urllib3-1.26.19-py2.py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_urllib3_py2_none_any_37a03444\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"urllib3-1.26.19.tar.gz\",\"repo\":\"rules_python_publish_deps_311_urllib3_sdist_3e3d753a\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"}]",
-                "docutils": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"docutils-0.19-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_docutils_py3_none_any_5e1de4d8\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"docutils-0.19.tar.gz\",\"repo\":\"rules_python_publish_deps_311_docutils_sdist_33995a67\",\"target_platforms\":null,\"version\":\"3.11\"}]",
+                "docutils": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"docutils-0.19-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_docutils_py3_none_any_5e1de4d8\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"docutils-0.19.tar.gz\",\"repo\":\"rules_python_publish_deps_311_docutils_sdist_33995a67\",\"target_platforms\":[\"cp311_linux_aarch64\",\"cp311_linux_arm\",\"cp311_linux_ppc\",\"cp311_linux_s390x\",\"cp311_linux_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"docutils-0.21.2-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_docutils_py3_none_any_dafca5b9\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"docutils-0.21.2.tar.gz\",\"repo\":\"rules_python_publish_deps_311_docutils_sdist_3a6b1873\",\"target_platforms\":[\"cp311_osx_aarch64\",\"cp311_osx_x86_64\",\"cp311_windows_x86_64\"],\"version\":\"3.11\"}]",
                 "pygments": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"Pygments-2.14.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_pygments_py3_none_any_fa7bd7bd\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"Pygments-2.14.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_pygments_sdist_b3ed06a9\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "requests": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"requests-2.28.2-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_requests_py3_none_any_64299f49\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"requests-2.28.2.tar.gz\",\"repo\":\"rules_python_publish_deps_311_requests_sdist_98b1b278\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "pycparser": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"pycparser-2.21-py2.py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_pycparser_py2_none_any_8ee45429\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"pycparser-2.21.tar.gz\",\"repo\":\"rules_python_publish_deps_311_pycparser_sdist_e644fdec\",\"target_platforms\":null,\"version\":\"3.11\"}]",
@@ -8105,7 +8298,6 @@
                 "importlib_metadata": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"importlib_metadata-6.0.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_importlib_metadata_py3_none_any_7efb448e\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"importlib_metadata-6.0.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_importlib_metadata_sdist_e354bede\",\"target_platforms\":null,\"version\":\"3.11\"}]",
                 "pywin32_ctypes": "[{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"pywin32_ctypes-0.2.0-py2.py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_pywin32_ctypes_py2_none_any_9dc2d991\",\"target_platforms\":null,\"version\":\"3.11\"},{\"config_setting\":\"//_config:is_python_3.11\",\"filename\":\"pywin32-ctypes-0.2.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_pywin32_ctypes_sdist_24ffc3b3\",\"target_platforms\":null,\"version\":\"3.11\"}]"
               },
-              "default_version": "3.9",
               "packages": [
                 "bleach",
                 "certifi",
diff --git a/examples/bzlmod/tests/BUILD.bazel b/examples/bzlmod/tests/BUILD.bazel
index 9f7aa1b..7cbc8d4 100644
--- a/examples/bzlmod/tests/BUILD.bazel
+++ b/examples/bzlmod/tests/BUILD.bazel
@@ -1,9 +1,10 @@
 load("@python_versions//3.10:defs.bzl", py_binary_3_10 = "py_binary", py_test_3_10 = "py_test")
 load("@python_versions//3.11:defs.bzl", py_binary_3_11 = "py_binary", py_test_3_11 = "py_test")
 load("@python_versions//3.9:defs.bzl", py_binary_3_9 = "py_binary", py_test_3_9 = "py_test")
+load("@pythons_hub//:versions.bzl", "MINOR_MAPPING")
 load("@rules_python//python:defs.bzl", "py_binary", "py_test")
-load("@rules_python//python:versions.bzl", "MINOR_MAPPING")
 load("@rules_python//python/config_settings:transition.bzl", py_versioned_binary = "py_binary", py_versioned_test = "py_test")
+load("@rules_shell//shell:sh_test.bzl", "sh_test")
 
 py_binary(
     name = "version_default",
diff --git a/examples/bzlmod_build_file_generation/requirements_lock.txt b/examples/bzlmod_build_file_generation/requirements_lock.txt
index 8ba315b..9d9ad94 100644
--- a/examples/bzlmod_build_file_generation/requirements_lock.txt
+++ b/examples/bzlmod_build_file_generation/requirements_lock.txt
@@ -8,9 +8,9 @@
     --hash=sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907 \
     --hash=sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7
     # via pylint
-certifi==2023.7.22 \
-    --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
-    --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
+certifi==2024.7.4 \
+    --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
+    --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
     # via requests
 chardet==4.0.0 \
     --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
diff --git a/examples/bzlmod_build_file_generation/requirements_windows.txt b/examples/bzlmod_build_file_generation/requirements_windows.txt
index 09971f9..5b31ff5 100644
--- a/examples/bzlmod_build_file_generation/requirements_windows.txt
+++ b/examples/bzlmod_build_file_generation/requirements_windows.txt
@@ -8,9 +8,9 @@
     --hash=sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907 \
     --hash=sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7
     # via pylint
-certifi==2023.7.22 \
-    --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
-    --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
+certifi==2024.7.4 \
+    --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
+    --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
     # via requests
 chardet==4.0.0 \
     --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
diff --git a/examples/multi_python_versions/MODULE.bazel b/examples/multi_python_versions/MODULE.bazel
index 1e5d32e..4223916 100644
--- a/examples/multi_python_versions/MODULE.bazel
+++ b/examples/multi_python_versions/MODULE.bazel
@@ -55,3 +55,6 @@
     python_version = "3.11",
     requirements_lock = "//requirements:requirements_lock_3_11.txt",
 )
+
+# example test dependencies
+bazel_dep(name = "rules_shell", version = "0.2.0", dev_dependency = True)
diff --git a/examples/multi_python_versions/WORKSPACE b/examples/multi_python_versions/WORKSPACE
index 4f731d9..48d2065 100644
--- a/examples/multi_python_versions/WORKSPACE
+++ b/examples/multi_python_versions/WORKSPACE
@@ -45,3 +45,19 @@
 load("@pypi//:requirements.bzl", "install_deps")
 
 install_deps()
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+# See https://github.com/bazelbuild/rules_shell/releases/tag/v0.2.0
+http_archive(
+    name = "rules_shell",
+    sha256 = "410e8ff32e018b9efd2743507e7595c26e2628567c42224411ff533b57d27c28",
+    strip_prefix = "rules_shell-0.2.0",
+    url = "https://github.com/bazelbuild/rules_shell/releases/download/v0.2.0/rules_shell-v0.2.0.tar.gz",
+)
+
+load("@rules_shell//shell:repositories.bzl", "rules_shell_dependencies", "rules_shell_toolchains")
+
+rules_shell_dependencies()
+
+rules_shell_toolchains()
diff --git a/examples/multi_python_versions/tests/BUILD.bazel b/examples/multi_python_versions/tests/BUILD.bazel
index 5df41bd..cf14bf0 100644
--- a/examples/multi_python_versions/tests/BUILD.bazel
+++ b/examples/multi_python_versions/tests/BUILD.bazel
@@ -4,6 +4,7 @@
 load("@python//3.8:defs.bzl", py_binary_3_8 = "py_binary", py_test_3_8 = "py_test")
 load("@python//3.9:defs.bzl", py_binary_3_9 = "py_binary", py_test_3_9 = "py_test")
 load("@rules_python//python:defs.bzl", "py_binary", "py_test")
+load("@rules_shell//shell:sh_test.bzl", "sh_test")
 
 copy_file(
     name = "copy_version",
diff --git a/examples/pip_parse/requirements_lock.txt b/examples/pip_parse/requirements_lock.txt
index 4e8af7f..5e7a198 100644
--- a/examples/pip_parse/requirements_lock.txt
+++ b/examples/pip_parse/requirements_lock.txt
@@ -12,9 +12,9 @@
     --hash=sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900 \
     --hash=sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed
     # via sphinx
-certifi==2023.7.22 \
-    --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
-    --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
+certifi==2024.7.4 \
+    --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
+    --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
     # via requests
 chardet==4.0.0 \
     --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
@@ -226,9 +226,9 @@
     --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \
     --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b
     # via -r requirements.in
-zipp==3.17.0 \
-    --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \
-    --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0
+zipp==3.19.1 \
+    --hash=sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091 \
+    --hash=sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f
     # via importlib-metadata
 
 # The following packages are considered to be unsafe in a requirements file:
diff --git a/examples/pip_parse/requirements_windows.txt b/examples/pip_parse/requirements_windows.txt
index 4debc11..4b19692 100644
--- a/examples/pip_parse/requirements_windows.txt
+++ b/examples/pip_parse/requirements_windows.txt
@@ -12,9 +12,9 @@
     --hash=sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900 \
     --hash=sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed
     # via sphinx
-certifi==2023.7.22 \
-    --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
-    --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
+certifi==2024.7.4 \
+    --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
+    --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
     # via requests
 chardet==4.0.0 \
     --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa \
@@ -230,9 +230,9 @@
     --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \
     --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b
     # via -r requirements.in
-zipp==3.17.0 \
-    --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \
-    --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0
+zipp==3.19.1 \
+    --hash=sha256:2828e64edb5386ea6a52e7ba7cdb17bb30a73a858f5eb6eb93d8d36f5ea26091 \
+    --hash=sha256:35427f6d5594f4acf82d25541438348c26736fa9b3afa2754bcd63cdb99d8e8f
     # via importlib-metadata
 
 # The following packages are considered to be unsafe in a requirements file:
diff --git a/examples/wheel/lib/BUILD.bazel b/examples/wheel/lib/BUILD.bazel
index 3b59662..755818d 100644
--- a/examples/wheel/lib/BUILD.bazel
+++ b/examples/wheel/lib/BUILD.bazel
@@ -26,7 +26,10 @@
 py_library(
     name = "module_with_data",
     srcs = ["module_with_data.py"],
-    data = [":data.txt"],
+    data = [
+        "data,with,commas.txt",
+        ":data.txt",
+    ],
 )
 
 genrule(
@@ -34,3 +37,9 @@
     outs = ["data.txt"],
     cmd = "echo foo bar baz > $@",
 )
+
+genrule(
+    name = "make_data_with_commas_in_name",
+    outs = ["data,with,commas.txt"],
+    cmd = "echo foo bar baz > $@",
+)
diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py
index 7212423..4494ee1 100644
--- a/examples/wheel/wheel_test.py
+++ b/examples/wheel/wheel_test.py
@@ -95,6 +95,7 @@
             self.assertEqual(
                 zf.namelist(),
                 [
+                    "examples/wheel/lib/data,with,commas.txt",
                     "examples/wheel/lib/data.txt",
                     "examples/wheel/lib/module_with_data.py",
                     "examples/wheel/lib/simple_module.py",
@@ -105,7 +106,7 @@
                 ],
             )
         self.assertFileSha256Equal(
-            filename, "b4815a1d3a17cc6a5ce717ed42b940fa7788cb5168f5c1de02f5f50abed7083e"
+            filename, "82370bf61310e2d3c7b1218368457dc7e161bf5dc1a280d7d45102b5e56acf43"
         )
 
     def test_customized_wheel(self):
@@ -117,6 +118,7 @@
             self.assertEqual(
                 zf.namelist(),
                 [
+                    "examples/wheel/lib/data,with,commas.txt",
                     "examples/wheel/lib/data.txt",
                     "examples/wheel/lib/module_with_data.py",
                     "examples/wheel/lib/simple_module.py",
@@ -140,6 +142,7 @@
                 record_contents,
                 # The entries are guaranteed to be sorted.
                 b"""\
+"examples/wheel/lib/data,with,commas.txt",sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12
 examples/wheel/lib/data.txt,sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg,12
 examples/wheel/lib/module_with_data.py,sha256=8s0Khhcqz3yVsBKv2IB5u4l4TMKh7-c_V6p65WVHPms,637
 examples/wheel/lib/simple_module.py,sha256=z2hwciab_XPNIBNH8B1Q5fYgnJvQTeYf0ZQJpY8yLLY,637
@@ -194,7 +197,7 @@
 second = second.main:s""",
             )
         self.assertFileSha256Equal(
-            filename, "27f3038be6e768d28735441a1bc567eca2213bd3568d18b22a414e6399a2d48e"
+            filename, "706e8dd45884d8cb26e92869f7d29ab7ed9f683b4e2d08f06c03dbdaa12191b8"
         )
 
     def test_filename_escaping(self):
@@ -205,6 +208,7 @@
             self.assertEqual(
                 zf.namelist(),
                 [
+                    "examples/wheel/lib/data,with,commas.txt",
                     "examples/wheel/lib/data.txt",
                     "examples/wheel/lib/module_with_data.py",
                     "examples/wheel/lib/simple_module.py",
@@ -241,6 +245,7 @@
             self.assertEqual(
                 zf.namelist(),
                 [
+                    "wheel/lib/data,with,commas.txt",
                     "wheel/lib/data.txt",
                     "wheel/lib/module_with_data.py",
                     "wheel/lib/simple_module.py",
@@ -260,7 +265,7 @@
             for line in record_contents.splitlines():
                 self.assertFalse(line.startswith("/"))
         self.assertFileSha256Equal(
-            filename, "f034b3278781f4df32a33df70d794bb94170b450e477c8bd9cd42d2d922476ae"
+            filename, "568922541703f6edf4b090a8413991f9fa625df2844e644dd30bdbe9deb660be"
         )
 
     def test_custom_package_root_multi_prefix_wheel(self):
@@ -273,6 +278,7 @@
             self.assertEqual(
                 zf.namelist(),
                 [
+                    "data,with,commas.txt",
                     "data.txt",
                     "module_with_data.py",
                     "simple_module.py",
@@ -291,7 +297,7 @@
             for line in record_contents.splitlines():
                 self.assertFalse(line.startswith("/"))
         self.assertFileSha256Equal(
-            filename, "ff19f5e4540948247742716338bb4194d619cb56df409045d1a99f265ce8e36c"
+            filename, "a8b91ce9d6f570e97b40a357a292a6f595d3470f07c479cb08550257cc9c8306"
         )
 
     def test_custom_package_root_multi_prefix_reverse_order_wheel(self):
@@ -304,6 +310,7 @@
             self.assertEqual(
                 zf.namelist(),
                 [
+                    "lib/data,with,commas.txt",
                     "lib/data.txt",
                     "lib/module_with_data.py",
                     "lib/simple_module.py",
@@ -322,7 +329,7 @@
             for line in record_contents.splitlines():
                 self.assertFalse(line.startswith("/"))
         self.assertFileSha256Equal(
-            filename, "4331e378ea8b8148409ae7c02177e4eb24d151a85ef937bb44b79ff5258d634b"
+            filename, "8f44e940731757c186079a42cfe7ea3d43cd96b526e3fb2ca2a3ea3048a9d489"
         )
 
     def test_python_requires_wheel(self):
@@ -347,7 +354,7 @@
 """,
             )
         self.assertFileSha256Equal(
-            filename, "b34676828f93da8cd898d50dcd4f36e02fe273150e213aacb999310a05f5f38c"
+            filename, "ba32493f5e43e481346384aaab9e8fa09c23884276ad057c5f432096a0350101"
         )
 
     def test_python_abi3_binary_wheel(self):
diff --git a/internal_deps.bzl b/internal_deps.bzl
index 56962cb..9c2e6b2 100644
--- a/internal_deps.bzl
+++ b/internal_deps.bzl
@@ -46,6 +46,14 @@
         ],
     )
 
+    # See https://github.com/bazelbuild/rules_shell/releases/tag/v0.2.0
+    http_archive(
+        name = "rules_shell",
+        sha256 = "410e8ff32e018b9efd2743507e7595c26e2628567c42224411ff533b57d27c28",
+        strip_prefix = "rules_shell-0.2.0",
+        url = "https://github.com/bazelbuild/rules_shell/releases/download/v0.2.0/rules_shell-v0.2.0.tar.gz",
+    )
+
     http_archive(
         name = "rules_pkg",
         urls = [
diff --git a/internal_setup.bzl b/internal_setup.bzl
index 1967c0e..b3dc326 100644
--- a/internal_setup.bzl
+++ b/internal_setup.bzl
@@ -21,14 +21,27 @@
 load("@rules_bazel_integration_test//bazel_integration_test:deps.bzl", "bazel_integration_test_rules_dependencies")
 load("@rules_bazel_integration_test//bazel_integration_test:repo_defs.bzl", "bazel_binaries")
 load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains")
+load("@rules_shell//shell:repositories.bzl", "rules_shell_dependencies", "rules_shell_toolchains")
 load("//:version.bzl", "SUPPORTED_BAZEL_VERSIONS")
+load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
 load("//python/private:internal_config_repo.bzl", "internal_config_repo")  # buildifier: disable=bzl-visibility
+load("//python/private:pythons_hub.bzl", "hub_repo")  # buildifier: disable=bzl-visibility
 load("//python/private/pypi:deps.bzl", "pypi_deps")  # buildifier: disable=bzl-visibility
 
 def rules_python_internal_setup():
     """Setup for rules_python tests and tools."""
 
     internal_config_repo(name = "rules_python_internal")
+    hub_repo(
+        name = "pythons_hub",
+        minor_mapping = MINOR_MAPPING,
+        default_python_version = "",
+        toolchain_prefixes = [],
+        toolchain_python_versions = [],
+        toolchain_set_python_version_constraints = [],
+        toolchain_user_repository_names = [],
+        python_versions = sorted(TOOL_VERSIONS.keys()),
+    )
 
     # Because we don't use the pip_install rule, we have to call this to fetch its deps
     pypi_deps()
@@ -44,3 +57,5 @@
     bazel_starlib_dependencies()
     bazel_binaries(versions = SUPPORTED_BAZEL_VERSIONS)
     bazel_features_deps()
+    rules_shell_dependencies()
+    rules_shell_toolchains()
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index b7a2172..f2f3374 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -34,6 +34,7 @@
 filegroup(
     name = "distribution",
     srcs = glob(["**"]) + [
+        "//python/api:distribution",
         "//python/cc:distribution",
         "//python/config_settings:distribution",
         "//python/constraints:distribution",
@@ -123,9 +124,9 @@
     name = "py_binary_bzl",
     srcs = ["py_binary.bzl"],
     deps = [
+        "//python/private:py_binary_macro_bazel_bzl",
         "//python/private:register_extension_info_bzl",
         "//python/private:util_bzl",
-        "//python/private/common:py_binary_macro_bazel_bzl",
         "@rules_python_internal//:rules_python_config_bzl",
     ],
 )
@@ -134,7 +135,7 @@
     name = "py_cc_link_params_info_bzl",
     srcs = ["py_cc_link_params_info.bzl"],
     deps = [
-        "//python/private/common:providers_bzl",
+        "//python/private:py_cc_link_params_info_bzl",
         "@rules_python_internal//:rules_python_config_bzl",
     ],
 )
@@ -167,8 +168,8 @@
     name = "py_info_bzl",
     srcs = ["py_info.bzl"],
     deps = [
+        "//python/private:py_info_bzl",
         "//python/private:reexports_bzl",
-        "//python/private/common:providers_bzl",
         "@rules_python_internal//:rules_python_config_bzl",
     ],
 )
@@ -177,9 +178,9 @@
     name = "py_library_bzl",
     srcs = ["py_library.bzl"],
     deps = [
+        "//python/private:py_library_macro_bazel_bzl",
         "//python/private:register_extension_info_bzl",
         "//python/private:util_bzl",
-        "//python/private/common:py_library_macro_bazel_bzl",
         "@rules_python_internal//:rules_python_config_bzl",
     ],
 )
@@ -188,8 +189,8 @@
     name = "py_runtime_bzl",
     srcs = ["py_runtime.bzl"],
     deps = [
+        "//python/private:py_runtime_macro_bzl",
         "//python/private:util_bzl",
-        "//python/private/common:py_runtime_macro_bzl",
     ],
 )
 
@@ -207,9 +208,9 @@
     name = "py_runtime_info_bzl",
     srcs = ["py_runtime_info.bzl"],
     deps = [
+        "//python/private:py_runtime_info_bzl",
         "//python/private:reexports_bzl",
         "//python/private:util_bzl",
-        "//python/private/common:providers_bzl",
         "@rules_python_internal//:rules_python_config_bzl",
     ],
 )
@@ -218,9 +219,9 @@
     name = "py_test_bzl",
     srcs = ["py_test.bzl"],
     deps = [
+        "//python/private:py_test_macro_bazel_bzl",
         "//python/private:register_extension_info_bzl",
         "//python/private:util_bzl",
-        "//python/private/common:py_test_macro_bazel_bzl",
         "@rules_python_internal//:rules_python_config_bzl",
     ],
 )
diff --git a/python/api/BUILD.bazel b/python/api/BUILD.bazel
new file mode 100644
index 0000000..1df6877
--- /dev/null
+++ b/python/api/BUILD.bazel
@@ -0,0 +1,31 @@
+# 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("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+
+package(
+    default_visibility = ["//:__subpackages__"],
+)
+
+bzl_library(
+    name = "api_bzl",
+    srcs = ["api.bzl"],
+    visibility = ["//visibility:public"],
+    deps = ["//python/private/api:api_bzl"],
+)
+
+filegroup(
+    name = "distribution",
+    srcs = glob(["**"]),
+)
diff --git a/python/api/api.bzl b/python/api/api.bzl
new file mode 100644
index 0000000..c8fb921
--- /dev/null
+++ b/python/api/api.bzl
@@ -0,0 +1,5 @@
+"""Public, analysis phase APIs for Python rules."""
+
+load("//python/private/api:api.bzl", _py_common = "py_common")
+
+py_common = _py_common
diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel
index c31d69f..c530afe 100644
--- a/python/config_settings/BUILD.bazel
+++ b/python/config_settings/BUILD.bazel
@@ -1,13 +1,12 @@
 load("@bazel_skylib//rules:common_settings.bzl", "string_flag")
-load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
+load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING", "PYTHON_VERSIONS")
 load(
     "//python/private:flags.bzl",
+    "AddSrcsToRunfilesFlag",
     "BootstrapImplFlag",
     "ExecToolsToolchainFlag",
-    "PrecompileAddToRunfilesFlag",
     "PrecompileFlag",
     "PrecompileSourceRetentionFlag",
-    "PycCollectionFlag",
 )
 load(
     "//python/private/pypi:flags.bzl",
@@ -28,13 +27,22 @@
 
 construct_config_settings(
     name = "construct_config_settings",
+    default_version = DEFAULT_PYTHON_VERSION,
     minor_mapping = MINOR_MAPPING,
-    versions = TOOL_VERSIONS.keys(),
+    versions = PYTHON_VERSIONS,
+)
+
+string_flag(
+    name = "add_srcs_to_runfiles",
+    build_setting_default = AddSrcsToRunfilesFlag.AUTO,
+    values = AddSrcsToRunfilesFlag.flag_values(),
+    # NOTE: Only public because it is dependency of public rules.
+    visibility = ["//visibility:public"],
 )
 
 string_flag(
     name = "exec_tools_toolchain",
-    build_setting_default = ExecToolsToolchainFlag.DISABLED,
+    build_setting_default = ExecToolsToolchainFlag.ENABLED,
     values = sorted(ExecToolsToolchainFlag.__members__.values()),
     # NOTE: Only public because it is used in py_toolchain_suite from toolchain
     # repositories
@@ -68,22 +76,6 @@
 )
 
 string_flag(
-    name = "precompile_add_to_runfiles",
-    build_setting_default = PrecompileAddToRunfilesFlag.ALWAYS,
-    values = sorted(PrecompileAddToRunfilesFlag.__members__.values()),
-    # NOTE: Only public because it's an implicit dependency
-    visibility = ["//visibility:public"],
-)
-
-string_flag(
-    name = "pyc_collection",
-    build_setting_default = PycCollectionFlag.DISABLED,
-    values = sorted(PycCollectionFlag.__members__.values()),
-    # NOTE: Only public because it's an implicit dependency
-    visibility = ["//visibility:public"],
-)
-
-string_flag(
     name = "bootstrap_impl",
     build_setting_default = BootstrapImplFlag.SYSTEM_PYTHON,
     values = sorted(BootstrapImplFlag.__members__.values()),
diff --git a/python/config_settings/transition.bzl b/python/config_settings/transition.bzl
index 7ac41f8..a7646dc 100644
--- a/python/config_settings/transition.bzl
+++ b/python/config_settings/transition.bzl
@@ -101,12 +101,12 @@
     ]
     if PyInfo in target:
         providers.append(target[PyInfo])
-    if BuiltinPyInfo in target and PyInfo != BuiltinPyInfo:
+    if BuiltinPyInfo != None and BuiltinPyInfo in target and PyInfo != BuiltinPyInfo:
         providers.append(target[BuiltinPyInfo])
 
     if PyRuntimeInfo in target:
         providers.append(target[PyRuntimeInfo])
-    if BuiltinPyRuntimeInfo in target and PyRuntimeInfo != BuiltinPyRuntimeInfo:
+    if BuiltinPyRuntimeInfo != None and BuiltinPyRuntimeInfo in target and PyRuntimeInfo != BuiltinPyRuntimeInfo:
         providers.append(target[BuiltinPyRuntimeInfo])
     return providers
 
diff --git a/python/extensions/python.bzl b/python/extensions/python.bzl
index 0f0da00..abd5080 100644
--- a/python/extensions/python.bzl
+++ b/python/extensions/python.bzl
@@ -14,7 +14,7 @@
 
 """Python toolchain module extensions for use with bzlmod.
 
-:::{topic} Basic usage
+::::{topic} Basic usage
 
 The simplest way to configure the toolchain with `rules_python` is as follows.
 
@@ -27,22 +27,22 @@
 use_repo(python, "python_3_11")
 ```
 
-::::{seealso}
+:::{seealso}
 For more in-depth documentation see the {obj}`python.toolchain`.
-::::
 :::
+::::
 
-:::{topic} Overrides
+::::{topic} Overrides
 
 Overrides can be done at 3 different levels:
 * Overrides affecting all python toolchain versions on all platforms - {obj}`python.override`.
 * Overrides affecting a single toolchain versions on all platforms - {obj}`python.single_version_override`.
 * Overrides affecting a single toolchain versions on a single platforms - {obj}`python.single_version_platform_override`.
 
-::::{seealso}
+:::{seealso}
 The main documentation page on registering [toolchains](/toolchains).
-::::
 :::
+::::
 """
 
 load("//python/private:python.bzl", _python = "python")
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index bfe3764..d3b9bf4 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -30,7 +30,6 @@
 filegroup(
     name = "distribution",
     srcs = glob(["**"]) + [
-        "//python/private/common:distribution",
         "//python/private/proto:distribution",
         "//python/private/pypi:distribution",
         "//python/private/whl_filegroup:distribution",
@@ -53,6 +52,28 @@
 )
 
 bzl_library(
+    name = "attributes_bazel_bzl",
+    srcs = ["attributes_bazel.bzl"],
+    deps = ["//python/private:rules_cc_srcs_bzl"],
+)
+
+bzl_library(
+    name = "attributes_bzl",
+    srcs = ["attributes.bzl"],
+    deps = [
+        ":common_bzl",
+        ":py_internal_bzl",
+        ":semantics_bzl",
+        "//python/private:enum_bzl",
+        "//python/private:flags_bzl",
+        "//python/private:py_info_bzl",
+        "//python/private:reexports_bzl",
+        "//python/private:rules_cc_srcs_bzl",
+        "@bazel_skylib//rules:common_settings",
+    ],
+)
+
+bzl_library(
     name = "auth_bzl",
     srcs = ["auth.bzl"],
     deps = [":bazel_tools_bzl"],
@@ -70,11 +91,52 @@
 )
 
 bzl_library(
+    name = "builders_bzl",
+    srcs = ["builders.bzl"],
+    deps = [
+        "@bazel_skylib//lib:types",
+    ],
+)
+
+bzl_library(
     name = "bzlmod_enabled_bzl",
     srcs = ["bzlmod_enabled.bzl"],
 )
 
 bzl_library(
+    name = "cc_helper_bzl",
+    srcs = ["cc_helper.bzl"],
+    deps = [":py_internal_bzl"],
+)
+
+bzl_library(
+    name = "common_bazel_bzl",
+    srcs = ["common_bazel.bzl"],
+    deps = [
+        ":attributes_bzl",
+        ":common_bzl",
+        ":py_internal_bzl",
+        "//python/private:py_cc_link_params_info_bzl",
+        "//python/private:py_interpreter_program_bzl",
+        "//python/private:toolchain_types_bzl",
+        "@bazel_skylib//lib:paths",
+    ],
+)
+
+bzl_library(
+    name = "common_bzl",
+    srcs = ["common.bzl"],
+    deps = [
+        ":cc_helper_bzl",
+        ":py_internal_bzl",
+        ":semantics_bzl",
+        "//python/private:py_info_bzl",
+        "//python/private:reexports_bzl",
+        "//python/private:rules_cc_srcs_bzl",
+    ],
+)
+
+bzl_library(
     name = "config_settings_bzl",
     srcs = ["config_settings.bzl"],
     deps = [
@@ -152,22 +214,11 @@
 )
 
 bzl_library(
-    name = "py_repositories_bzl",
-    srcs = ["py_repositories.bzl"],
-    deps = [
-        ":bazel_tools_bzl",
-        ":internal_config_repo_bzl",
-        "//python/private/pypi:deps_bzl",
-    ],
-)
-
-bzl_library(
     name = "python_register_toolchains_bzl",
     srcs = ["python_register_toolchains.bzl"],
     deps = [
         ":auth_bzl",
         ":bazel_tools_bzl",
-        ":bzlmod_enabled_bzl",
         ":coverage_deps_bzl",
         ":full_version_bzl",
         ":internal_config_repo_bzl",
@@ -204,6 +255,36 @@
     srcs = ["pythons_hub.bzl"],
     deps = [
         ":py_toolchain_suite_bzl",
+        ":text_util_bzl",
+    ],
+)
+
+bzl_library(
+    name = "py_binary_macro_bazel_bzl",
+    srcs = ["py_binary_macro_bazel.bzl"],
+    deps = [
+        ":common_bzl",
+        ":py_binary_rule_bazel_bzl",
+    ],
+)
+
+bzl_library(
+    name = "py_binary_rule_bazel_bzl",
+    srcs = ["py_binary_rule_bazel.bzl"],
+    deps = [
+        ":attributes_bzl",
+        ":py_executable_bazel_bzl",
+        ":semantics_bzl",
+        "@bazel_skylib//lib:dicts",
+    ],
+)
+
+bzl_library(
+    name = "py_cc_link_params_info_bzl",
+    srcs = ["py_cc_link_params_info.bzl"],
+    deps = [
+        ":rules_cc_srcs_bzl",
+        ":util_bzl",
     ],
 )
 
@@ -255,27 +336,152 @@
         ":py_exec_tools_info_bzl",
         ":sentinel_bzl",
         ":toolchain_types_bzl",
-        "//python/private/common:providers_bzl",
         "@bazel_skylib//lib:paths",
         "@bazel_skylib//rules:common_settings",
     ],
 )
 
 bzl_library(
+    name = "py_executable_bazel_bzl",
+    srcs = ["py_executable_bazel.bzl"],
+    deps = [
+        ":attributes_bazel_bzl",
+        ":common_bazel_bzl",
+        ":common_bzl",
+        ":py_executable_bzl",
+        ":py_internal_bzl",
+        ":semantics_bzl",
+        "//python/private:py_runtime_info_bzl",
+    ],
+)
+
+bzl_library(
+    name = "py_executable_bzl",
+    srcs = ["py_executable.bzl"],
+    deps = [
+        ":attributes_bzl",
+        ":cc_helper_bzl",
+        ":common_bzl",
+        ":py_internal_bzl",
+        "//python/private:flags_bzl",
+        "//python/private:py_cc_link_params_info_bzl",
+        "//python/private:py_executable_info_bzl",
+        "//python/private:py_info_bzl",
+        "//python/private:py_runtime_info_bzl",
+        "//python/private:rules_cc_srcs_bzl",
+        "//python/private:toolchain_types_bzl",
+        "@bazel_skylib//lib:dicts",
+        "@bazel_skylib//lib:structs",
+        "@bazel_skylib//rules:common_settings",
+    ],
+)
+
+bzl_library(
     name = "py_executable_info_bzl",
     srcs = ["py_executable_info.bzl"],
 )
 
 bzl_library(
+    name = "py_info_bzl",
+    srcs = ["py_info.bzl"],
+    deps = [
+        ":builders_bzl",
+        ":reexports_bzl",
+        ":util_bzl",
+        "@rules_python_internal//:rules_python_config_bzl",
+    ],
+)
+
+bzl_library(
+    name = "py_internal_bzl",
+    srcs = ["py_internal.bzl"],
+    deps = ["@rules_python_internal//:py_internal_bzl"],
+)
+
+bzl_library(
     name = "py_interpreter_program_bzl",
     srcs = ["py_interpreter_program.bzl"],
     deps = ["@bazel_skylib//rules:common_settings"],
 )
 
 bzl_library(
+    name = "py_library_bzl",
+    srcs = ["py_library.bzl"],
+    deps = [
+        ":attributes_bzl",
+        ":common_bzl",
+        ":py_internal_bzl",
+        "//python/private:flags_bzl",
+        "//python/private:py_cc_link_params_info_bzl",
+        "//python/private:toolchain_types_bzl",
+        "@bazel_skylib//lib:dicts",
+        "@bazel_skylib//rules:common_settings",
+    ],
+)
+
+bzl_library(
+    name = "py_library_macro_bazel_bzl",
+    srcs = ["py_library_macro_bazel.bzl"],
+    deps = [":py_library_rule_bazel_bzl"],
+)
+
+bzl_library(
+    name = "py_library_rule_bazel_bzl",
+    srcs = ["py_library_rule_bazel.bzl"],
+    deps = [
+        ":attributes_bazel_bzl",
+        ":common_bazel_bzl",
+        ":common_bzl",
+        ":py_library_bzl",
+    ],
+)
+
+bzl_library(
     name = "py_package_bzl",
     srcs = ["py_package.bzl"],
     visibility = ["//:__subpackages__"],
+    deps = [
+        ":builders_bzl",
+        ":py_info_bzl",
+    ],
+)
+
+bzl_library(
+    name = "py_runtime_info_bzl",
+    srcs = ["py_runtime_info.bzl"],
+    deps = [":util_bzl"],
+)
+
+bzl_library(
+    name = "py_repositories_bzl",
+    srcs = ["py_repositories.bzl"],
+    deps = [
+        ":bazel_tools_bzl",
+        ":internal_config_repo_bzl",
+        ":pythons_hub_bzl",
+        "//python:versions_bzl",
+        "//python/private/pypi:deps_bzl",
+    ],
+)
+
+bzl_library(
+    name = "py_runtime_macro_bzl",
+    srcs = ["py_runtime_macro.bzl"],
+    deps = [":py_runtime_rule_bzl"],
+)
+
+bzl_library(
+    name = "py_runtime_rule_bzl",
+    srcs = ["py_runtime_rule.bzl"],
+    deps = [
+        ":py_runtime_info_bzl",
+        ":reexports_bzl",
+        ":util_bzl",
+        "//python/private:attributes_bzl",
+        "//python/private:py_internal_bzl",
+        "@bazel_skylib//lib:dicts",
+        "@bazel_skylib//lib:paths",
+    ],
 )
 
 bzl_library(
@@ -296,6 +502,27 @@
 )
 
 bzl_library(
+    name = "py_test_macro_bazel_bzl",
+    srcs = ["py_test_macro_bazel.bzl"],
+    deps = [
+        ":common_bazel_bzl",
+        ":py_test_rule_bazel_bzl",
+    ],
+)
+
+bzl_library(
+    name = "py_test_rule_bazel_bzl",
+    srcs = ["py_test_rule_bazel.bzl"],
+    deps = [
+        ":attributes_bzl",
+        ":common_bzl",
+        ":py_executable_bazel_bzl",
+        ":semantics_bzl",
+        "@bazel_skylib//lib:dicts",
+    ],
+)
+
+bzl_library(
     name = "py_toolchain_suite_bzl",
     srcs = ["py_toolchain_suite.bzl"],
     deps = [
@@ -322,7 +549,10 @@
     visibility = [
         "//:__subpackages__",
     ],
-    deps = [":bazel_tools_bzl"],
+    deps = [
+        ":bazel_tools_bzl",
+        "@rules_python_internal//:rules_python_config_bzl",
+    ],
 )
 
 bzl_library(
@@ -377,7 +607,10 @@
     visibility = [
         "//:__subpackages__",
     ],
-    deps = ["@bazel_skylib//lib:types"],
+    deps = [
+        "@bazel_skylib//lib:types",
+        "@rules_python_internal//:rules_python_config_bzl",
+    ],
 )
 
 bzl_library(
@@ -403,6 +636,11 @@
     deps = [":bazel_tools_bzl"],
 )
 
+bzl_library(
+    name = "semantics_bzl",
+    srcs = ["semantics.bzl"],
+)
+
 # Needed to define bzl_library targets for docgen. (We don't define the
 # bzl_library target here because it'd give our users a transitive dependency
 # on Skylib.)
diff --git a/python/private/api/BUILD.bazel b/python/private/api/BUILD.bazel
new file mode 100644
index 0000000..9e97dc2
--- /dev/null
+++ b/python/private/api/BUILD.bazel
@@ -0,0 +1,43 @@
+# 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("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load(":py_common_api.bzl", "py_common_api")
+
+package(
+    default_visibility = ["//:__subpackages__"],
+)
+
+py_common_api(
+    name = "py_common_api",
+    # NOTE: Not actually public. Implicit dependency of public rules.
+    visibility = ["//visibility:public"],
+)
+
+bzl_library(
+    name = "api_bzl",
+    srcs = ["api.bzl"],
+    deps = [
+        "//python/private:py_info_bzl",
+    ],
+)
+
+bzl_library(
+    name = "py_common_api_bzl",
+    srcs = ["py_common_api.bzl"],
+    deps = [
+        ":api_bzl",
+        "//python/private:py_info_bzl",
+    ],
+)
diff --git a/python/private/api/api.bzl b/python/private/api/api.bzl
new file mode 100644
index 0000000..06fb729
--- /dev/null
+++ b/python/private/api/api.bzl
@@ -0,0 +1,55 @@
+# 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.
+"""Implementation of py_api."""
+
+_PY_COMMON_API_LABEL = Label("//python/private/api:py_common_api")
+
+ApiImplInfo = provider(
+    doc = "Provider to hold an API implementation",
+    fields = {
+        "impl": """
+:type: struct
+
+The implementation of the API being provided. The object it contains
+will depend on the target that is providing the API struct.
+""",
+    },
+)
+
+def _py_common_get(ctx):
+    """Get the py_common API instance.
+
+    NOTE: to use this function, the rule must have added `py_common.API_ATTRS`
+    to its attributes.
+
+    Args:
+        ctx: {type}`ctx` current rule ctx
+
+    Returns:
+        {type}`PyCommonApi`
+    """
+
+    # A generic provider is used to decouple the API implementations from
+    # the loading phase of the rules using an implementation.
+    return ctx.attr._py_common_api[ApiImplInfo].impl
+
+py_common = struct(
+    get = _py_common_get,
+    API_ATTRS = {
+        "_py_common_api": attr.label(
+            default = _PY_COMMON_API_LABEL,
+            providers = [ApiImplInfo],
+        ),
+    },
+)
diff --git a/python/private/api/py_common_api.bzl b/python/private/api/py_common_api.bzl
new file mode 100644
index 0000000..401b359
--- /dev/null
+++ b/python/private/api/py_common_api.bzl
@@ -0,0 +1,38 @@
+# 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.
+"""Implementation of py_api."""
+
+load("//python/private:py_info.bzl", "PyInfoBuilder")
+load("//python/private/api:api.bzl", "ApiImplInfo")
+
+def _py_common_api_impl(ctx):
+    _ = ctx  # @unused
+    return [ApiImplInfo(impl = PyCommonApi)]
+
+py_common_api = rule(
+    implementation = _py_common_api_impl,
+    doc = "Rule implementing py_common API.",
+)
+
+def _merge_py_infos(transitive, *, direct = []):
+    builder = PyInfoBuilder()
+    builder.merge_all(transitive, direct = direct)
+    return builder.build()
+
+# Exposed for doc generation, not directly used.
+# buildifier: disable=name-conventions
+PyCommonApi = struct(
+    merge_py_infos = _merge_py_infos,
+    PyInfoBuilder = PyInfoBuilder,
+)
diff --git a/python/private/common/attributes.bzl b/python/private/attributes.bzl
similarity index 89%
rename from python/private/common/attributes.bzl
rename to python/private/attributes.bzl
index 90a5332..424a2c5 100644
--- a/python/private/common/attributes.bzl
+++ b/python/private/attributes.bzl
@@ -15,12 +15,12 @@
 
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("@rules_cc//cc:defs.bzl", "CcInfo")
-load("//python/private:enum.bzl", "enum")
-load("//python/private:flags.bzl", "PrecompileFlag", "PrecompileSourceRetentionFlag")
-load("//python/private:reexports.bzl", "BuiltinPyInfo")
 load(":common.bzl", "union_attrs")
-load(":providers.bzl", "PyInfo")
+load(":enum.bzl", "enum")
+load(":flags.bzl", "PrecompileFlag", "PrecompileSourceRetentionFlag")
+load(":py_info.bzl", "PyInfo")
 load(":py_internal.bzl", "py_internal")
+load(":reexports.bzl", "BuiltinPyInfo")
 load(
     ":semantics.bzl",
     "DEPS_ATTR_ALLOW_RULES",
@@ -29,6 +29,26 @@
 
 _PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None)
 
+# Due to how the common exec_properties attribute works, rules must add exec
+# groups even if they don't actually use them. This is due to two interactions:
+# 1. Rules give an error if users pass an unsupported exec group.
+# 2. exec_properties is configurable, so macro-code can't always filter out
+#    exec group names that aren't supported by the rule.
+# The net effect is, if a user passes exec_properties to a macro, and the macro
+# invokes two rules, the macro can't always ensure each rule is only passed
+# valid exec groups, and is thus liable to cause an error.
+#
+# NOTE: These are no-op/empty exec groups. If a rule *does* support an exec
+# group and needs custom settings, it should merge this dict with one that
+# overrides the supported key.
+REQUIRED_EXEC_GROUPS = {
+    # py_binary may invoke C++ linking, or py rules may be used in combination
+    # with cc rules (e.g. within the same macro), so support that exec group.
+    # This exec group is defined by rules_cc for the cc rules.
+    "cpp_link": exec_group(),
+    "py_precompile": exec_group(),
+}
+
 _STAMP_VALUES = [-1, 0, 1]
 
 def _precompile_attr_get_effective_value(ctx):
@@ -50,7 +70,6 @@
     if precompile not in (
         PrecompileAttr.ENABLED,
         PrecompileAttr.DISABLED,
-        PrecompileAttr.IF_GENERATED_SOURCE,
     ):
         fail("Unexpected final precompile value: {}".format(repr(precompile)))
 
@@ -60,14 +79,10 @@
 PrecompileAttr = enum(
     # Determine the effective value from --precompile
     INHERIT = "inherit",
-    # Compile Python source files at build time. Note that
-    # --precompile_add_to_runfiles affects how the compiled files are included
-    # into a downstream binary.
+    # Compile Python source files at build time.
     ENABLED = "enabled",
     # Don't compile Python source files at build time.
     DISABLED = "disabled",
-    # Compile Python source files, but only if they're a generated file.
-    IF_GENERATED_SOURCE = "if_generated_source",
     get_effective_value = _precompile_attr_get_effective_value,
 )
 
@@ -90,7 +105,6 @@
     if attr_value not in (
         PrecompileSourceRetentionAttr.KEEP_SOURCE,
         PrecompileSourceRetentionAttr.OMIT_SOURCE,
-        PrecompileSourceRetentionAttr.OMIT_IF_GENERATED_SOURCE,
     ):
         fail("Unexpected final precompile_source_retention value: {}".format(repr(attr_value)))
     return attr_value
@@ -100,14 +114,17 @@
     INHERIT = "inherit",
     KEEP_SOURCE = "keep_source",
     OMIT_SOURCE = "omit_source",
-    OMIT_IF_GENERATED_SOURCE = "omit_if_generated_source",
     get_effective_value = _precompile_source_retention_get_effective_value,
 )
 
 def _pyc_collection_attr_is_pyc_collection_enabled(ctx):
     pyc_collection = ctx.attr.pyc_collection
     if pyc_collection == PycCollectionAttr.INHERIT:
-        pyc_collection = ctx.attr._pyc_collection_flag[BuildSettingInfo].value
+        precompile_flag = PrecompileFlag.get_effective_value(ctx)
+        if precompile_flag in (PrecompileFlag.ENABLED, PrecompileFlag.FORCE_ENABLED):
+            pyc_collection = PycCollectionAttr.INCLUDE_PYC
+        else:
+            pyc_collection = PycCollectionAttr.DISABLED
 
     if pyc_collection not in (PycCollectionAttr.INCLUDE_PYC, PycCollectionAttr.DISABLED):
         fail("Unexpected final pyc_collection value: {}".format(repr(pyc_collection)))
@@ -253,6 +270,8 @@
     allow_none = True,
 )
 
+_MaybeBuiltinPyInfo = [[BuiltinPyInfo]] if BuiltinPyInfo != None else []
+
 # Attributes common to rules accepting Python sources and deps.
 PY_SRCS_ATTRS = union_attrs(
     {
@@ -260,8 +279,7 @@
             providers = [
                 [PyInfo],
                 [CcInfo],
-                [BuiltinPyInfo],
-            ],
+            ] + _MaybeBuiltinPyInfo,
             # TODO(b/228692666): Google-specific; remove these allowances once
             # the depot is cleaned up.
             allow_rules = DEPS_ATTR_ALLOW_RULES,
@@ -282,13 +300,9 @@
 
 Values:
 
-* `inherit`: Determine the value from the {flag}`--precompile` flag.
-* `enabled`: Compile Python source files at build time. Note that
-  --precompile_add_to_runfiles affects how the compiled files are included into
-  a downstream binary.
+* `inherit`: Allow the downstream binary decide if precompiled files are used.
+* `enabled`: Compile Python source files at build time.
 * `disabled`: Don't compile Python source files at build time.
-* `if_generated_source`: Compile Python source files, but only if they're a
-  generated file.
 
 :::{seealso}
 
@@ -343,8 +357,6 @@
 * `inherit`: Inherit the value from the {flag}`--precompile_source_retention` flag.
 * `keep_source`: Include the original Python source.
 * `omit_source`: Don't include the original py source.
-* `omit_if_generated_source`: Keep the original source if it's a regular source
-  file, but omit it if it's a generated file.
 """,
         ),
         # Required attribute, but details vary by rule.
@@ -356,10 +368,6 @@
         # Required attribute, but the details vary by rule.
         # Use create_srcs_version_attr to create one.
         "srcs_version": None,
-        "_precompile_add_to_runfiles_flag": attr.label(
-            default = "//python/config_settings:precompile_add_to_runfiles",
-            providers = [BuildSettingInfo],
-        ),
         "_precompile_flag": attr.label(
             default = "//python/config_settings:precompile",
             providers = [BuildSettingInfo],
diff --git a/python/private/common/attributes_bazel.bzl b/python/private/attributes_bazel.bzl
similarity index 100%
rename from python/private/common/attributes_bazel.bzl
rename to python/private/attributes_bazel.bzl
diff --git a/python/private/builders.bzl b/python/private/builders.bzl
new file mode 100644
index 0000000..50aa3ed
--- /dev/null
+++ b/python/private/builders.bzl
@@ -0,0 +1,190 @@
+# 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.
+"""Builders to make building complex objects easier."""
+
+load("@bazel_skylib//lib:types.bzl", "types")
+
+def _DepsetBuilder():
+    """Create a builder for a depset."""
+
+    # buildifier: disable=uninitialized
+    self = struct(
+        _order = [None],
+        add = lambda *a, **k: _DepsetBuilder_add(self, *a, **k),
+        build = lambda *a, **k: _DepsetBuilder_build(self, *a, **k),
+        direct = [],
+        get_order = lambda *a, **k: _DepsetBuilder_get_order(self, *a, **k),
+        set_order = lambda *a, **k: _DepsetBuilder_set_order(self, *a, **k),
+        transitive = [],
+    )
+    return self
+
+def _DepsetBuilder_add(self, *values):
+    """Add value to the depset.
+
+    Args:
+        self: {type}`DepsetBuilder` implicitly added.
+        *values: {type}`depset | list | object` Values to add to the depset.
+            The values can be a depset, the non-depset value to add, or
+            a list of such values to add.
+
+    Returns:
+        {type}`DepsetBuilder`
+    """
+    for value in values:
+        if types.is_list(value):
+            for sub_value in value:
+                if types.is_depset(sub_value):
+                    self.transitive.append(sub_value)
+                else:
+                    self.direct.append(sub_value)
+        elif types.is_depset(value):
+            self.transitive.append(value)
+        else:
+            self.direct.append(value)
+    return self
+
+def _DepsetBuilder_set_order(self, order):
+    """Sets the order to use.
+
+    Args:
+        self: {type}`DepsetBuilder` implicitly added.
+        order: {type}`str` One of the {obj}`depset` `order` values.
+
+    Returns:
+        {type}`DepsetBuilder`
+    """
+    self._order[0] = order
+    return self
+
+def _DepsetBuilder_get_order(self):
+    """Gets the depset order that will be used.
+
+    Args:
+        self: {type}`DepsetBuilder` implicitly added.
+
+    Returns:
+        {type}`str | None` If not previously set, `None` is returned.
+    """
+    return self._order[0]
+
+def _DepsetBuilder_build(self):
+    """Creates a {obj}`depset` from the accumulated values.
+
+    Args:
+        self: {type}`DepsetBuilder` implicitly added.
+
+    Returns:
+        {type}`depset`
+    """
+    if not self.direct and len(self.transitive) == 1 and self._order[0] == None:
+        return self.transitive[0]
+    else:
+        kwargs = {}
+        if self._order[0] != None:
+            kwargs["order"] = self._order[0]
+        return depset(direct = self.direct, transitive = self.transitive, **kwargs)
+
+def _RunfilesBuilder():
+    """Creates a `RunfilesBuilder`.
+
+    Returns:
+        {type}`RunfilesBuilder`
+    """
+
+    # buildifier: disable=uninitialized
+    self = struct(
+        add = lambda *a, **k: _RunfilesBuilder_add(self, *a, **k),
+        add_targets = lambda *a, **k: _RunfilesBuilder_add_targets(self, *a, **k),
+        build = lambda *a, **k: _RunfilesBuilder_build(self, *a, **k),
+        files = _DepsetBuilder(),
+        root_symlinks = {},
+        runfiles = [],
+        symlinks = {},
+    )
+    return self
+
+def _RunfilesBuilder_add(self, *values):
+    """Adds a value to the runfiles.
+
+    Args:
+        self: {type}`RunfilesBuilder` implicitly added.
+        *values: {type}`File | runfiles | list[File] | depset[File] | list[runfiles]`
+            The values to add.
+
+    Returns:
+        {type}`RunfilesBuilder`
+    """
+    for value in values:
+        if types.is_list(value):
+            for sub_value in value:
+                _RunfilesBuilder_add_internal(self, sub_value)
+        else:
+            _RunfilesBuilder_add_internal(self, value)
+    return self
+
+def _RunfilesBuilder_add_targets(self, targets):
+    """Adds runfiles from targets
+
+    Args:
+        self: {type}`RunfilesBuilder` implicitly added.
+        targets: {type}`list[Target]` targets whose default runfiles
+            to add.
+
+    Returns:
+        {type}`RunfilesBuilder`
+    """
+    for t in targets:
+        self.runfiles.append(t[DefaultInfo].default_runfiles)
+    return self
+
+def _RunfilesBuilder_add_internal(self, value):
+    if _is_file(value):
+        self.files.add(value)
+    elif types.is_depset(value):
+        self.files.add(value)
+    elif _is_runfiles(value):
+        self.runfiles.append(value)
+    else:
+        fail("Unhandled value: type {}: {}".format(type(value), value))
+
+def _RunfilesBuilder_build(self, ctx, **kwargs):
+    """Creates a {obj}`runfiles` from the accumulated values.
+
+    Args:
+        self: {type}`RunfilesBuilder` implicitly added.
+        ctx: {type}`ctx` The rule context to use to create the runfiles object.
+        **kwargs: additional args to pass along to {obj}`ctx.runfiles`.
+
+    Returns:
+        {type}`runfiles`
+    """
+    return ctx.runfiles(
+        transitive_files = self.files.build(),
+        symlinks = self.symlinks,
+        root_symlinks = self.root_symlinks,
+        **kwargs
+    ).merge_all(self.runfiles)
+
+# Skylib's types module doesn't have is_file, so roll our own
+def _is_file(value):
+    return type(value) == "File"
+
+def _is_runfiles(value):
+    return type(value) == "runfiles"
+
+builders = struct(
+    DepsetBuilder = _DepsetBuilder,
+    RunfilesBuilder = _RunfilesBuilder,
+)
diff --git a/python/private/common/cc_helper.bzl b/python/private/cc_helper.bzl
similarity index 100%
rename from python/private/common/cc_helper.bzl
rename to python/private/cc_helper.bzl
diff --git a/python/private/common/common.bzl b/python/private/common.bzl
similarity index 86%
rename from python/private/common/common.bzl
rename to python/private/common.bzl
index 5559ccd..2dcc948 100644
--- a/python/private/common/common.bzl
+++ b/python/private/common.bzl
@@ -13,10 +13,10 @@
 # limitations under the License.
 """Various things common to Bazel and Google rule implementations."""
 
-load("//python/private:reexports.bzl", "BuiltinPyInfo")
 load(":cc_helper.bzl", "cc_helper")
-load(":providers.bzl", "PyInfo")
+load(":py_info.bzl", "PyInfo", "PyInfoBuilder")
 load(":py_internal.bzl", "py_internal")
+load(":reexports.bzl", "BuiltinPyInfo")
 load(
     ":semantics.bzl",
     "NATIVE_RULES_MIGRATION_FIX_CMD",
@@ -173,7 +173,7 @@
             runfiles.
         cc_toolchain: CcToolchain that should be used when building.
         feature_config: struct from cc_configure_features(); see
-            //python/private/common:py_executable.bzl%cc_configure_features.
+            //python/private:py_executable.bzl%cc_configure_features.
         **kwargs: Additional keys/values to set in the returned struct. This is to
             facilitate extensions with less patching. Any added fields should
             pick names that are unlikely to collide if the CcDetails API has
@@ -280,9 +280,9 @@
         dep[BuiltinPyInfo].imports
         for dep in ctx.attr.deps
         if BuiltinPyInfo in dep
-    ])
+    ] if BuiltinPyInfo != None else [])
 
-def collect_runfiles(ctx, files):
+def collect_runfiles(ctx, files = depset()):
     """Collects the necessary files from the rule's context.
 
     This presumes the ctx is for a py_binary, py_test, or py_library rule.
@@ -348,15 +348,29 @@
         collect_default = True,
     )
 
-def create_py_info(ctx, *, direct_sources, direct_pyc_files, imports):
+def create_py_info(
+        ctx,
+        *,
+        required_py_files,
+        required_pyc_files,
+        implicit_pyc_files,
+        implicit_pyc_source_files,
+        imports):
     """Create PyInfo provider.
 
     Args:
         ctx: rule ctx.
-        direct_sources: depset of Files; the direct, raw `.py` sources for the
-            target. This should only be Python source files. It should not
-            include pyc files.
-        direct_pyc_files: depset of Files; the direct `.pyc` sources for the target.
+        required_py_files: `depset[File]`; the direct, `.py` sources for the
+            target that **must** be included by downstream targets. This should
+            only be Python source files. It should not include pyc files.
+        required_pyc_files: `depset[File]`; the direct `.pyc` files this target
+            produces.
+        implicit_pyc_files: `depset[File]` pyc files that are only used if pyc
+            collection is enabled.
+        implicit_pyc_source_files: `depset[File]` source files for implicit pyc
+            files that are used when the implicit pyc files are not.
+        implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files
+            that a binary can choose to include.
         imports: depset of strings; the import path values to propagate.
 
     Returns:
@@ -364,87 +378,55 @@
         transitive sources collected from dependencies (the latter is only
         necessary for deprecated extra actions support).
     """
-    uses_shared_libraries = False
-    has_py2_only_sources = ctx.attr.srcs_version in ("PY2", "PY2ONLY")
-    has_py3_only_sources = ctx.attr.srcs_version in ("PY3", "PY3ONLY")
-    transitive_sources_depsets = []  # list of depsets
-    transitive_sources_files = []  # list of Files
-    transitive_pyc_depsets = [direct_pyc_files]  # list of depsets
+
+    py_info = PyInfoBuilder()
+    py_info.direct_pyc_files.add(required_pyc_files)
+    py_info.transitive_pyc_files.add(required_pyc_files)
+    py_info.transitive_implicit_pyc_files.add(implicit_pyc_files)
+    py_info.transitive_implicit_pyc_source_files.add(implicit_pyc_source_files)
+    py_info.imports.add(imports)
+    py_info.merge_has_py2_only_sources(ctx.attr.srcs_version in ("PY2", "PY2ONLY"))
+    py_info.merge_has_py3_only_sources(ctx.attr.srcs_version in ("PY3", "PY3ONLY"))
+
     for target in ctx.attr.deps:
         # PyInfo may not be present e.g. cc_library rules.
-        if PyInfo in target or BuiltinPyInfo in target:
-            info = _get_py_info(target)
-            transitive_sources_depsets.append(info.transitive_sources)
-            uses_shared_libraries = uses_shared_libraries or info.uses_shared_libraries
-            has_py2_only_sources = has_py2_only_sources or info.has_py2_only_sources
-            has_py3_only_sources = has_py3_only_sources or info.has_py3_only_sources
-
-            # BuiltinPyInfo doesn't have this field.
-            if hasattr(info, "transitive_pyc_files"):
-                transitive_pyc_depsets.append(info.transitive_pyc_files)
+        if PyInfo in target or (BuiltinPyInfo != None and BuiltinPyInfo in target):
+            py_info.merge(_get_py_info(target))
         else:
             # TODO(b/228692666): Remove this once non-PyInfo targets are no
             # longer supported in `deps`.
             files = target.files.to_list()
             for f in files:
                 if f.extension == "py":
-                    transitive_sources_files.append(f)
-                uses_shared_libraries = (
-                    uses_shared_libraries or
-                    cc_helper.is_valid_shared_library_artifact(f)
-                )
-    deps_transitive_sources = depset(
-        direct = transitive_sources_files,
-        transitive = transitive_sources_depsets,
-    )
+                    py_info.transitive_sources.add(f)
+                py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f))
+
+    deps_transitive_sources = py_info.transitive_sources.build()
+    py_info.transitive_sources.add(required_py_files)
 
     # We only look at data to calculate uses_shared_libraries, if it's already
     # true, then we don't need to waste time looping over it.
-    if not uses_shared_libraries:
+    if not py_info.get_uses_shared_libraries():
         # Similar to the above, except we only calculate uses_shared_libraries
         for target in ctx.attr.data:
             # TODO(b/234730058): Remove checking for PyInfo in data once depot
             # cleaned up.
-            if PyInfo in target or BuiltinPyInfo in target:
+            if PyInfo in target or (BuiltinPyInfo != None and BuiltinPyInfo in target):
                 info = _get_py_info(target)
-                uses_shared_libraries = info.uses_shared_libraries
+                py_info.merge_uses_shared_libraries(info.uses_shared_libraries)
             else:
                 files = target.files.to_list()
                 for f in files:
-                    uses_shared_libraries = cc_helper.is_valid_shared_library_artifact(f)
-                    if uses_shared_libraries:
+                    py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f))
+                    if py_info.get_uses_shared_libraries():
                         break
-            if uses_shared_libraries:
+            if py_info.get_uses_shared_libraries():
                 break
 
-    py_info_kwargs = dict(
-        transitive_sources = depset(
-            transitive = [deps_transitive_sources, direct_sources],
-        ),
-        imports = imports,
-        # NOTE: This isn't strictly correct, but with Python 2 gone,
-        # the srcs_version logic is largely defunct, so shouldn't matter in
-        # practice.
-        has_py2_only_sources = has_py2_only_sources,
-        has_py3_only_sources = has_py3_only_sources,
-        uses_shared_libraries = uses_shared_libraries,
-        direct_pyc_files = direct_pyc_files,
-        transitive_pyc_files = depset(transitive = transitive_pyc_depsets),
-    )
-
-    # TODO(b/203567235): Set `uses_shared_libraries` field, though the Bazel
-    # docs indicate it's unused in Bazel and may be removed.
-    py_info = PyInfo(**py_info_kwargs)
-
-    # Remove args that BuiltinPyInfo doesn't support
-    py_info_kwargs.pop("direct_pyc_files")
-    py_info_kwargs.pop("transitive_pyc_files")
-    builtin_py_info = BuiltinPyInfo(**py_info_kwargs)
-
-    return py_info, deps_transitive_sources, builtin_py_info
+    return py_info.build(), deps_transitive_sources, py_info.build_builtin_py_info()
 
 def _get_py_info(target):
-    return target[PyInfo] if PyInfo in target else target[BuiltinPyInfo]
+    return target[PyInfo] if PyInfo in target or BuiltinPyInfo == None else target[BuiltinPyInfo]
 
 def create_instrumented_files_info(ctx):
     return _coverage_common.instrumented_files_info(
diff --git a/python/private/common/BUILD.bazel b/python/private/common/BUILD.bazel
deleted file mode 100644
index 805c002..0000000
--- a/python/private/common/BUILD.bazel
+++ /dev/null
@@ -1,226 +0,0 @@
-# Copyright 2023 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("@bazel_skylib//:bzl_library.bzl", "bzl_library")
-
-package(
-    default_visibility = ["//:__subpackages__"],
-)
-
-bzl_library(
-    name = "attributes_bazel_bzl",
-    srcs = ["attributes_bazel.bzl"],
-    deps = ["//python/private:rules_cc_srcs_bzl"],
-)
-
-bzl_library(
-    name = "attributes_bzl",
-    srcs = ["attributes.bzl"],
-    deps = [
-        ":common_bzl",
-        ":providers_bzl",
-        ":py_internal_bzl",
-        ":semantics_bzl",
-        "//python/private:enum_bzl",
-        "//python/private:flags_bzl",
-        "//python/private:reexports_bzl",
-        "//python/private:rules_cc_srcs_bzl",
-        "@bazel_skylib//rules:common_settings",
-    ],
-)
-
-bzl_library(
-    name = "cc_helper_bzl",
-    srcs = ["cc_helper.bzl"],
-    deps = [":py_internal_bzl"],
-)
-
-bzl_library(
-    name = "common_bazel_bzl",
-    srcs = ["common_bazel.bzl"],
-    deps = [
-        ":attributes_bzl",
-        ":common_bzl",
-        ":providers_bzl",
-        ":py_internal_bzl",
-        "//python/private:py_interpreter_program_bzl",
-        "//python/private:toolchain_types_bzl",
-        "@bazel_skylib//lib:paths",
-    ],
-)
-
-bzl_library(
-    name = "common_bzl",
-    srcs = ["common.bzl"],
-    deps = [
-        ":cc_helper_bzl",
-        ":providers_bzl",
-        ":py_internal_bzl",
-        ":semantics_bzl",
-        "//python/private:reexports_bzl",
-        "//python/private:rules_cc_srcs_bzl",
-    ],
-)
-
-filegroup(
-    name = "distribution",
-    srcs = glob(["**"]),
-)
-
-bzl_library(
-    name = "providers_bzl",
-    srcs = ["providers.bzl"],
-    deps = [
-        ":semantics_bzl",
-        "//python/private:rules_cc_srcs_bzl",
-        "//python/private:util_bzl",
-    ],
-)
-
-bzl_library(
-    name = "py_binary_macro_bazel_bzl",
-    srcs = ["py_binary_macro_bazel.bzl"],
-    deps = [
-        ":common_bzl",
-        ":py_binary_rule_bazel_bzl",
-    ],
-)
-
-bzl_library(
-    name = "py_binary_rule_bazel_bzl",
-    srcs = ["py_binary_rule_bazel.bzl"],
-    deps = [
-        ":attributes_bzl",
-        ":py_executable_bazel_bzl",
-        ":semantics_bzl",
-        "@bazel_skylib//lib:dicts",
-    ],
-)
-
-bzl_library(
-    name = "py_executable_bazel_bzl",
-    srcs = ["py_executable_bazel.bzl"],
-    deps = [
-        ":attributes_bazel_bzl",
-        ":common_bazel_bzl",
-        ":common_bzl",
-        ":providers_bzl",
-        ":py_executable_bzl",
-        ":py_internal_bzl",
-        ":semantics_bzl",
-    ],
-)
-
-bzl_library(
-    name = "py_executable_bzl",
-    srcs = ["py_executable.bzl"],
-    deps = [
-        ":attributes_bzl",
-        ":cc_helper_bzl",
-        ":common_bzl",
-        ":providers_bzl",
-        ":py_internal_bzl",
-        "//python/private:flags_bzl",
-        "//python/private:py_executable_info_bzl",
-        "//python/private:rules_cc_srcs_bzl",
-        "//python/private:toolchain_types_bzl",
-        "@bazel_skylib//lib:dicts",
-        "@bazel_skylib//lib:structs",
-        "@bazel_skylib//rules:common_settings",
-    ],
-)
-
-bzl_library(
-    name = "py_internal_bzl",
-    srcs = ["py_internal.bzl"],
-    deps = ["@rules_python_internal//:py_internal_bzl"],
-)
-
-bzl_library(
-    name = "py_library_bzl",
-    srcs = ["py_library.bzl"],
-    deps = [
-        ":attributes_bzl",
-        ":common_bzl",
-        ":providers_bzl",
-        ":py_internal_bzl",
-        "//python/private:flags_bzl",
-        "//python/private:toolchain_types_bzl",
-        "@bazel_skylib//lib:dicts",
-        "@bazel_skylib//rules:common_settings",
-    ],
-)
-
-bzl_library(
-    name = "py_library_macro_bazel_bzl",
-    srcs = ["py_library_macro_bazel.bzl"],
-    deps = [":py_library_rule_bazel_bzl"],
-)
-
-bzl_library(
-    name = "py_library_rule_bazel_bzl",
-    srcs = ["py_library_rule_bazel.bzl"],
-    deps = [
-        ":attributes_bazel_bzl",
-        ":common_bazel_bzl",
-        ":common_bzl",
-        ":py_library_bzl",
-    ],
-)
-
-bzl_library(
-    name = "py_runtime_macro_bzl",
-    srcs = ["py_runtime_macro.bzl"],
-    deps = [":py_runtime_rule_bzl"],
-)
-
-bzl_library(
-    name = "py_runtime_rule_bzl",
-    srcs = ["py_runtime_rule.bzl"],
-    deps = [
-        ":attributes_bzl",
-        ":providers_bzl",
-        ":py_internal_bzl",
-        "//python/private:reexports_bzl",
-        "//python/private:util_bzl",
-        "@bazel_skylib//lib:dicts",
-        "@bazel_skylib//lib:paths",
-    ],
-)
-
-bzl_library(
-    name = "py_test_macro_bazel_bzl",
-    srcs = ["py_test_macro_bazel.bzl"],
-    deps = [
-        ":common_bazel_bzl",
-        ":py_test_rule_bazel_bzl",
-    ],
-)
-
-bzl_library(
-    name = "py_test_rule_bazel_bzl",
-    srcs = ["py_test_rule_bazel.bzl"],
-    deps = [
-        ":attributes_bzl",
-        ":common_bzl",
-        ":py_executable_bazel_bzl",
-        ":semantics_bzl",
-        "@bazel_skylib//lib:dicts",
-    ],
-)
-
-bzl_library(
-    name = "semantics_bzl",
-    srcs = ["semantics.bzl"],
-)
diff --git a/python/private/common/py_binary_rule_bazel.bzl b/python/private/common/py_binary_rule_bazel.bzl
index 9ce0726..7858411 100644
--- a/python/private/common/py_binary_rule_bazel.bzl
+++ b/python/private/common/py_binary_rule_bazel.bzl
@@ -1,52 +1,6 @@
-# Copyright 2022 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.
-"""Rule implementation of py_binary for Bazel."""
+"""Stub file for Bazel docs to link to.
 
-load("@bazel_skylib//lib:dicts.bzl", "dicts")
-load(":attributes.bzl", "AGNOSTIC_BINARY_ATTRS")
-load(
-    ":py_executable_bazel.bzl",
-    "create_executable_rule",
-    "py_executable_bazel_impl",
-)
+The Bazel docs link to this file, but the implementation was moved.
 
-_PY_TEST_ATTRS = {
-    # Magic attribute to help C++ coverage work. There's no
-    # docs about this; see TestActionBuilder.java
-    "_collect_cc_coverage": attr.label(
-        default = "@bazel_tools//tools/test:collect_cc_coverage",
-        executable = True,
-        cfg = "exec",
-    ),
-    # Magic attribute to make coverage work. There's no
-    # docs about this; see TestActionBuilder.java
-    "_lcov_merger": attr.label(
-        default = configuration_field(fragment = "coverage", name = "output_generator"),
-        executable = True,
-        cfg = "exec",
-    ),
-}
-
-def _py_binary_impl(ctx):
-    return py_executable_bazel_impl(
-        ctx = ctx,
-        is_test = False,
-        inherited_environment = [],
-    )
-
-py_binary = create_executable_rule(
-    implementation = _py_binary_impl,
-    attrs = dicts.add(AGNOSTIC_BINARY_ATTRS, _PY_TEST_ATTRS),
-    executable = True,
-)
+Please see: https://rules-python.readthedocs.io/en/latest/api/rules_python/python/defs.html#py_binary
+"""
diff --git a/python/private/common/py_library_rule_bazel.bzl b/python/private/common/py_library_rule_bazel.bzl
index 453abcb..be631c9 100644
--- a/python/private/common/py_library_rule_bazel.bzl
+++ b/python/private/common/py_library_rule_bazel.bzl
@@ -1,47 +1,6 @@
-# Copyright 2022 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.
-"""Implementation of py_library for Bazel."""
+"""Stub file for Bazel docs to link to.
 
-load(":attributes_bazel.bzl", "IMPORTS_ATTRS")
-load(":common.bzl", "create_library_semantics_struct", "union_attrs")
-load(":common_bazel.bzl", "collect_cc_info", "get_imports", "maybe_precompile")
-load(
-    ":py_library.bzl",
-    "LIBRARY_ATTRS",
-    "create_py_library_rule",
-    bazel_py_library_impl = "py_library_impl",
-)
+The Bazel docs link to this file, but the implementation was moved.
 
-_BAZEL_LIBRARY_ATTRS = union_attrs(
-    LIBRARY_ATTRS,
-    IMPORTS_ATTRS,
-)
-
-def create_library_semantics_bazel():
-    return create_library_semantics_struct(
-        get_imports = get_imports,
-        maybe_precompile = maybe_precompile,
-        get_cc_info_for_library = collect_cc_info,
-    )
-
-def _py_library_impl(ctx):
-    return bazel_py_library_impl(
-        ctx,
-        semantics = create_library_semantics_bazel(),
-    )
-
-py_library = create_py_library_rule(
-    implementation = _py_library_impl,
-    attrs = _BAZEL_LIBRARY_ATTRS,
-)
+Please see: https://rules-python.readthedocs.io/en/latest/api/rules_python/python/defs.html#py_library
+"""
diff --git a/python/private/common/py_runtime_rule.bzl b/python/private/common/py_runtime_rule.bzl
index b339425..cadb48c 100644
--- a/python/private/common/py_runtime_rule.bzl
+++ b/python/private/common/py_runtime_rule.bzl
@@ -1,356 +1,6 @@
-# Copyright 2022 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.
-"""Implementation of py_runtime rule."""
+"""Stub file for Bazel docs to link to.
 
-load("@bazel_skylib//lib:dicts.bzl", "dicts")
-load("@bazel_skylib//lib:paths.bzl", "paths")
-load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
-load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
-load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")
-load(":attributes.bzl", "NATIVE_RULES_ALLOWLIST_ATTRS")
-load(":providers.bzl", "DEFAULT_BOOTSTRAP_TEMPLATE", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo")
-load(":py_internal.bzl", "py_internal")
+The Bazel docs link to this file, but the implementation was moved.
 
-_py_builtins = py_internal
-
-def _py_runtime_impl(ctx):
-    interpreter_path = ctx.attr.interpreter_path or None  # Convert empty string to None
-    interpreter = ctx.attr.interpreter
-    if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
-        fail("exactly one of the 'interpreter' or 'interpreter_path' attributes must be specified")
-
-    runtime_files = depset(transitive = [
-        t[DefaultInfo].files
-        for t in ctx.attr.files
-    ])
-
-    runfiles = ctx.runfiles()
-
-    hermetic = bool(interpreter)
-    if not hermetic:
-        if runtime_files:
-            fail("if 'interpreter_path' is given then 'files' must be empty")
-        if not paths.is_absolute(interpreter_path):
-            fail("interpreter_path must be an absolute path")
-    else:
-        interpreter_di = interpreter[DefaultInfo]
-
-        if interpreter_di.files_to_run and interpreter_di.files_to_run.executable:
-            interpreter = interpreter_di.files_to_run.executable
-            runfiles = runfiles.merge(interpreter_di.default_runfiles)
-
-            runtime_files = depset(transitive = [
-                interpreter_di.files,
-                interpreter_di.default_runfiles.files,
-                runtime_files,
-            ])
-        elif _is_singleton_depset(interpreter_di.files):
-            interpreter = interpreter_di.files.to_list()[0]
-        else:
-            fail("interpreter must be an executable target or must produce exactly one file.")
-
-    if ctx.attr.coverage_tool:
-        coverage_di = ctx.attr.coverage_tool[DefaultInfo]
-
-        if _is_singleton_depset(coverage_di.files):
-            coverage_tool = coverage_di.files.to_list()[0]
-        elif coverage_di.files_to_run and coverage_di.files_to_run.executable:
-            coverage_tool = coverage_di.files_to_run.executable
-        else:
-            fail("coverage_tool must be an executable target or must produce exactly one file.")
-
-        coverage_files = depset(transitive = [
-            coverage_di.files,
-            coverage_di.default_runfiles.files,
-        ])
-    else:
-        coverage_tool = None
-        coverage_files = None
-
-    python_version = ctx.attr.python_version
-
-    interpreter_version_info = ctx.attr.interpreter_version_info
-    if not interpreter_version_info:
-        python_version_flag = ctx.attr._python_version_flag[BuildSettingInfo].value
-        if python_version_flag:
-            interpreter_version_info = _interpreter_version_info_from_version_str(python_version_flag)
-
-    # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true
-    # if ctx.fragments.py.disable_py2 and python_version == "PY2":
-    #     fail("Using Python 2 is not supported and disabled; see " +
-    #          "https://github.com/bazelbuild/bazel/issues/15684")
-
-    pyc_tag = ctx.attr.pyc_tag
-    if not pyc_tag and (ctx.attr.implementation_name and
-                        interpreter_version_info.get("major") and
-                        interpreter_version_info.get("minor")):
-        pyc_tag = "{}-{}{}".format(
-            ctx.attr.implementation_name,
-            interpreter_version_info["major"],
-            interpreter_version_info["minor"],
-        )
-
-    py_runtime_info_kwargs = dict(
-        interpreter_path = interpreter_path or None,
-        interpreter = interpreter,
-        files = runtime_files if hermetic else None,
-        coverage_tool = coverage_tool,
-        coverage_files = coverage_files,
-        python_version = python_version,
-        stub_shebang = ctx.attr.stub_shebang,
-        bootstrap_template = ctx.file.bootstrap_template,
-    )
-    builtin_py_runtime_info_kwargs = dict(py_runtime_info_kwargs)
-
-    # There are all args that BuiltinPyRuntimeInfo doesn't support
-    py_runtime_info_kwargs.update(dict(
-        implementation_name = ctx.attr.implementation_name,
-        interpreter_version_info = interpreter_version_info,
-        pyc_tag = pyc_tag,
-        stage2_bootstrap_template = ctx.file.stage2_bootstrap_template,
-        zip_main_template = ctx.file.zip_main_template,
-    ))
-
-    if not IS_BAZEL_7_OR_HIGHER:
-        builtin_py_runtime_info_kwargs.pop("bootstrap_template")
-
-    return [
-        PyRuntimeInfo(**py_runtime_info_kwargs),
-        # Return the builtin provider for better compatibility.
-        # 1. There is a legacy code path in py_binary that
-        #    checks for the provider when toolchains aren't used
-        # 2. It makes it easier to transition from builtins to rules_python
-        BuiltinPyRuntimeInfo(**builtin_py_runtime_info_kwargs),
-        DefaultInfo(
-            files = runtime_files,
-            runfiles = runfiles,
-        ),
-    ]
-
-# Bind to the name "py_runtime" to preserve the kind/rule_class it shows up
-# as elsewhere.
-py_runtime = rule(
-    implementation = _py_runtime_impl,
-    doc = """
-Represents a Python runtime used to execute Python code.
-
-A `py_runtime` target can represent either a *platform runtime* or an *in-build
-runtime*. A platform runtime accesses a system-installed interpreter at a known
-path, whereas an in-build runtime points to an executable target that acts as
-the interpreter. In both cases, an "interpreter" means any executable binary or
-wrapper script that is capable of running a Python script passed on the command
-line, following the same conventions as the standard CPython interpreter.
-
-A platform runtime is by its nature non-hermetic. It imposes a requirement on
-the target platform to have an interpreter located at a specific path. An
-in-build runtime may or may not be hermetic, depending on whether it points to
-a checked-in interpreter or a wrapper script that accesses the system
-interpreter.
-
-Example
-
-```
-load("@rules_python//python:py_runtime.bzl", "py_runtime")
-
-py_runtime(
-    name = "python-2.7.12",
-    files = glob(["python-2.7.12/**"]),
-    interpreter = "python-2.7.12/bin/python",
-)
-
-py_runtime(
-    name = "python-3.6.0",
-    interpreter_path = "/opt/pyenv/versions/3.6.0/bin/python",
-)
-```
-""",
-    fragments = ["py"],
-    attrs = dicts.add(NATIVE_RULES_ALLOWLIST_ATTRS, {
-        "bootstrap_template": attr.label(
-            allow_single_file = True,
-            default = DEFAULT_BOOTSTRAP_TEMPLATE,
-            doc = """
-The bootstrap script template file to use. Should have %python_binary%,
-%workspace_name%, %main%, and %imports%.
-
-This template, after expansion, becomes the executable file used to start the
-process, so it is responsible for initial bootstrapping actions such as finding
-the Python interpreter, runfiles, and constructing an environment to run the
-intended Python application.
-
-While this attribute is currently optional, it will become required when the
-Python rules are moved out of Bazel itself.
-
-The exact variable names expanded is an unstable API and is subject to change.
-The API will become more stable when the Python rules are moved out of Bazel
-itself.
-
-See @bazel_tools//tools/python:python_bootstrap_template.txt for more variables.
-""",
-        ),
-        "coverage_tool": attr.label(
-            allow_files = False,
-            doc = """
-This is a target to use for collecting code coverage information from
-{rule}`py_binary` and {rule}`py_test` targets.
-
-If set, the target must either produce a single file or be an executable target.
-The path to the single file, or the executable if the target is executable,
-determines the entry point for the python coverage tool.  The target and its
-runfiles will be added to the runfiles when coverage is enabled.
-
-The entry point for the tool must be loadable by a Python interpreter (e.g. a
-`.py` or `.pyc` file).  It must accept the command line arguments
-of [`coverage.py`](https://coverage.readthedocs.io), at least including
-the `run` and `lcov` subcommands.
-""",
-        ),
-        "files": attr.label_list(
-            allow_files = True,
-            doc = """
-For an in-build runtime, this is the set of files comprising this runtime.
-These files will be added to the runfiles of Python binaries that use this
-runtime. For a platform runtime this attribute must not be set.
-""",
-        ),
-        "implementation_name": attr.string(
-            doc = "The Python implementation name (`sys.implementation.name`)",
-        ),
-        "interpreter": attr.label(
-            # We set `allow_files = True` to allow specifying executable
-            # targets from rules that have more than one default output,
-            # e.g. sh_binary.
-            allow_files = True,
-            doc = """
-For an in-build runtime, this is the target to invoke as the interpreter. It
-can be either of:
-
-* A single file, which will be the interpreter binary. It's assumed such
-  interpreters are either self-contained single-file executables or any
-  supporting files are specified in `files`.
-* An executable target. The target's executable will be the interpreter binary.
-  Any other default outputs (`target.files`) and plain files runfiles
-  (`runfiles.files`) will be automatically included as if specified in the
-  `files` attribute.
-
-  NOTE: the runfiles of the target may not yet be properly respected/propagated
-  to consumers of the toolchain/interpreter, see
-  bazelbuild/rules_python/issues/1612
-
-For a platform runtime (i.e. `interpreter_path` being set) this attribute must
-not be set.
-""",
-        ),
-        "interpreter_path": attr.string(doc = """
-For a platform runtime, this is the absolute path of a Python interpreter on
-the target platform. For an in-build runtime this attribute must not be set.
-"""),
-        "interpreter_version_info": attr.string_dict(
-            doc = """
-Version information about the interpreter this runtime provides.
-
-If not specified, uses {obj}`--python_version`
-
-The supported keys match the names for `sys.version_info`. While the input
-values are strings, most are converted to ints. The supported keys are:
-  * major: int, the major version number
-  * minor: int, the minor version number
-  * micro: optional int, the micro version number
-  * releaselevel: optional str, the release level
-  * serial: optional int, the serial number of the release
-
-:::{versionchanged} 0.36.0
-{obj}`--python_version` determines the default value.
-:::
-""",
-            mandatory = False,
-        ),
-        "pyc_tag": attr.string(
-            doc = """
-Optional string; the tag portion of a pyc filename, e.g. the `cpython-39` infix
-of `foo.cpython-39.pyc`. See PEP 3147. If not specified, it will be computed
-from `implementation_name` and `interpreter_version_info`. If no pyc_tag is
-available, then only source-less pyc generation will function correctly.
-""",
-        ),
-        "python_version": attr.string(
-            default = "PY3",
-            values = ["PY2", "PY3"],
-            doc = """
-Whether this runtime is for Python major version 2 or 3. Valid values are `"PY2"`
-and `"PY3"`.
-
-The default value is controlled by the `--incompatible_py3_is_default` flag.
-However, in the future this attribute will be mandatory and have no default
-value.
-            """,
-        ),
-        "stage2_bootstrap_template": attr.label(
-            default = "//python/private:stage2_bootstrap_template",
-            allow_single_file = True,
-            doc = """
-The template to use when two stage bootstrapping is enabled
-
-:::{seealso}
-{obj}`PyRuntimeInfo.stage2_bootstrap_template` and {obj}`--bootstrap_impl`
-:::
-""",
-        ),
-        "stub_shebang": attr.string(
-            default = DEFAULT_STUB_SHEBANG,
-            doc = """
-"Shebang" expression prepended to the bootstrapping Python stub script
-used when executing {rule}`py_binary` targets.
-
-See https://github.com/bazelbuild/bazel/issues/8685 for
-motivation.
-
-Does not apply to Windows.
-""",
-        ),
-        "zip_main_template": attr.label(
-            default = "//python/private:zip_main_template",
-            allow_single_file = True,
-            doc = """
-The template to use for a zip's top-level `__main__.py` file.
-
-This becomes the entry point executed when `python foo.zip` is run.
-
-:::{seealso}
-The {obj}`PyRuntimeInfo.zip_main_template` field.
-:::
-""",
-        ),
-        "_python_version_flag": attr.label(
-            default = "//python/config_settings:python_version",
-        ),
-    }),
-)
-
-def _is_singleton_depset(files):
-    # Bazel 6 doesn't have this helper to optimize detecting singleton depsets.
-    if _py_builtins:
-        return _py_builtins.is_singleton_depset(files)
-    else:
-        return len(files.to_list()) == 1
-
-def _interpreter_version_info_from_version_str(version_str):
-    parts = version_str.split(".")
-    version_info = {}
-    for key in ("major", "minor", "micro"):
-        if not parts:
-            break
-        version_info[key] = parts.pop(0)
-
-    return version_info
+Please see: https://rules-python.readthedocs.io/en/latest/api/rules_python/python/defs.html#py_runtime
+"""
diff --git a/python/private/common/py_test_rule_bazel.bzl b/python/private/common/py_test_rule_bazel.bzl
index 369360d..c89e3a6 100644
--- a/python/private/common/py_test_rule_bazel.bzl
+++ b/python/private/common/py_test_rule_bazel.bzl
@@ -1,55 +1,6 @@
-# Copyright 2022 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.
-"""Rule implementation of py_test for Bazel."""
+"""Stub file for Bazel docs to link to.
 
-load("@bazel_skylib//lib:dicts.bzl", "dicts")
-load(":attributes.bzl", "AGNOSTIC_TEST_ATTRS")
-load(":common.bzl", "maybe_add_test_execution_info")
-load(
-    ":py_executable_bazel.bzl",
-    "create_executable_rule",
-    "py_executable_bazel_impl",
-)
+The Bazel docs link to this file, but the implementation was moved.
 
-_BAZEL_PY_TEST_ATTRS = {
-    # This *might* be a magic attribute to help C++ coverage work. There's no
-    # docs about this; see TestActionBuilder.java
-    "_collect_cc_coverage": attr.label(
-        default = "@bazel_tools//tools/test:collect_cc_coverage",
-        executable = True,
-        cfg = "exec",
-    ),
-    # This *might* be a magic attribute to help C++ coverage work. There's no
-    # docs about this; see TestActionBuilder.java
-    "_lcov_merger": attr.label(
-        default = configuration_field(fragment = "coverage", name = "output_generator"),
-        cfg = "exec",
-        executable = True,
-    ),
-}
-
-def _py_test_impl(ctx):
-    providers = py_executable_bazel_impl(
-        ctx = ctx,
-        is_test = True,
-        inherited_environment = ctx.attr.env_inherit,
-    )
-    maybe_add_test_execution_info(providers, ctx)
-    return providers
-
-py_test = create_executable_rule(
-    implementation = _py_test_impl,
-    attrs = dicts.add(AGNOSTIC_TEST_ATTRS, _BAZEL_PY_TEST_ATTRS),
-    test = True,
-)
+Please see: https://rules-python.readthedocs.io/en/latest/api/rules_python/python/defs.html#py_test
+"""
diff --git a/python/private/common/common_bazel.bzl b/python/private/common_bazel.bzl
similarity index 83%
rename from python/private/common/common_bazel.bzl
rename to python/private/common_bazel.bzl
index c86abd2..642cfd8 100644
--- a/python/private/common/common_bazel.bzl
+++ b/python/private/common_bazel.bzl
@@ -14,13 +14,15 @@
 """Common functions that are specific to Bazel rule implementation"""
 
 load("@bazel_skylib//lib:paths.bzl", "paths")
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("@rules_cc//cc:defs.bzl", "CcInfo", "cc_common")
-load("//python/private:py_interpreter_program.bzl", "PyInterpreterProgramInfo")
-load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE")
 load(":attributes.bzl", "PrecompileAttr", "PrecompileInvalidationModeAttr", "PrecompileSourceRetentionAttr")
 load(":common.bzl", "is_bool")
-load(":providers.bzl", "PyCcLinkParamsProvider")
+load(":flags.bzl", "PrecompileFlag")
+load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
 load(":py_internal.bzl", "py_internal")
+load(":py_interpreter_program.bzl", "PyInterpreterProgramInfo")
+load(":toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE")
 
 _py_builtins = py_internal
 
@@ -43,8 +45,8 @@
         if CcInfo in dep:
             cc_infos.append(dep[CcInfo])
 
-        if PyCcLinkParamsProvider in dep:
-            cc_infos.append(dep[PyCcLinkParamsProvider].cc_info)
+        if PyCcLinkParamsInfo in dep:
+            cc_infos.append(dep[PyCcLinkParamsInfo].cc_info)
 
     return cc_common.merge_cc_infos(cc_infos = cc_infos)
 
@@ -60,7 +62,7 @@
     Returns:
         Struct of precompiling results with fields:
         * `keep_srcs`: list of File; the input sources that should be included
-          as default outputs and runfiles.
+          as default outputs.
         * `pyc_files`: list of File; the precompiled files.
         * `py_to_pyc_map`: dict of src File input to pyc File output. If a source
           file wasn't precompiled, it won't be in the dict.
@@ -72,9 +74,27 @@
     if exec_tools_toolchain == None or exec_tools_toolchain.exec_tools.precompiler == None:
         precompile = PrecompileAttr.DISABLED
     else:
-        precompile = PrecompileAttr.get_effective_value(ctx)
+        precompile_flag = ctx.attr._precompile_flag[BuildSettingInfo].value
+
+        if precompile_flag == PrecompileFlag.FORCE_ENABLED:
+            precompile = PrecompileAttr.ENABLED
+        elif precompile_flag == PrecompileFlag.FORCE_DISABLED:
+            precompile = PrecompileAttr.DISABLED
+        else:
+            precompile = ctx.attr.precompile
+
+    # Unless explicitly disabled, we always generate a pyc. This allows
+    # binaries to decide whether to include them or not later.
+    if precompile != PrecompileAttr.DISABLED:
+        should_precompile = True
+    else:
+        should_precompile = False
 
     source_retention = PrecompileSourceRetentionAttr.get_effective_value(ctx)
+    keep_source = (
+        not should_precompile or
+        source_retention == PrecompileSourceRetentionAttr.KEEP_SOURCE
+    )
 
     result = struct(
         keep_srcs = [],
@@ -82,26 +102,17 @@
         py_to_pyc_map = {},
     )
     for src in srcs:
-        # The logic below is a bit convoluted. The gist is:
-        # * If precompiling isn't done, add the py source to default outputs.
-        #   Otherwise, the source retention flag decides.
-        # * In order to determine `use_pycache`, we have to know if the source
-        #   is being added to the default outputs.
-        is_generated_source = not src.is_source
-        should_precompile = (
-            precompile == PrecompileAttr.ENABLED or
-            (precompile == PrecompileAttr.IF_GENERATED_SOURCE and is_generated_source)
-        )
-        keep_source = (
-            not should_precompile or
-            source_retention == PrecompileSourceRetentionAttr.KEEP_SOURCE or
-            (source_retention == PrecompileSourceRetentionAttr.OMIT_IF_GENERATED_SOURCE and not is_generated_source)
-        )
         if should_precompile:
+            # NOTE: _precompile() may return None
             pyc = _precompile(ctx, src, use_pycache = keep_source)
+        else:
+            pyc = None
+
+        if pyc:
             result.pyc_files.append(pyc)
             result.py_to_pyc_map[src] = pyc
-        if keep_source:
+
+        if keep_source or not pyc:
             result.keep_srcs.append(src)
 
     return result
@@ -119,6 +130,12 @@
     Returns:
         File of the generated pyc file.
     """
+
+    # Generating a file in another package is an error, so we have to skip
+    # such cases.
+    if ctx.label.package != src.owner.package:
+        return None
+
     exec_tools_info = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools
     target_toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE].py3_runtime
 
@@ -149,7 +166,11 @@
     stem = src.basename[:-(len(src.extension) + 1)]
     if use_pycache:
         if not target_toolchain.pyc_tag:
-            fail("Unable to create __pycache__ pyc: pyc_tag is empty")
+            # This is most likely because of a "runtime toolchain", i.e. the
+            # autodetecting toolchain, or some equivalent toolchain that can't
+            # assume to know the runtime Python version at build time.
+            # Instead of failing, just don't generate any pyc.
+            return None
         pyc_path = "__pycache__/{stem}.{tag}.pyc".format(
             stem = stem,
             tag = target_toolchain.pyc_tag,
diff --git a/python/private/config_settings.bzl b/python/private/config_settings.bzl
index b15d6a8..10b4d68 100644
--- a/python/private/config_settings.bzl
+++ b/python/private/config_settings.bzl
@@ -20,9 +20,9 @@
 load(":semver.bzl", "semver")
 
 _PYTHON_VERSION_FLAG = Label("//python/config_settings:python_version")
-_PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:_python_version_major_minor")
+_PYTHON_VERSION_MAJOR_MINOR_FLAG = Label("//python/config_settings:python_version_major_minor")
 
-def construct_config_settings(*, name, versions, minor_mapping):  # buildifier: disable=function-docstring
+def construct_config_settings(*, name, default_version, versions, minor_mapping):  # buildifier: disable=function-docstring
     """Create a 'python_version' config flag and construct all config settings used in rules_python.
 
     This mainly includes the targets that are used in the toolchain and pip hub
@@ -30,17 +30,14 @@
 
     Args:
         name: {type}`str` A dummy name value that is no-op for now.
+        default_version: {type}`str` the default value for the `python_version` flag.
         versions: {type}`list[str]` A list of versions to build constraint settings for.
         minor_mapping: {type}`dict[str, str]` A mapping from `X.Y` to `X.Y.Z` python versions.
     """
     _ = name  # @unused
     _python_version_flag(
         name = _PYTHON_VERSION_FLAG.name,
-        # TODO: The default here should somehow match the MODULE config. Until
-        # then, use the empty string to indicate an unknown version. This
-        # also prevents version-unaware targets from inadvertently matching
-        # a select condition when they shouldn't.
-        build_setting_default = "",
+        build_setting_default = default_version,
         visibility = ["//visibility:public"],
     )
 
diff --git a/python/private/coverage_deps.bzl b/python/private/coverage_deps.bzl
index d69fab9..d3a6d96 100644
--- a/python/private/coverage_deps.bzl
+++ b/python/private/coverage_deps.bzl
@@ -23,92 +23,106 @@
 _coverage_deps = {
     "cp310": {
         "aarch64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/a3/36/b5ae380c05f58544a40ff36f87fa1d6e45f5c2f299335586aac140c341ce/coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl",
-            "718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4",
+            "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl",
+            "cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36",
         ),
         "aarch64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/9e/48/5ae1ccf4601500af0ca36eba0a2c1f1796e58fb7495de6da55ed43e13e5f/coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
-            "767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524",
+            "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+            "e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02",
         ),
         "x86_64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/50/5a/d727fcd2e0fc3aba61591b6f0fe1e87865ea9b6275f58f35810d6f85b05b/coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl",
-            "8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6",
+            "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl",
+            "b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16",
         ),
         "x86_64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/23/0a/ab5b0f6d6b24f7156624e7697ec7ab49f9d5cdac922da90d9927ae5de1cf/coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
-            "ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb",
+            "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+            "8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23",
         ),
     },
     "cp311": {
         "aarch64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/f8/a1/161102d2e26fde2d878d68cc1ed303758dc7b01ee14cc6aa70f5fd1b910d/coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl",
-            "489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113",
+            "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl",
+            "ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3",
         ),
         "aarch64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/a7/af/1510df1132a68ca876013c0417ca46836252e43871d2623b489e4339c980/coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
-            "451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe",
+            "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+            "d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff",
         ),
         "x86_64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/ca/77/f17a5b199e8ca0443ace312f7e07ff3e4e7ba7d7c52847567d6f1edb22a7/coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl",
-            "cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47",
+            "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl",
+            "7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93",
         ),
         "x86_64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/a9/1a/e2120233177b3e2ea9dcfd49a050748060166c74792b2b1db4a803307da4/coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
-            "b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3",
+            "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+            "0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6",
         ),
     },
     "cp312": {
         "aarch64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/9d/d8/111ec1a65fef57ad2e31445af627d481f660d4a9218ee5c774b45187812a/coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl",
-            "d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328",
+            "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl",
+            "5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391",
         ),
         "aarch64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/8f/eb/28416f1721a3b7fa28ea499e8a6f867e28146ea2453839c2bca04a001eeb/coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
-            "3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30",
+            "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+            "260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8",
         ),
         "x86_64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/11/5c/2cf3e794fa5d1eb443aa8544e2ba3837d75073eaf25a1fda64d232065609/coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl",
-            "b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10",
+            "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl",
+            "95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778",
         ),
         "x86_64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/2f/db/70900f10b85a66f761a3a28950ccd07757d51548b1d10157adc4b9415f15/coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
-            "b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e",
+            "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+            "c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca",
+        ),
+    },
+    "cp313": {
+        "aarch64-apple-darwin": (
+            "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl",
+            "a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9",
+        ),
+        "aarch64-unknown-linux-gnu": (
+            "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+            "6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb",
+        ),
+        "x86_64-unknown-linux-gnu": (
+            "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+            "13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b",
         ),
     },
     "cp38": {
         "aarch64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/96/71/1c299b12e80d231e04a2bfd695e761fb779af7ab66f8bd3cb15649be82b3/coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl",
-            "280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e",
+            "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl",
+            "f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a",
         ),
         "aarch64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/c7/a7/b00eaa53d904193478eae01625d784b2af8b522a98028f47c831dcc95663/coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
-            "6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2",
+            "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+            "a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b",
         ),
         "x86_64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/e2/bc/f54b24b476db0069ac04ff2cdeb28cd890654c8619761bf818726022c76a/coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl",
-            "28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454",
+            "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl",
+            "6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0",
         ),
         "x86_64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/d0/3a/e882caceca2c7d65791a4a759764a1bf803bbbd10caf38ec41d73a45219e/coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
-            "dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6",
+            "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+            "8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de",
         ),
     },
     "cp39": {
         "aarch64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/66/f2/57f5d3c9d2e78c088e4c8dbc933b85fa81c424f23641f10c1aa64052ee4f/coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl",
-            "77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c",
+            "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl",
+            "547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8",
         ),
         "aarch64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/ad/3f/cde6fd2e4cc447bd24e3dc2e79abd2e0fba67ac162996253d3505f8efef4/coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
-            "6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e",
+            "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
+            "645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2",
         ),
         "x86_64-apple-darwin": (
-            "https://files.pythonhosted.org/packages/d6/cf/4094ac6410b680c91c5e55a56f25f4b3a878e2fcbf773c1cecfbdbaaec4f/coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl",
-            "3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f",
+            "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl",
+            "abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255",
         ),
         "x86_64-unknown-linux-gnu": (
-            "https://files.pythonhosted.org/packages/b5/ad/effc12b8f72321cb847c5ba7f4ea7ce3e5c19c641f6418131f8fb0ab2f61/coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
-            "8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee",
+            "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
+            "609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc",
         ),
     },
 }
diff --git a/python/private/enum.bzl b/python/private/enum.bzl
index 011d9fb..d71442e 100644
--- a/python/private/enum.bzl
+++ b/python/private/enum.bzl
@@ -17,10 +17,13 @@
 This is a separate file to minimize transitive loads.
 """
 
-def enum(**kwargs):
+def enum(methods = {}, **kwargs):
     """Creates a struct whose primary purpose is to be like an enum.
 
     Args:
+        methods: {type}`dict[str, callable]` functions that will be
+            added to the created enum object, but will have the enum object
+            itself passed as the first positional arg when calling them.
         **kwargs: The fields of the returned struct. All uppercase names will
             be treated as enum values and added to `__members__`.
 
@@ -33,4 +36,10 @@
         for key, value in kwargs.items()
         if key.upper() == key
     }
-    return struct(__members__ = members, **kwargs)
+
+    for name, unbound_method in methods.items():
+        # buildifier: disable=uninitialized
+        kwargs[name] = lambda *a, **k: unbound_method(self, *a, **k)
+
+    self = struct(__members__ = members, **kwargs)
+    return self
diff --git a/python/private/flags.bzl b/python/private/flags.bzl
index 652e117..c190cf6 100644
--- a/python/private/flags.bzl
+++ b/python/private/flags.bzl
@@ -19,7 +19,41 @@
 """
 
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
-load("//python/private:enum.bzl", "enum")
+load(":enum.bzl", "enum")
+
+def _FlagEnum_flag_values(self):
+    return sorted(self.__members__.values())
+
+def FlagEnum(**kwargs):
+    """Define an enum specialized for flags.
+
+    Args:
+        **kwargs: members of the enum.
+
+    Returns:
+        {type}`FlagEnum` struct. This is an enum with the following extras:
+        * `flag_values`: A function that returns a sorted list of the
+          flag values (enum `__members__`). Useful for passing to the
+          `values` attribute for string flags.
+    """
+    return enum(
+        methods = dict(flag_values = _FlagEnum_flag_values),
+        **kwargs
+    )
+
+def _AddSrcsToRunfilesFlag_is_enabled(ctx):
+    value = ctx.attr._add_srcs_to_runfiles_flag[BuildSettingInfo].value
+    if value == AddSrcsToRunfilesFlag.AUTO:
+        value = AddSrcsToRunfilesFlag.ENABLED
+    return value == AddSrcsToRunfilesFlag.ENABLED
+
+# buildifier: disable=name-conventions
+AddSrcsToRunfilesFlag = FlagEnum(
+    AUTO = "auto",
+    ENABLED = "enabled",
+    DISABLED = "disabled",
+    is_enabled = _AddSrcsToRunfilesFlag_is_enabled,
+)
 
 def _bootstrap_impl_flag_get_value(ctx):
     return ctx.attr._bootstrap_impl_flag[BuildSettingInfo].value
@@ -55,17 +89,13 @@
     # Automatically decide the effective value based on environment,
     # target platform, etc.
     AUTO = "auto",
-    # Compile Python source files at build time. Note that
-    # --precompile_add_to_runfiles affects how the compiled files are included
-    # into a downstream binary.
+    # Compile Python source files at build time.
     ENABLED = "enabled",
     # Don't compile Python source files at build time.
     DISABLED = "disabled",
-    # Compile Python source files, but only if they're a generated file.
-    IF_GENERATED_SOURCE = "if_generated_source",
     # Like `enabled`, except overrides target-level setting. This is mostly
     # useful for development, testing enabling precompilation more broadly, or
-    # as an escape hatch if build-time compiling is not available.
+    # as an escape hatch to force all transitive deps to precompile.
     FORCE_ENABLED = "force_enabled",
     # Like `disabled`, except overrides target-level setting. This is useful
     # useful for development, testing enabling precompilation more broadly, or
@@ -90,32 +120,5 @@
     KEEP_SOURCE = "keep_source",
     # Don't include the original py source.
     OMIT_SOURCE = "omit_source",
-    # Keep the original py source if it's a regular source file, but omit it
-    # if it's a generated file.
-    OMIT_IF_GENERATED_SOURCE = "omit_if_generated_source",
     get_effective_value = _precompile_source_retention_flag_get_effective_value,
 )
-
-# Determines if a target adds its compiled files to its runfiles. When a target
-# compiles its files, but doesn't add them to its own runfiles, it relies on
-# a downstream target to retrieve them from `PyInfo.transitive_pyc_files`
-# buildifier: disable=name-conventions
-PrecompileAddToRunfilesFlag = enum(
-    # Always include the compiled files in the target's runfiles.
-    ALWAYS = "always",
-    # Don't include the compiled files in the target's runfiles; they are
-    # still added to `PyInfo.transitive_pyc_files`. See also:
-    # `py_binary.pyc_collection` attribute. This is useful for allowing
-    # incrementally enabling precompilation on a per-binary basis.
-    DECIDED_ELSEWHERE = "decided_elsewhere",
-)
-
-# Determine if `py_binary` collects transitive pyc files.
-# NOTE: This flag is only respect if `py_binary.pyc_collection` is `inherit`.
-# buildifier: disable=name-conventions
-PycCollectionFlag = enum(
-    # Include `PyInfo.transitive_pyc_files` as part of the binary.
-    INCLUDE_PYC = "include_pyc",
-    # Don't include `PyInfo.transitive_pyc_files` as part of the binary.
-    DISABLED = "disabled",
-)
diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl
index c37bc35..e2fa8f6 100644
--- a/python/private/internal_config_repo.bzl
+++ b/python/private/internal_config_repo.bzl
@@ -24,6 +24,9 @@
 _CONFIG_TEMPLATE = """\
 config = struct(
   enable_pystar = {enable_pystar},
+  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}),
 )
 """
 
@@ -65,8 +68,20 @@
     else:
         enable_pystar = False
 
+    if native.bazel_version.startswith("8."):
+        builtin_py_info_symbol = "None"
+        builtin_py_runtime_info_symbol = "None"
+        builtin_py_cc_link_params_provider = "None"
+    else:
+        builtin_py_info_symbol = "PyInfo"
+        builtin_py_runtime_info_symbol = "PyRuntimeInfo"
+        builtin_py_cc_link_params_provider = "PyCcLinkParamsProvider"
+
     rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format(
         enable_pystar = enable_pystar,
+        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,
     ))
 
     if enable_pystar:
diff --git a/python/private/local_runtime_repo.bzl b/python/private/local_runtime_repo.bzl
index 4e7edde..fb1a8e2 100644
--- a/python/private/local_runtime_repo.bzl
+++ b/python/private/local_runtime_repo.bzl
@@ -14,7 +14,7 @@
 
 """Create a repository for a locally installed Python runtime."""
 
-load("//python/private:enum.bzl", "enum")
+load(":enum.bzl", "enum")
 load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
 
 # buildifier: disable=name-conventions
diff --git a/python/private/local_runtime_toolchains_repo.bzl b/python/private/local_runtime_toolchains_repo.bzl
index 880fbfe..adb3bb5 100644
--- a/python/private/local_runtime_toolchains_repo.bzl
+++ b/python/private/local_runtime_toolchains_repo.bzl
@@ -14,8 +14,8 @@
 
 """Create a repository to hold a local Python toolchain definitions."""
 
-load("//python/private:text_util.bzl", "render")
 load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
+load(":text_util.bzl", "render")
 
 _TOOLCHAIN_TEMPLATE = """
 # Generated by local_runtime_toolchains_repo.bzl
diff --git a/python/private/proto/py_proto_library.bzl b/python/private/proto/py_proto_library.bzl
index e123ff8..ecb0938 100644
--- a/python/private/proto/py_proto_library.bzl
+++ b/python/private/proto/py_proto_library.bzl
@@ -16,6 +16,7 @@
 
 load("@rules_proto//proto:defs.bzl", "ProtoInfo", "proto_common")
 load("//python:defs.bzl", "PyInfo")
+load("//python/api:api.bzl", _py_common = "py_common")
 
 PY_PROTO_TOOLCHAIN = "@rules_python//python/proto:toolchain_type"
 
@@ -25,6 +26,7 @@
         "imports": """
             (depset[str]) The field forwarding PyInfo.imports coming from
             the proto language runtime dependency.""",
+        "py_info": "PyInfo from proto runtime (or other deps) to propagate.",
         "runfiles_from_proto_deps": """
             (depset[File]) Files from the transitive closure implicit proto
             dependencies""",
@@ -71,6 +73,11 @@
     else:
         proto_lang_toolchain_info = getattr(ctx.attr, "_aspect_proto_toolchain")[proto_common.ProtoLangToolchainInfo]
 
+    py_common = _py_common.get(ctx)
+    py_info = py_common.PyInfoBuilder().merge_target(
+        proto_lang_toolchain_info.runtime,
+    ).build()
+
     api_deps = [proto_lang_toolchain_info.runtime]
 
     generated_sources = []
@@ -127,16 +134,19 @@
             ),
             runfiles_from_proto_deps = runfiles_from_proto_deps,
             transitive_sources = transitive_sources,
+            py_info = py_info,
         ),
     ]
 
 _py_proto_aspect = aspect(
     implementation = _py_proto_aspect_impl,
-    attrs = {} if _incompatible_toolchains_enabled() else {
-        "_aspect_proto_toolchain": attr.label(
-            default = ":python_toolchain",
-        ),
-    },
+    attrs = _py_common.API_ATTRS | (
+        {} if _incompatible_toolchains_enabled() else {
+            "_aspect_proto_toolchain": attr.label(
+                default = ":python_toolchain",
+            ),
+        }
+    ),
     attr_aspects = ["deps"],
     required_providers = [ProtoInfo],
     provides = [_PyProtoInfo],
@@ -159,6 +169,17 @@
         transitive = [info.transitive_sources for info in pyproto_infos],
     )
 
+    py_common = _py_common.get(ctx)
+
+    py_info = py_common.PyInfoBuilder()
+    py_info.set_has_py2_only_sources(False)
+    py_info.set_has_py3_only_sources(False)
+    py_info.transitive_sources.add(default_outputs)
+    py_info.imports.add([info.imports for info in pyproto_infos])
+    py_info.merge_all([
+        pyproto_info.py_info
+        for pyproto_info in pyproto_infos
+    ])
     return [
         DefaultInfo(
             files = default_outputs,
@@ -171,13 +192,7 @@
         OutputGroupInfo(
             default = depset(),
         ),
-        PyInfo(
-            transitive_sources = default_outputs,
-            imports = depset(transitive = [info.imports for info in pyproto_infos]),
-            # Proto always produces 2- and 3- compatible source files
-            has_py2_only_sources = False,
-            has_py3_only_sources = False,
-        ),
+        py_info.build(),
     ]
 
 py_proto_library = rule(
@@ -218,6 +233,6 @@
             providers = [ProtoInfo],
             aspects = [_py_proto_aspect],
         ),
-    },
+    } | _py_common.API_ATTRS,
     provides = [PyInfo],
 )
diff --git a/python/private/common/py_binary_macro_bazel.bzl b/python/private/py_binary_macro_bazel.bzl
similarity index 100%
rename from python/private/common/py_binary_macro_bazel.bzl
rename to python/private/py_binary_macro_bazel.bzl
diff --git a/python/private/py_binary_rule_bazel.bzl b/python/private/py_binary_rule_bazel.bzl
new file mode 100644
index 0000000..9ce0726
--- /dev/null
+++ b/python/private/py_binary_rule_bazel.bzl
@@ -0,0 +1,52 @@
+# Copyright 2022 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.
+"""Rule implementation of py_binary for Bazel."""
+
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
+load(":attributes.bzl", "AGNOSTIC_BINARY_ATTRS")
+load(
+    ":py_executable_bazel.bzl",
+    "create_executable_rule",
+    "py_executable_bazel_impl",
+)
+
+_PY_TEST_ATTRS = {
+    # Magic attribute to help C++ coverage work. There's no
+    # docs about this; see TestActionBuilder.java
+    "_collect_cc_coverage": attr.label(
+        default = "@bazel_tools//tools/test:collect_cc_coverage",
+        executable = True,
+        cfg = "exec",
+    ),
+    # Magic attribute to make coverage work. There's no
+    # docs about this; see TestActionBuilder.java
+    "_lcov_merger": attr.label(
+        default = configuration_field(fragment = "coverage", name = "output_generator"),
+        executable = True,
+        cfg = "exec",
+    ),
+}
+
+def _py_binary_impl(ctx):
+    return py_executable_bazel_impl(
+        ctx = ctx,
+        is_test = False,
+        inherited_environment = [],
+    )
+
+py_binary = create_executable_rule(
+    implementation = _py_binary_impl,
+    attrs = dicts.add(AGNOSTIC_BINARY_ATTRS, _PY_TEST_ATTRS),
+    executable = True,
+)
diff --git a/python/private/common/providers.bzl b/python/private/py_cc_link_params_info.bzl
similarity index 74%
rename from python/private/common/providers.bzl
rename to python/private/py_cc_link_params_info.bzl
index eb8b910..e5f4534 100644
--- a/python/private/common/providers.bzl
+++ b/python/private/py_cc_link_params_info.bzl
@@ -14,7 +14,7 @@
 """Providers for Python rules."""
 
 load("@rules_cc//cc:defs.bzl", "CcInfo")
-load("//python/private:util.bzl", "IS_BAZEL_6_OR_HIGHER")
+load(":util.bzl", "define_bazel_6_provider")
 
 DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3"
 
@@ -22,18 +22,6 @@
 
 _PYTHON_VERSION_VALUES = ["PY2", "PY3"]
 
-# Helper to make the provider definitions not crash under Bazel 5.4:
-# Bazel 5.4 doesn't support the `init` arg of `provider()`, so we have to
-# not pass that when using Bazel 5.4. But, not passing the `init` arg
-# changes the return value from a two-tuple to a single value, which then
-# breaks Bazel 6+ code.
-# This isn't actually used under Bazel 5.4, so just stub out the values
-# to get past the loading phase.
-def _define_provider(doc, fields, **kwargs):
-    if not IS_BAZEL_6_OR_HIGHER:
-        return provider("Stub, not used", fields = []), None
-    return provider(doc = doc, fields = fields, **kwargs)
-
 def _optional_int(value):
     return int(value) if value != None else None
 
@@ -133,9 +121,7 @@
         "zip_main_template": zip_main_template,
     }
 
-# TODO(#15897): Rename this to PyRuntimeInfo when we're ready to replace the Java
-# implemented provider with the Starlark one.
-PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = _define_provider(
+PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = define_bazel_6_provider(
     doc = """Contains information about a Python runtime, as returned by the `py_runtime`
 rule.
 
@@ -314,106 +300,17 @@
     },
 )
 
-def _check_arg_type(name, required_type, value):
-    value_type = type(value)
-    if value_type != required_type:
-        fail("parameter '{}' got value of type '{}', want '{}'".format(
-            name,
-            value_type,
-            required_type,
-        ))
-
-def _PyInfo_init(
-        *,
-        transitive_sources,
-        uses_shared_libraries = False,
-        imports = depset(),
-        has_py2_only_sources = False,
-        has_py3_only_sources = False,
-        direct_pyc_files = depset(),
-        transitive_pyc_files = depset()):
-    _check_arg_type("transitive_sources", "depset", transitive_sources)
-
-    # Verify it's postorder compatible, but retain is original ordering.
-    depset(transitive = [transitive_sources], order = "postorder")
-
-    _check_arg_type("uses_shared_libraries", "bool", uses_shared_libraries)
-    _check_arg_type("imports", "depset", imports)
-    _check_arg_type("has_py2_only_sources", "bool", has_py2_only_sources)
-    _check_arg_type("has_py3_only_sources", "bool", has_py3_only_sources)
-    _check_arg_type("direct_pyc_files", "depset", direct_pyc_files)
-    _check_arg_type("transitive_pyc_files", "depset", transitive_pyc_files)
-    return {
-        "direct_pyc_files": direct_pyc_files,
-        "has_py2_only_sources": has_py2_only_sources,
-        "has_py3_only_sources": has_py2_only_sources,
-        "imports": imports,
-        "transitive_pyc_files": transitive_pyc_files,
-        "transitive_sources": transitive_sources,
-        "uses_shared_libraries": uses_shared_libraries,
-    }
-
-PyInfo, _unused_raw_py_info_ctor = _define_provider(
-    doc = "Encapsulates information provided by the Python rules.",
-    init = _PyInfo_init,
-    fields = {
-        "direct_pyc_files": """
-:type: depset[File]
-
-Precompiled Python files that are considered directly provided
-by the target.
-""",
-        "has_py2_only_sources": """
-:type: bool
-
-Whether any of this target's transitive sources requires a Python 2 runtime.
-""",
-        "has_py3_only_sources": """
-:type: bool
-
-Whether any of this target's transitive sources requires a Python 3 runtime.
-""",
-        "imports": """\
-:type: depset[str]
-
-A depset of import path strings to be added to the `PYTHONPATH` of executable
-Python targets. These are accumulated from the transitive `deps`.
-The order of the depset is not guaranteed and may be changed in the future. It
-is recommended to use `default` order (the default).
-""",
-        "transitive_pyc_files": """
-:type: depset[File]
-
-Direct and transitive precompiled Python files that are provided by the target.
-""",
-        "transitive_sources": """\
-:type: depset[File]
-
-A (`postorder`-compatible) depset of `.py` files appearing in the target's
-`srcs` and the `srcs` of the target's transitive `deps`.
-""",
-        "uses_shared_libraries": """
-:type: bool
-
-Whether any of this target's transitive `deps` has a shared library file (such
-as a `.so` file).
-
-This field is currently unused in Bazel and may go away in the future.
-""",
-    },
-)
-
-def _PyCcLinkParamsProvider_init(cc_info):
+def _PyCcLinkParamsInfo_init(cc_info):
     return {
         "cc_info": CcInfo(linking_context = cc_info.linking_context),
     }
 
 # buildifier: disable=name-conventions
-PyCcLinkParamsProvider, _unused_raw_py_cc_link_params_provider_ctor = _define_provider(
+PyCcLinkParamsInfo, _unused_raw_py_cc_link_params_provider_ctor = define_bazel_6_provider(
     doc = ("Python-wrapper to forward {obj}`CcInfo.linking_context`. This is to " +
            "allow Python targets to propagate C++ linking information, but " +
            "without the Python target appearing to be a valid C++ rule dependency"),
-    init = _PyCcLinkParamsProvider_init,
+    init = _PyCcLinkParamsInfo_init,
     fields = {
         "cc_info": """
 :type: CcInfo
diff --git a/python/private/py_exec_tools_toolchain.bzl b/python/private/py_exec_tools_toolchain.bzl
index 957448f..edf9159 100644
--- a/python/private/py_exec_tools_toolchain.bzl
+++ b/python/private/py_exec_tools_toolchain.bzl
@@ -16,9 +16,9 @@
 
 load("@bazel_skylib//lib:paths.bzl", "paths")
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
-load("//python/private:sentinel.bzl", "SentinelInfo")
-load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
 load(":py_exec_tools_info.bzl", "PyExecToolsInfo")
+load(":sentinel.bzl", "SentinelInfo")
+load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
 
 def _py_exec_tools_toolchain_impl(ctx):
     extra_kwargs = {}
@@ -43,7 +43,7 @@
 Provides a toolchain for build time tools.
 
 This provides `ToolchainInfo` with the following attributes:
-* `exec_tools`: {type}`PyExecToolsInfo` 
+* `exec_tools`: {type}`PyExecToolsInfo`
 * `toolchain_label`: {type}`Label` _only present when `--visibile_for_testing=True`
   for internal testing_. The rule's label; this allows identifying what toolchain
   implmentation was selected for testing purposes.
diff --git a/python/private/common/py_executable.bzl b/python/private/py_executable.bzl
similarity index 87%
rename from python/private/common/py_executable.bzl
rename to python/private/py_executable.bzl
index 80418ac..ce1288c 100644
--- a/python/private/common/py_executable.bzl
+++ b/python/private/py_executable.bzl
@@ -17,24 +17,19 @@
 load("@bazel_skylib//lib:structs.bzl", "structs")
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("@rules_cc//cc:defs.bzl", "cc_common")
-load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag")
-load("//python/private:py_executable_info.bzl", "PyExecutableInfo")
-load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
-load(
-    "//python/private:toolchain_types.bzl",
-    "EXEC_TOOLS_TOOLCHAIN_TYPE",
-    TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE",
-)
 load(
     ":attributes.bzl",
     "AGNOSTIC_EXECUTABLE_ATTRS",
     "COMMON_ATTRS",
     "PY_SRCS_ATTRS",
+    "PrecompileAttr",
     "PycCollectionAttr",
+    "REQUIRED_EXEC_GROUPS",
     "SRCS_VERSION_ALL_VALUES",
     "create_srcs_attr",
     "create_srcs_version_attr",
 )
+load(":builders.bzl", "builders")
 load(":cc_helper.bzl", "cc_helper")
 load(
     ":common.bzl",
@@ -49,13 +44,12 @@
     "target_platform_has_any_constraint",
     "union_attrs",
 )
-load(
-    ":providers.bzl",
-    "PyCcLinkParamsProvider",
-    "PyInfo",
-    "PyRuntimeInfo",
-)
+load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
+load(":py_executable_info.bzl", "PyExecutableInfo")
+load(":py_info.bzl", "PyInfo")
 load(":py_internal.bzl", "py_internal")
+load(":py_runtime_info.bzl", "PyRuntimeInfo")
+load(":reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
 load(
     ":semantics.bzl",
     "ALLOWED_MAIN_EXTENSIONS",
@@ -63,6 +57,11 @@
     "IS_BAZEL",
     "PY_RUNTIME_ATTR_NAME",
 )
+load(
+    ":toolchain_types.bzl",
+    "EXEC_TOOLS_TOOLCHAIN_TYPE",
+    TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE",
+)
 
 _py_builtins = py_internal
 
@@ -98,16 +97,13 @@
             doc = """
 Determines whether pyc files from dependencies should be manually included.
 
-NOTE: This setting is only useful with {flag}`--precompile_add_to_runfiles=decided_elsewhere`.
-
 Valid values are:
-* `inherit`: Inherit the value from {flag}`--pyc_collection`.
-* `include_pyc`: Add pyc files from dependencies in the binary (from
-  {obj}`PyInfo.transitive_pyc_files`.
-* `disabled`: Don't explicitly add pyc files from dependencies. Note that
-  pyc files may still come from dependencies if a target includes them as
-  part of their runfiles (such as when {obj}`--precompile_add_to_runfiles=always`
-  is used).
+* `inherit`: Inherit the value from {flag}`--precompile`.
+* `include_pyc`: Add implicitly generated pyc files from dependencies. i.e.
+  pyc files for targets that specify {attr}`precompile="inherit"`.
+* `disabled`: Don't add implicitly generated pyc files. Note that
+  pyc files may still come from dependencies that enable precompiling at the
+  target level.
 """,
         ),
         # TODO(b/203567235): In Google, this attribute is deprecated, and can
@@ -125,10 +121,6 @@
             default = "//python/config_settings:bootstrap_impl",
             providers = [BuildSettingInfo],
         ),
-        "_pyc_collection_flag": attr.label(
-            default = "//python/config_settings:pyc_collection",
-            providers = [BuildSettingInfo],
-        ),
         "_windows_constraints": attr.label_list(
             default = [
                 "@platforms//os:windows",
@@ -163,16 +155,27 @@
     direct_sources = filter_to_py_srcs(ctx.files.srcs)
     precompile_result = semantics.maybe_precompile(ctx, direct_sources)
 
+    required_py_files = precompile_result.keep_srcs
+    required_pyc_files = []
+    implicit_pyc_files = []
+    implicit_pyc_source_files = direct_sources
+
+    if ctx.attr.precompile == PrecompileAttr.ENABLED:
+        required_pyc_files.extend(precompile_result.pyc_files)
+    else:
+        implicit_pyc_files.extend(precompile_result.pyc_files)
+
     # Sourceless precompiled builds omit the main py file from outputs, so
     # main has to be pointed to the precompiled main instead.
-    if main_py not in precompile_result.keep_srcs:
+    if (main_py not in precompile_result.keep_srcs and
+        PycCollectionAttr.is_pyc_collection_enabled(ctx)):
         main_py = precompile_result.py_to_pyc_map[main_py]
-    direct_pyc_files = depset(precompile_result.pyc_files)
 
     executable = _declare_executable_file(ctx)
-    default_outputs = [executable]
-    default_outputs.extend(precompile_result.keep_srcs)
-    default_outputs.extend(precompile_result.pyc_files)
+    default_outputs = builders.DepsetBuilder()
+    default_outputs.add(executable)
+    default_outputs.add(precompile_result.keep_srcs)
+    default_outputs.add(required_pyc_files)
 
     imports = collect_imports(ctx, semantics)
 
@@ -198,8 +201,10 @@
         ctx,
         executable = executable,
         extra_deps = extra_deps,
-        main_py_files = depset([main_py] + precompile_result.keep_srcs),
-        direct_pyc_files = direct_pyc_files,
+        required_py_files = required_py_files,
+        required_pyc_files = required_pyc_files,
+        implicit_pyc_files = implicit_pyc_files,
+        implicit_pyc_source_files = implicit_pyc_source_files,
         extra_common_runfiles = [
             runtime_details.runfiles,
             cc_details.extra_runfiles,
@@ -219,6 +224,7 @@
         native_deps_details = native_deps_details,
         runfiles_details = runfiles_details,
     )
+    default_outputs.add(exec_result.extra_files_to_build)
 
     extra_exec_runfiles = exec_result.extra_runfiles.merge(
         ctx.runfiles(transitive_files = exec_result.extra_files_to_build),
@@ -238,9 +244,11 @@
         runfiles_details = runfiles_details,
         main_py = main_py,
         imports = imports,
-        direct_sources = direct_sources,
-        direct_pyc_files = direct_pyc_files,
-        default_outputs = depset(default_outputs, transitive = [exec_result.extra_files_to_build]),
+        required_py_files = required_py_files,
+        required_pyc_files = required_pyc_files,
+        implicit_pyc_files = implicit_pyc_files,
+        implicit_pyc_source_files = implicit_pyc_source_files,
+        default_outputs = default_outputs.build(),
         runtime_details = runtime_details,
         cc_info = cc_details.cc_info_for_propagating,
         inherited_environment = inherited_environment,
@@ -400,8 +408,10 @@
         *,
         executable,
         extra_deps,
-        main_py_files,
-        direct_pyc_files,
+        required_py_files,
+        required_pyc_files,
+        implicit_pyc_files,
+        implicit_pyc_source_files,
         extra_common_runfiles,
         semantics):
     """Returns the set of runfiles necessary prior to executable creation.
@@ -414,8 +424,15 @@
         executable: The main executable output.
         extra_deps: List of Targets; additional targets whose runfiles
             will be added to the common runfiles.
-        main_py_files: depset of File of the default outputs to add into runfiles.
-        direct_pyc_files: depset of File of pyc files directly from this target.
+        required_py_files: `depset[File]` the direct, `.py` sources for the
+            target that **must** be included by downstream targets. This should
+            only be Python source files. It should not include pyc files.
+        required_pyc_files: `depset[File]` the direct `.pyc` files this target
+            produces.
+        implicit_pyc_files: `depset[File]` pyc files that are only used if pyc
+            collection is enabled.
+        implicit_pyc_source_files: `depset[File]` source files for implicit pyc
+            files that are used when the implicit pyc files are not.
         extra_common_runfiles: List of runfiles; additional runfiles that
             will be added to the common runfiles.
         semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`.
@@ -429,26 +446,37 @@
         * build_data_file: A file with build stamp information if stamping is enabled, otherwise
           None.
     """
-    common_runfiles_depsets = [main_py_files]
+    common_runfiles = builders.RunfilesBuilder()
+    common_runfiles.files.add(required_py_files)
+    common_runfiles.files.add(required_pyc_files)
+    pyc_collection_enabled = PycCollectionAttr.is_pyc_collection_enabled(ctx)
+    if pyc_collection_enabled:
+        common_runfiles.files.add(implicit_pyc_files)
+    else:
+        common_runfiles.files.add(implicit_pyc_source_files)
 
-    if ctx.attr._precompile_add_to_runfiles_flag[BuildSettingInfo].value == PrecompileAddToRunfilesFlag.ALWAYS:
-        common_runfiles_depsets.append(direct_pyc_files)
-    elif PycCollectionAttr.is_pyc_collection_enabled(ctx):
-        common_runfiles_depsets.append(direct_pyc_files)
-        for dep in (ctx.attr.deps + extra_deps):
-            if PyInfo not in dep:
-                continue
-            common_runfiles_depsets.append(dep[PyInfo].transitive_pyc_files)
+    for dep in (ctx.attr.deps + extra_deps):
+        if not (PyInfo in dep or BuiltinPyInfo in dep):
+            continue
+        info = dep[PyInfo] if PyInfo in dep else dep[BuiltinPyInfo]
+        common_runfiles.files.add(info.transitive_sources)
 
-    common_runfiles = collect_runfiles(ctx, depset(
-        transitive = common_runfiles_depsets,
-    ))
+        # Everything past this won't work with BuiltinPyInfo
+        if not hasattr(info, "transitive_pyc_files"):
+            continue
+
+        common_runfiles.files.add(info.transitive_pyc_files)
+        if pyc_collection_enabled:
+            common_runfiles.files.add(info.transitive_implicit_pyc_files)
+        else:
+            common_runfiles.files.add(info.transitive_implicit_pyc_source_files)
+
+    common_runfiles.runfiles.append(collect_runfiles(ctx))
     if extra_deps:
-        common_runfiles = common_runfiles.merge_all([
-            t[DefaultInfo].default_runfiles
-            for t in extra_deps
-        ])
-    common_runfiles = common_runfiles.merge_all(extra_common_runfiles)
+        common_runfiles.add_runfiles(targets = extra_deps)
+    common_runfiles.add(extra_common_runfiles)
+
+    common_runfiles = common_runfiles.build(ctx)
 
     if semantics.should_create_init_files(ctx):
         common_runfiles = _py_builtins.merge_runfiles_with_generated_inits_empty_files_supplier(
@@ -780,8 +808,10 @@
         ctx,
         executable,
         main_py,
-        direct_sources,
-        direct_pyc_files,
+        required_py_files,
+        required_pyc_files,
+        implicit_pyc_files,
+        implicit_pyc_source_files,
         default_outputs,
         runfiles_details,
         imports,
@@ -796,15 +826,20 @@
         ctx: The rule ctx.
         executable: File; the target's executable file.
         main_py: File; the main .py entry point.
-        direct_sources: list of Files; the direct, raw `.py` sources for the target.
-            This should only be Python source files. It should not include pyc
-            files.
-        direct_pyc_files: depset of File; the direct pyc files for the target.
+        required_py_files: `depset[File]` the direct, `.py` sources for the
+            target that **must** be included by downstream targets. This should
+            only be Python source files. It should not include pyc files.
+        required_pyc_files: `depset[File]` the direct `.pyc` files this target
+            produces.
+        implicit_pyc_files: `depset[File]` pyc files that are only used if pyc
+            collection is enabled.
+        implicit_pyc_source_files: `depset[File]` source files for implicit pyc
+            files that are used when the implicit pyc files are not.
         default_outputs: depset of Files; the files for DefaultInfo.files
         runfiles_details: runfiles that will become the default  and data runfiles.
         imports: depset of strings; the import paths to propagate
         cc_info: optional CcInfo; Linking information to propagate as
-            PyCcLinkParamsProvider. Note that only the linking information
+            PyCcLinkParamsInfo. Note that only the linking information
             is propagated, not the whole CcInfo.
         inherited_environment: list of strings; Environment variable names
             that should be inherited from the environment the executuble
@@ -853,7 +888,7 @@
         # builtin py_runtime rule or defined their own. We can't directly detect
         # the type of the provider object, but the rules_python PyRuntimeInfo
         # object has an extra attribute that the builtin one doesn't.
-        if hasattr(py_runtime_info, "interpreter_version_info"):
+        if hasattr(py_runtime_info, "interpreter_version_info") and BuiltinPyRuntimeInfo != None:
             providers.append(BuiltinPyRuntimeInfo(
                 interpreter_path = py_runtime_info.interpreter_path,
                 interpreter = py_runtime_info.interpreter,
@@ -865,17 +900,19 @@
                 bootstrap_template = py_runtime_info.bootstrap_template,
             ))
 
-    # TODO(b/163083591): Remove the PyCcLinkParamsProvider once binaries-in-deps
+    # TODO(b/163083591): Remove the PyCcLinkParamsInfo once binaries-in-deps
     # are cleaned up.
     if cc_info:
         providers.append(
-            PyCcLinkParamsProvider(cc_info = cc_info),
+            PyCcLinkParamsInfo(cc_info = cc_info),
         )
 
     py_info, deps_transitive_sources, builtin_py_info = create_py_info(
         ctx,
-        direct_sources = depset(direct_sources),
-        direct_pyc_files = direct_pyc_files,
+        required_py_files = required_py_files,
+        required_pyc_files = required_pyc_files,
+        implicit_pyc_files = implicit_pyc_files,
+        implicit_pyc_source_files = implicit_pyc_source_files,
         imports = imports,
     )
 
@@ -888,7 +925,8 @@
         )
 
     providers.append(py_info)
-    providers.append(builtin_py_info)
+    if builtin_py_info:
+        providers.append(builtin_py_info)
     providers.append(create_output_group_info(py_info.transitive_sources, output_groups))
 
     extra_providers = semantics.get_extra_providers(
@@ -928,6 +966,7 @@
         # The list might be frozen, so use concatentation
         fragments = fragments + ["py"]
     kwargs.setdefault("provides", []).append(PyExecutableInfo)
+    kwargs["exec_groups"] = REQUIRED_EXEC_GROUPS | (kwargs.get("exec_groups") or {})
     return rule(
         # TODO: add ability to remove attrs, i.e. for imports attr
         attrs = dicts.add(EXECUTABLE_ATTRS, attrs),
diff --git a/python/private/common/py_executable_bazel.bzl b/python/private/py_executable_bazel.bzl
similarity index 98%
rename from python/private/common/py_executable_bazel.bzl
rename to python/private/py_executable_bazel.bzl
index dae1c4a..53206bd 100644
--- a/python/private/common/py_executable_bazel.bzl
+++ b/python/private/py_executable_bazel.bzl
@@ -15,8 +15,6 @@
 
 load("@bazel_skylib//lib:dicts.bzl", "dicts")
 load("@bazel_skylib//lib:paths.bzl", "paths")
-load("//python/private:flags.bzl", "BootstrapImplFlag")
-load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
 load(":attributes_bazel.bzl", "IMPORTS_ATTRS")
 load(
     ":common.bzl",
@@ -27,13 +25,15 @@
     "union_attrs",
 )
 load(":common_bazel.bzl", "collect_cc_info", "get_imports", "maybe_precompile")
-load(":providers.bzl", "DEFAULT_STUB_SHEBANG")
+load(":flags.bzl", "BootstrapImplFlag")
 load(
     ":py_executable.bzl",
     "create_base_executable_rule",
     "py_executable_base_impl",
 )
 load(":py_internal.bzl", "py_internal")
+load(":py_runtime_info.bzl", "DEFAULT_STUB_SHEBANG")
+load(":toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")
 
 _py_builtins = py_internal
 _EXTERNAL_PATH_PREFIX = "external"
diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl
new file mode 100644
index 0000000..7a0bdea
--- /dev/null
+++ b/python/private/py_info.bzl
@@ -0,0 +1,286 @@
+# 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.
+"""Implementation of PyInfo provider and PyInfo-specific utilities."""
+
+load("@rules_python_internal//:rules_python_config.bzl", "config")
+load(":builders.bzl", "builders")
+load(":reexports.bzl", "BuiltinPyInfo")
+load(":util.bzl", "define_bazel_6_provider")
+
+def _check_arg_type(name, required_type, value):
+    """Check that a value is of an expected type."""
+    value_type = type(value)
+    if value_type != required_type:
+        fail("parameter '{}' got value of type '{}', want '{}'".format(
+            name,
+            value_type,
+            required_type,
+        ))
+
+def _PyInfo_init(
+        *,
+        transitive_sources,
+        uses_shared_libraries = False,
+        imports = depset(),
+        has_py2_only_sources = False,
+        has_py3_only_sources = False,
+        direct_pyc_files = depset(),
+        transitive_pyc_files = depset(),
+        transitive_implicit_pyc_files = depset(),
+        transitive_implicit_pyc_source_files = depset()):
+    _check_arg_type("transitive_sources", "depset", transitive_sources)
+
+    # Verify it's postorder compatible, but retain is original ordering.
+    depset(transitive = [transitive_sources], order = "postorder")
+
+    _check_arg_type("uses_shared_libraries", "bool", uses_shared_libraries)
+    _check_arg_type("imports", "depset", imports)
+    _check_arg_type("has_py2_only_sources", "bool", has_py2_only_sources)
+    _check_arg_type("has_py3_only_sources", "bool", has_py3_only_sources)
+    _check_arg_type("direct_pyc_files", "depset", direct_pyc_files)
+    _check_arg_type("transitive_pyc_files", "depset", transitive_pyc_files)
+
+    _check_arg_type("transitive_implicit_pyc_files", "depset", transitive_pyc_files)
+    _check_arg_type("transitive_implicit_pyc_source_files", "depset", transitive_pyc_files)
+    return {
+        "direct_pyc_files": direct_pyc_files,
+        "has_py2_only_sources": has_py2_only_sources,
+        "has_py3_only_sources": has_py2_only_sources,
+        "imports": imports,
+        "transitive_implicit_pyc_files": transitive_implicit_pyc_files,
+        "transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files,
+        "transitive_pyc_files": transitive_pyc_files,
+        "transitive_sources": transitive_sources,
+        "uses_shared_libraries": uses_shared_libraries,
+    }
+
+PyInfo, _unused_raw_py_info_ctor = define_bazel_6_provider(
+    doc = "Encapsulates information provided by the Python rules.",
+    init = _PyInfo_init,
+    fields = {
+        "direct_pyc_files": """
+:type: depset[File]
+
+Precompiled Python files that are considered directly provided
+by the target and **must be included**.
+
+These files usually come from, e.g., a library setting {attr}`precompile=enabled`
+to forcibly enable precompiling for itself. Downstream binaries are expected
+to always include these files, as the originating target expects them to exist.
+""",
+        "has_py2_only_sources": """
+:type: bool
+
+Whether any of this target's transitive sources requires a Python 2 runtime.
+""",
+        "has_py3_only_sources": """
+:type: bool
+
+Whether any of this target's transitive sources requires a Python 3 runtime.
+""",
+        "imports": """\
+:type: depset[str]
+
+A depset of import path strings to be added to the `PYTHONPATH` of executable
+Python targets. These are accumulated from the transitive `deps`.
+The order of the depset is not guaranteed and may be changed in the future. It
+is recommended to use `default` order (the default).
+""",
+        "transitive_implicit_pyc_files": """
+:type: depset[File]
+
+Automatically generated pyc files that downstream binaries (or equivalent)
+can choose to include in their output. If not included, then
+{obj}`transitive_implicit_pyc_source_files` should be included instead.
+
+::::{versionadded} 0.37.0
+::::
+""",
+        "transitive_implicit_pyc_source_files": """
+:type: depset[File]
+
+Source `.py` files for {obj}`transitive_implicit_pyc_files` that downstream
+binaries (or equivalent) can choose to include in their output. If not included,
+then {obj}`transitive_implicit_pyc_files` should be included instead.
+
+::::{versionadded} 0.37.0
+::::
+""",
+        "transitive_pyc_files": """
+:type: depset[File]
+
+The transitive set of precompiled files that must be included.
+
+These files usually come from, e.g., a library setting {attr}`precompile=enabled`
+to forcibly enable precompiling for itself. Downstream binaries are expected
+to always include these files, as the originating target expects them to exist.
+""",
+        "transitive_sources": """\
+:type: depset[File]
+
+A (`postorder`-compatible) depset of `.py` files appearing in the target's
+`srcs` and the `srcs` of the target's transitive `deps`.
+
+These are `.py` source files that are considered required and downstream
+binaries (or equivalent) must include in their outputs.
+
+::::{versionchanged} 0.37.0
+The files are considered necessary for downstream binaries to function;
+previously they were considerd informational and largely unused.
+::::
+""",
+        "uses_shared_libraries": """
+:type: bool
+
+Whether any of this target's transitive `deps` has a shared library file (such
+as a `.so` file).
+
+This field is currently unused in Bazel and may go away in the future.
+""",
+    },
+)
+
+# The "effective" PyInfo is what the canonical //python:py_info.bzl%PyInfo symbol refers to
+_EffectivePyInfo = PyInfo if (config.enable_pystar or BuiltinPyInfo == None) else BuiltinPyInfo
+
+def PyInfoBuilder():
+    # buildifier: disable=uninitialized
+    self = struct(
+        _has_py2_only_sources = [False],
+        _has_py3_only_sources = [False],
+        _uses_shared_libraries = [False],
+        build = lambda *a, **k: _PyInfoBuilder_build(self, *a, **k),
+        build_builtin_py_info = lambda *a, **k: _PyInfoBuilder_build_builtin_py_info(self, *a, **k),
+        direct_pyc_files = builders.DepsetBuilder(),
+        get_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_get_has_py2_only_sources(self, *a, **k),
+        get_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_get_has_py3_only_sources(self, *a, **k),
+        get_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_get_uses_shared_libraries(self, *a, **k),
+        imports = builders.DepsetBuilder(),
+        merge = lambda *a, **k: _PyInfoBuilder_merge(self, *a, **k),
+        merge_all = lambda *a, **k: _PyInfoBuilder_merge_all(self, *a, **k),
+        merge_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_merge_has_py2_only_sources(self, *a, **k),
+        merge_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_merge_has_py3_only_sources(self, *a, **k),
+        merge_target = lambda *a, **k: _PyInfoBuilder_merge_target(self, *a, **k),
+        merge_targets = lambda *a, **k: _PyInfoBuilder_merge_targets(self, *a, **k),
+        merge_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_merge_uses_shared_libraries(self, *a, **k),
+        set_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_set_has_py2_only_sources(self, *a, **k),
+        set_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_set_has_py3_only_sources(self, *a, **k),
+        set_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_set_uses_shared_libraries(self, *a, **k),
+        transitive_implicit_pyc_files = builders.DepsetBuilder(),
+        transitive_implicit_pyc_source_files = builders.DepsetBuilder(),
+        transitive_pyc_files = builders.DepsetBuilder(),
+        transitive_sources = builders.DepsetBuilder(),
+    )
+    return self
+
+def _PyInfoBuilder_get_has_py3_only_sources(self):
+    return self._has_py3_only_sources[0]
+
+def _PyInfoBuilder_get_has_py2_only_sources(self):
+    return self._has_py2_only_sources[0]
+
+def _PyInfoBuilder_set_has_py2_only_sources(self, value):
+    self._has_py2_only_sources[0] = value
+    return self
+
+def _PyInfoBuilder_set_has_py3_only_sources(self, value):
+    self._has_py3_only_sources[0] = value
+    return self
+
+def _PyInfoBuilder_merge_has_py2_only_sources(self, value):
+    self._has_py2_only_sources[0] = self._has_py2_only_sources[0] or value
+    return self
+
+def _PyInfoBuilder_merge_has_py3_only_sources(self, value):
+    self._has_py3_only_sources[0] = self._has_py3_only_sources[0] or value
+    return self
+
+def _PyInfoBuilder_merge_uses_shared_libraries(self, value):
+    self._uses_shared_libraries[0] = self._uses_shared_libraries[0] or value
+    return self
+
+def _PyInfoBuilder_get_uses_shared_libraries(self):
+    return self._uses_shared_libraries[0]
+
+def _PyInfoBuilder_set_uses_shared_libraries(self, value):
+    self._uses_shared_libraries[0] = value
+    return self
+
+def _PyInfoBuilder_merge(self, *infos, direct = []):
+    return self.merge_all(list(infos), direct = direct)
+
+def _PyInfoBuilder_merge_all(self, transitive, *, direct = []):
+    for info in direct:
+        # BuiltinPyInfo doesn't have this field
+        if hasattr(info, "direct_pyc_files"):
+            self.direct_pyc_files.add(info.direct_pyc_files)
+
+    for info in direct + transitive:
+        self.imports.add(info.imports)
+        self.merge_has_py2_only_sources(info.has_py2_only_sources)
+        self.merge_has_py3_only_sources(info.has_py3_only_sources)
+        self.merge_uses_shared_libraries(info.uses_shared_libraries)
+        self.transitive_sources.add(info.transitive_sources)
+
+        # BuiltinPyInfo doesn't have these fields
+        if hasattr(info, "transitive_pyc_files"):
+            self.transitive_implicit_pyc_files.add(info.transitive_implicit_pyc_files)
+            self.transitive_implicit_pyc_source_files.add(info.transitive_implicit_pyc_source_files)
+            self.transitive_pyc_files.add(info.transitive_pyc_files)
+
+    return self
+
+def _PyInfoBuilder_merge_target(self, target):
+    if PyInfo in target:
+        self.merge(target[PyInfo])
+    elif BuiltinPyInfo != None and BuiltinPyInfo in target:
+        self.merge(target[BuiltinPyInfo])
+    return self
+
+def _PyInfoBuilder_merge_targets(self, targets):
+    for t in targets:
+        self.merge_target(t)
+    return self
+
+def _PyInfoBuilder_build(self):
+    if config.enable_pystar:
+        kwargs = dict(
+            direct_pyc_files = self.direct_pyc_files.build(),
+            transitive_pyc_files = self.transitive_pyc_files.build(),
+            transitive_implicit_pyc_files = self.transitive_implicit_pyc_files.build(),
+            transitive_implicit_pyc_source_files = self.transitive_implicit_pyc_source_files.build(),
+        )
+    else:
+        kwargs = {}
+
+    return _EffectivePyInfo(
+        has_py2_only_sources = self._has_py2_only_sources[0],
+        has_py3_only_sources = self._has_py3_only_sources[0],
+        imports = self.imports.build(),
+        transitive_sources = self.transitive_sources.build(),
+        uses_shared_libraries = self._uses_shared_libraries[0],
+        **kwargs
+    )
+
+def _PyInfoBuilder_build_builtin_py_info(self):
+    if BuiltinPyInfo == None:
+        return None
+
+    return BuiltinPyInfo(
+        has_py2_only_sources = self._has_py2_only_sources[0],
+        has_py3_only_sources = self._has_py3_only_sources[0],
+        imports = self.imports.build(),
+        transitive_sources = self.transitive_sources.build(),
+        uses_shared_libraries = self._uses_shared_libraries[0],
+    )
diff --git a/python/private/common/py_internal.bzl b/python/private/py_internal.bzl
similarity index 100%
rename from python/private/common/py_internal.bzl
rename to python/private/py_internal.bzl
diff --git a/python/private/common/py_library.bzl b/python/private/py_library.bzl
similarity index 68%
rename from python/private/common/py_library.bzl
rename to python/private/py_library.bzl
index fd53490..4f43116 100644
--- a/python/private/common/py_library.bzl
+++ b/python/private/py_library.bzl
@@ -15,20 +15,17 @@
 
 load("@bazel_skylib//lib:dicts.bzl", "dicts")
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
-load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag")
-load(
-    "//python/private:toolchain_types.bzl",
-    "EXEC_TOOLS_TOOLCHAIN_TYPE",
-    TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE",
-)
 load(
     ":attributes.bzl",
     "COMMON_ATTRS",
     "PY_SRCS_ATTRS",
+    "PrecompileAttr",
+    "REQUIRED_EXEC_GROUPS",
     "SRCS_VERSION_ALL_VALUES",
     "create_srcs_attr",
     "create_srcs_version_attr",
 )
+load(":builders.bzl", "builders")
 load(
     ":common.bzl",
     "check_native_allowed",
@@ -40,8 +37,14 @@
     "filter_to_py_srcs",
     "union_attrs",
 )
-load(":providers.bzl", "PyCcLinkParamsProvider")
+load(":flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag")
+load(":py_cc_link_params_info.bzl", "PyCcLinkParamsInfo")
 load(":py_internal.bzl", "py_internal")
+load(
+    ":toolchain_types.bzl",
+    "EXEC_TOOLS_TOOLCHAIN_TYPE",
+    TOOLCHAIN_TYPE = "TARGET_TOOLCHAIN_TYPE",
+)
 
 _py_builtins = py_internal
 
@@ -50,6 +53,11 @@
     PY_SRCS_ATTRS,
     create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES),
     create_srcs_attr(mandatory = False),
+    {
+        "_add_srcs_to_runfiles_flag": attr.label(
+            default = "//python/config_settings:add_srcs_to_runfiles",
+        ),
+    },
 )
 
 def py_library_impl(ctx, *, semantics):
@@ -66,24 +74,39 @@
     direct_sources = filter_to_py_srcs(ctx.files.srcs)
 
     precompile_result = semantics.maybe_precompile(ctx, direct_sources)
-    direct_pyc_files = depset(precompile_result.pyc_files)
-    default_outputs = depset(precompile_result.keep_srcs, transitive = [direct_pyc_files])
 
-    extra_runfiles_depsets = [depset(precompile_result.keep_srcs)]
-    if ctx.attr._precompile_add_to_runfiles_flag[BuildSettingInfo].value == PrecompileAddToRunfilesFlag.ALWAYS:
-        extra_runfiles_depsets.append(direct_pyc_files)
+    required_py_files = precompile_result.keep_srcs
+    required_pyc_files = []
+    implicit_pyc_files = []
+    implicit_pyc_source_files = direct_sources
 
-    runfiles = collect_runfiles(
-        ctx = ctx,
-        files = depset(transitive = extra_runfiles_depsets),
-    )
+    precompile_attr = ctx.attr.precompile
+    precompile_flag = ctx.attr._precompile_flag[BuildSettingInfo].value
+    if (precompile_attr == PrecompileAttr.ENABLED or
+        precompile_flag == PrecompileFlag.FORCE_ENABLED):
+        required_pyc_files.extend(precompile_result.pyc_files)
+    else:
+        implicit_pyc_files.extend(precompile_result.pyc_files)
+
+    default_outputs = builders.DepsetBuilder()
+    default_outputs.add(precompile_result.keep_srcs)
+    default_outputs.add(required_pyc_files)
+    default_outputs = default_outputs.build()
+
+    runfiles = builders.RunfilesBuilder()
+    if AddSrcsToRunfilesFlag.is_enabled(ctx):
+        runfiles.add(required_py_files)
+    runfiles.add(collect_runfiles(ctx))
+    runfiles = runfiles.build(ctx)
 
     cc_info = semantics.get_cc_info_for_library(ctx)
     py_info, deps_transitive_sources, builtins_py_info = create_py_info(
         ctx,
-        direct_sources = depset(direct_sources),
+        required_py_files = required_py_files,
+        required_pyc_files = required_pyc_files,
+        implicit_pyc_files = implicit_pyc_files,
+        implicit_pyc_source_files = implicit_pyc_source_files,
         imports = collect_imports(ctx, semantics),
-        direct_pyc_files = direct_pyc_files,
     )
 
     # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455
@@ -94,14 +117,16 @@
             dependency_transitive_python_sources = deps_transitive_sources,
         )
 
-    return [
+    providers = [
         DefaultInfo(files = default_outputs, runfiles = runfiles),
         py_info,
-        builtins_py_info,
         create_instrumented_files_info(ctx),
-        PyCcLinkParamsProvider(cc_info = cc_info),
+        PyCcLinkParamsInfo(cc_info = cc_info),
         create_output_group_info(py_info.transitive_sources, extra_groups = {}),
     ]
+    if builtins_py_info:
+        providers.append(builtins_py_info)
+    return providers
 
 _DEFAULT_PY_LIBRARY_DOC = """
 A library of Python code that can be depended upon.
@@ -113,6 +138,10 @@
 NOTE: Precompilation affects which of the default outputs are included in the
 resulting runfiles. See the precompile-related attributes and flags for
 more information.
+
+:::{versionchanged} 0.37.0
+Source files are no longer added to the runfiles directly.
+:::
 """
 
 def create_py_library_rule(*, attrs = {}, **kwargs):
@@ -131,6 +160,7 @@
     # TODO: b/253818097 - fragments=py is only necessary so that
     # RequiredConfigFragmentsTest passes
     fragments = kwargs.pop("fragments", None) or []
+    kwargs["exec_groups"] = REQUIRED_EXEC_GROUPS | (kwargs.get("exec_groups") or {})
     return rule(
         attrs = dicts.add(LIBRARY_ATTRS, attrs),
         toolchains = [
diff --git a/python/private/common/py_library_macro_bazel.bzl b/python/private/py_library_macro_bazel.bzl
similarity index 100%
rename from python/private/common/py_library_macro_bazel.bzl
rename to python/private/py_library_macro_bazel.bzl
diff --git a/python/private/py_library_rule_bazel.bzl b/python/private/py_library_rule_bazel.bzl
new file mode 100644
index 0000000..453abcb
--- /dev/null
+++ b/python/private/py_library_rule_bazel.bzl
@@ -0,0 +1,47 @@
+# Copyright 2022 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.
+"""Implementation of py_library for Bazel."""
+
+load(":attributes_bazel.bzl", "IMPORTS_ATTRS")
+load(":common.bzl", "create_library_semantics_struct", "union_attrs")
+load(":common_bazel.bzl", "collect_cc_info", "get_imports", "maybe_precompile")
+load(
+    ":py_library.bzl",
+    "LIBRARY_ATTRS",
+    "create_py_library_rule",
+    bazel_py_library_impl = "py_library_impl",
+)
+
+_BAZEL_LIBRARY_ATTRS = union_attrs(
+    LIBRARY_ATTRS,
+    IMPORTS_ATTRS,
+)
+
+def create_library_semantics_bazel():
+    return create_library_semantics_struct(
+        get_imports = get_imports,
+        maybe_precompile = maybe_precompile,
+        get_cc_info_for_library = collect_cc_info,
+    )
+
+def _py_library_impl(ctx):
+    return bazel_py_library_impl(
+        ctx,
+        semantics = create_library_semantics_bazel(),
+    )
+
+py_library = create_py_library_rule(
+    implementation = _py_library_impl,
+    attrs = _BAZEL_LIBRARY_ATTRS,
+)
diff --git a/python/private/py_package.bzl b/python/private/py_package.bzl
index 08f4b0b..fd8bc27 100644
--- a/python/private/py_package.bzl
+++ b/python/private/py_package.bzl
@@ -11,9 +11,11 @@
 # 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.
-
 "Implementation of py_package rule"
 
+load(":builders.bzl", "builders")
+load(":py_info.bzl", "PyInfoBuilder")
+
 def _path_inside_wheel(input_file):
     # input_file.short_path is sometimes relative ("../${repository_root}/foobar")
     # which is not a valid path within a zip file. Fix that.
@@ -31,10 +33,20 @@
     return short_path
 
 def _py_package_impl(ctx):
-    inputs = depset(
-        transitive = [dep[DefaultInfo].data_runfiles.files for dep in ctx.attr.deps] +
-                     [dep[DefaultInfo].default_runfiles.files for dep in ctx.attr.deps],
-    )
+    inputs = builders.DepsetBuilder()
+    py_info = PyInfoBuilder()
+    for dep in ctx.attr.deps:
+        inputs.add(dep[DefaultInfo].data_runfiles.files)
+        inputs.add(dep[DefaultInfo].default_runfiles.files)
+        py_info.merge_target(dep)
+    py_info = py_info.build()
+    inputs.add(py_info.transitive_sources)
+
+    # Remove conditional once Bazel 6 support dropped.
+    if hasattr(py_info, "transitive_pyc_files"):
+        inputs.add(py_info.transitive_pyc_files)
+
+    inputs = inputs.build()
 
     # TODO: '/' is wrong on windows, but the path separator is not available in starlark.
     # Fix this once ctx.configuration has directory separator information.
diff --git a/python/private/py_repositories.bzl b/python/private/py_repositories.bzl
index 8ddcb5d..ff8a638 100644
--- a/python/private/py_repositories.bzl
+++ b/python/private/py_repositories.bzl
@@ -16,8 +16,10 @@
 
 load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive")
 load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
+load("//python:versions.bzl", "MINOR_MAPPING", "TOOL_VERSIONS")
 load("//python/private/pypi:deps.bzl", "pypi_deps")
 load(":internal_config_repo.bzl", "internal_config_repo")
+load(":pythons_hub.bzl", "hub_repo")
 
 def http_archive(**kwargs):
     maybe(_http_archive, **kwargs)
@@ -32,6 +34,17 @@
         internal_config_repo,
         name = "rules_python_internal",
     )
+    maybe(
+        hub_repo,
+        name = "pythons_hub",
+        minor_mapping = MINOR_MAPPING,
+        default_python_version = "",
+        toolchain_prefixes = [],
+        toolchain_python_versions = [],
+        toolchain_set_python_version_constraints = [],
+        toolchain_user_repository_names = [],
+        python_versions = sorted(TOOL_VERSIONS.keys()),
+    )
     http_archive(
         name = "bazel_skylib",
         sha256 = "74d544d96f4a5bb630d465ca8bbcfe231e3594e5aae57e1edbf17a6eb3ca2506",
diff --git a/python/private/common/providers.bzl b/python/private/py_runtime_info.bzl
similarity index 71%
copy from python/private/common/providers.bzl
copy to python/private/py_runtime_info.bzl
index eb8b910..359a9e7 100644
--- a/python/private/common/providers.bzl
+++ b/python/private/py_runtime_info.bzl
@@ -13,8 +13,7 @@
 # limitations under the License.
 """Providers for Python rules."""
 
-load("@rules_cc//cc:defs.bzl", "CcInfo")
-load("//python/private:util.bzl", "IS_BAZEL_6_OR_HIGHER")
+load(":util.bzl", "define_bazel_6_provider")
 
 DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3"
 
@@ -22,18 +21,6 @@
 
 _PYTHON_VERSION_VALUES = ["PY2", "PY3"]
 
-# Helper to make the provider definitions not crash under Bazel 5.4:
-# Bazel 5.4 doesn't support the `init` arg of `provider()`, so we have to
-# not pass that when using Bazel 5.4. But, not passing the `init` arg
-# changes the return value from a two-tuple to a single value, which then
-# breaks Bazel 6+ code.
-# This isn't actually used under Bazel 5.4, so just stub out the values
-# to get past the loading phase.
-def _define_provider(doc, fields, **kwargs):
-    if not IS_BAZEL_6_OR_HIGHER:
-        return provider("Stub, not used", fields = []), None
-    return provider(doc = doc, fields = fields, **kwargs)
-
 def _optional_int(value):
     return int(value) if value != None else None
 
@@ -133,9 +120,7 @@
         "zip_main_template": zip_main_template,
     }
 
-# TODO(#15897): Rename this to PyRuntimeInfo when we're ready to replace the Java
-# implemented provider with the Starlark one.
-PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = _define_provider(
+PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = define_bazel_6_provider(
     doc = """Contains information about a Python runtime, as returned by the `py_runtime`
 rule.
 
@@ -313,112 +298,3 @@
 """,
     },
 )
-
-def _check_arg_type(name, required_type, value):
-    value_type = type(value)
-    if value_type != required_type:
-        fail("parameter '{}' got value of type '{}', want '{}'".format(
-            name,
-            value_type,
-            required_type,
-        ))
-
-def _PyInfo_init(
-        *,
-        transitive_sources,
-        uses_shared_libraries = False,
-        imports = depset(),
-        has_py2_only_sources = False,
-        has_py3_only_sources = False,
-        direct_pyc_files = depset(),
-        transitive_pyc_files = depset()):
-    _check_arg_type("transitive_sources", "depset", transitive_sources)
-
-    # Verify it's postorder compatible, but retain is original ordering.
-    depset(transitive = [transitive_sources], order = "postorder")
-
-    _check_arg_type("uses_shared_libraries", "bool", uses_shared_libraries)
-    _check_arg_type("imports", "depset", imports)
-    _check_arg_type("has_py2_only_sources", "bool", has_py2_only_sources)
-    _check_arg_type("has_py3_only_sources", "bool", has_py3_only_sources)
-    _check_arg_type("direct_pyc_files", "depset", direct_pyc_files)
-    _check_arg_type("transitive_pyc_files", "depset", transitive_pyc_files)
-    return {
-        "direct_pyc_files": direct_pyc_files,
-        "has_py2_only_sources": has_py2_only_sources,
-        "has_py3_only_sources": has_py2_only_sources,
-        "imports": imports,
-        "transitive_pyc_files": transitive_pyc_files,
-        "transitive_sources": transitive_sources,
-        "uses_shared_libraries": uses_shared_libraries,
-    }
-
-PyInfo, _unused_raw_py_info_ctor = _define_provider(
-    doc = "Encapsulates information provided by the Python rules.",
-    init = _PyInfo_init,
-    fields = {
-        "direct_pyc_files": """
-:type: depset[File]
-
-Precompiled Python files that are considered directly provided
-by the target.
-""",
-        "has_py2_only_sources": """
-:type: bool
-
-Whether any of this target's transitive sources requires a Python 2 runtime.
-""",
-        "has_py3_only_sources": """
-:type: bool
-
-Whether any of this target's transitive sources requires a Python 3 runtime.
-""",
-        "imports": """\
-:type: depset[str]
-
-A depset of import path strings to be added to the `PYTHONPATH` of executable
-Python targets. These are accumulated from the transitive `deps`.
-The order of the depset is not guaranteed and may be changed in the future. It
-is recommended to use `default` order (the default).
-""",
-        "transitive_pyc_files": """
-:type: depset[File]
-
-Direct and transitive precompiled Python files that are provided by the target.
-""",
-        "transitive_sources": """\
-:type: depset[File]
-
-A (`postorder`-compatible) depset of `.py` files appearing in the target's
-`srcs` and the `srcs` of the target's transitive `deps`.
-""",
-        "uses_shared_libraries": """
-:type: bool
-
-Whether any of this target's transitive `deps` has a shared library file (such
-as a `.so` file).
-
-This field is currently unused in Bazel and may go away in the future.
-""",
-    },
-)
-
-def _PyCcLinkParamsProvider_init(cc_info):
-    return {
-        "cc_info": CcInfo(linking_context = cc_info.linking_context),
-    }
-
-# buildifier: disable=name-conventions
-PyCcLinkParamsProvider, _unused_raw_py_cc_link_params_provider_ctor = _define_provider(
-    doc = ("Python-wrapper to forward {obj}`CcInfo.linking_context`. This is to " +
-           "allow Python targets to propagate C++ linking information, but " +
-           "without the Python target appearing to be a valid C++ rule dependency"),
-    init = _PyCcLinkParamsProvider_init,
-    fields = {
-        "cc_info": """
-:type: CcInfo
-
-Linking information; it has only {obj}`CcInfo.linking_context` set.
-""",
-    },
-)
diff --git a/python/private/common/py_runtime_macro.bzl b/python/private/py_runtime_macro.bzl
similarity index 100%
rename from python/private/common/py_runtime_macro.bzl
rename to python/private/py_runtime_macro.bzl
diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl
index eb91413..b3b7a4e 100644
--- a/python/private/py_runtime_pair_rule.bzl
+++ b/python/private/py_runtime_pair_rule.bzl
@@ -16,8 +16,8 @@
 
 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")
+load(":reexports.bzl", "BuiltinPyRuntimeInfo")
+load(":util.bzl", "IS_BAZEL_7_OR_HIGHER")
 
 def _py_runtime_pair_impl(ctx):
     if ctx.attr.py2_runtime != None:
@@ -56,7 +56,7 @@
     # py_binary (implemented in Java) performs a type check on the provider
     # value to verify it is an instance of the Java-implemented PyRuntimeInfo
     # class.
-    if IS_BAZEL_7_OR_HIGHER and PyRuntimeInfo in target:
+    if (IS_BAZEL_7_OR_HIGHER and PyRuntimeInfo in target) or BuiltinPyRuntimeInfo == None:
         return target[PyRuntimeInfo]
     else:
         return target[BuiltinPyRuntimeInfo]
@@ -70,13 +70,15 @@
         return False
     return ctx.fragments.py.disable_py2
 
+_MaybeBuiltinPyRuntimeInfo = [[BuiltinPyRuntimeInfo]] if BuiltinPyRuntimeInfo != None else []
+
 py_runtime_pair = rule(
     implementation = _py_runtime_pair_impl,
     attrs = {
         # The two runtimes are used by the py_binary at runtime, and so need to
         # be built for the target platform.
         "py2_runtime": attr.label(
-            providers = [[PyRuntimeInfo], [BuiltinPyRuntimeInfo]],
+            providers = [[PyRuntimeInfo]] + _MaybeBuiltinPyRuntimeInfo,
             cfg = "target",
             doc = """\
 The runtime to use for Python 2 targets. Must have `python_version` set to
@@ -84,7 +86,7 @@
 """,
         ),
         "py3_runtime": attr.label(
-            providers = [[PyRuntimeInfo], [BuiltinPyRuntimeInfo]],
+            providers = [[PyRuntimeInfo]] + _MaybeBuiltinPyRuntimeInfo,
             cfg = "target",
             doc = """\
 The runtime to use for Python 3 targets. Must have `python_version` set to
diff --git a/python/private/py_runtime_rule.bzl b/python/private/py_runtime_rule.bzl
new file mode 100644
index 0000000..ba9b36d
--- /dev/null
+++ b/python/private/py_runtime_rule.bzl
@@ -0,0 +1,359 @@
+# Copyright 2022 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.
+"""Implementation of py_runtime rule."""
+
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
+load(":attributes.bzl", "NATIVE_RULES_ALLOWLIST_ATTRS")
+load(":py_internal.bzl", "py_internal")
+load(":py_runtime_info.bzl", "DEFAULT_BOOTSTRAP_TEMPLATE", "DEFAULT_STUB_SHEBANG", "PyRuntimeInfo")
+load(":reexports.bzl", "BuiltinPyRuntimeInfo")
+load(":util.bzl", "IS_BAZEL_7_OR_HIGHER")
+
+_py_builtins = py_internal
+
+def _py_runtime_impl(ctx):
+    interpreter_path = ctx.attr.interpreter_path or None  # Convert empty string to None
+    interpreter = ctx.attr.interpreter
+    if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
+        fail("exactly one of the 'interpreter' or 'interpreter_path' attributes must be specified")
+
+    runtime_files = depset(transitive = [
+        t[DefaultInfo].files
+        for t in ctx.attr.files
+    ])
+
+    runfiles = ctx.runfiles()
+
+    hermetic = bool(interpreter)
+    if not hermetic:
+        if runtime_files:
+            fail("if 'interpreter_path' is given then 'files' must be empty")
+        if not paths.is_absolute(interpreter_path):
+            fail("interpreter_path must be an absolute path")
+    else:
+        interpreter_di = interpreter[DefaultInfo]
+
+        if interpreter_di.files_to_run and interpreter_di.files_to_run.executable:
+            interpreter = interpreter_di.files_to_run.executable
+            runfiles = runfiles.merge(interpreter_di.default_runfiles)
+
+            runtime_files = depset(transitive = [
+                interpreter_di.files,
+                interpreter_di.default_runfiles.files,
+                runtime_files,
+            ])
+        elif _is_singleton_depset(interpreter_di.files):
+            interpreter = interpreter_di.files.to_list()[0]
+        else:
+            fail("interpreter must be an executable target or must produce exactly one file.")
+
+    if ctx.attr.coverage_tool:
+        coverage_di = ctx.attr.coverage_tool[DefaultInfo]
+
+        if _is_singleton_depset(coverage_di.files):
+            coverage_tool = coverage_di.files.to_list()[0]
+        elif coverage_di.files_to_run and coverage_di.files_to_run.executable:
+            coverage_tool = coverage_di.files_to_run.executable
+        else:
+            fail("coverage_tool must be an executable target or must produce exactly one file.")
+
+        coverage_files = depset(transitive = [
+            coverage_di.files,
+            coverage_di.default_runfiles.files,
+        ])
+    else:
+        coverage_tool = None
+        coverage_files = None
+
+    python_version = ctx.attr.python_version
+
+    interpreter_version_info = ctx.attr.interpreter_version_info
+    if not interpreter_version_info:
+        python_version_flag = ctx.attr._python_version_flag[BuildSettingInfo].value
+        if python_version_flag:
+            interpreter_version_info = _interpreter_version_info_from_version_str(python_version_flag)
+
+    # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true
+    # if ctx.fragments.py.disable_py2 and python_version == "PY2":
+    #     fail("Using Python 2 is not supported and disabled; see " +
+    #          "https://github.com/bazelbuild/bazel/issues/15684")
+
+    pyc_tag = ctx.attr.pyc_tag
+    if not pyc_tag and (ctx.attr.implementation_name and
+                        interpreter_version_info.get("major") and
+                        interpreter_version_info.get("minor")):
+        pyc_tag = "{}-{}{}".format(
+            ctx.attr.implementation_name,
+            interpreter_version_info["major"],
+            interpreter_version_info["minor"],
+        )
+
+    py_runtime_info_kwargs = dict(
+        interpreter_path = interpreter_path or None,
+        interpreter = interpreter,
+        files = runtime_files if hermetic else None,
+        coverage_tool = coverage_tool,
+        coverage_files = coverage_files,
+        python_version = python_version,
+        stub_shebang = ctx.attr.stub_shebang,
+        bootstrap_template = ctx.file.bootstrap_template,
+    )
+    builtin_py_runtime_info_kwargs = dict(py_runtime_info_kwargs)
+
+    # There are all args that BuiltinPyRuntimeInfo doesn't support
+    py_runtime_info_kwargs.update(dict(
+        implementation_name = ctx.attr.implementation_name,
+        interpreter_version_info = interpreter_version_info,
+        pyc_tag = pyc_tag,
+        stage2_bootstrap_template = ctx.file.stage2_bootstrap_template,
+        zip_main_template = ctx.file.zip_main_template,
+    ))
+
+    if not IS_BAZEL_7_OR_HIGHER:
+        builtin_py_runtime_info_kwargs.pop("bootstrap_template")
+
+    providers = [
+        PyRuntimeInfo(**py_runtime_info_kwargs),
+        DefaultInfo(
+            files = runtime_files,
+            runfiles = runfiles,
+        ),
+    ]
+    if BuiltinPyRuntimeInfo != None and BuiltinPyRuntimeInfo != PyRuntimeInfo:
+        # Return the builtin provider for better compatibility.
+        # 1. There is a legacy code path in py_binary that
+        #    checks for the provider when toolchains aren't used
+        # 2. It makes it easier to transition from builtins to rules_python
+        providers.append(BuiltinPyRuntimeInfo(**builtin_py_runtime_info_kwargs))
+    return providers
+
+# Bind to the name "py_runtime" to preserve the kind/rule_class it shows up
+# as elsewhere.
+py_runtime = rule(
+    implementation = _py_runtime_impl,
+    doc = """
+Represents a Python runtime used to execute Python code.
+
+A `py_runtime` target can represent either a *platform runtime* or an *in-build
+runtime*. A platform runtime accesses a system-installed interpreter at a known
+path, whereas an in-build runtime points to an executable target that acts as
+the interpreter. In both cases, an "interpreter" means any executable binary or
+wrapper script that is capable of running a Python script passed on the command
+line, following the same conventions as the standard CPython interpreter.
+
+A platform runtime is by its nature non-hermetic. It imposes a requirement on
+the target platform to have an interpreter located at a specific path. An
+in-build runtime may or may not be hermetic, depending on whether it points to
+a checked-in interpreter or a wrapper script that accesses the system
+interpreter.
+
+Example
+
+```
+load("@rules_python//python:py_runtime.bzl", "py_runtime")
+
+py_runtime(
+    name = "python-2.7.12",
+    files = glob(["python-2.7.12/**"]),
+    interpreter = "python-2.7.12/bin/python",
+)
+
+py_runtime(
+    name = "python-3.6.0",
+    interpreter_path = "/opt/pyenv/versions/3.6.0/bin/python",
+)
+```
+""",
+    fragments = ["py"],
+    attrs = dicts.add(NATIVE_RULES_ALLOWLIST_ATTRS, {
+        "bootstrap_template": attr.label(
+            allow_single_file = True,
+            default = DEFAULT_BOOTSTRAP_TEMPLATE,
+            doc = """
+The bootstrap script template file to use. Should have %python_binary%,
+%workspace_name%, %main%, and %imports%.
+
+This template, after expansion, becomes the executable file used to start the
+process, so it is responsible for initial bootstrapping actions such as finding
+the Python interpreter, runfiles, and constructing an environment to run the
+intended Python application.
+
+While this attribute is currently optional, it will become required when the
+Python rules are moved out of Bazel itself.
+
+The exact variable names expanded is an unstable API and is subject to change.
+The API will become more stable when the Python rules are moved out of Bazel
+itself.
+
+See @bazel_tools//tools/python:python_bootstrap_template.txt for more variables.
+""",
+        ),
+        "coverage_tool": attr.label(
+            allow_files = False,
+            doc = """
+This is a target to use for collecting code coverage information from
+{rule}`py_binary` and {rule}`py_test` targets.
+
+If set, the target must either produce a single file or be an executable target.
+The path to the single file, or the executable if the target is executable,
+determines the entry point for the python coverage tool.  The target and its
+runfiles will be added to the runfiles when coverage is enabled.
+
+The entry point for the tool must be loadable by a Python interpreter (e.g. a
+`.py` or `.pyc` file).  It must accept the command line arguments
+of [`coverage.py`](https://coverage.readthedocs.io), at least including
+the `run` and `lcov` subcommands.
+""",
+        ),
+        "files": attr.label_list(
+            allow_files = True,
+            doc = """
+For an in-build runtime, this is the set of files comprising this runtime.
+These files will be added to the runfiles of Python binaries that use this
+runtime. For a platform runtime this attribute must not be set.
+""",
+        ),
+        "implementation_name": attr.string(
+            doc = "The Python implementation name (`sys.implementation.name`)",
+            default = "cpython",
+        ),
+        "interpreter": attr.label(
+            # We set `allow_files = True` to allow specifying executable
+            # targets from rules that have more than one default output,
+            # e.g. sh_binary.
+            allow_files = True,
+            doc = """
+For an in-build runtime, this is the target to invoke as the interpreter. It
+can be either of:
+
+* A single file, which will be the interpreter binary. It's assumed such
+  interpreters are either self-contained single-file executables or any
+  supporting files are specified in `files`.
+* An executable target. The target's executable will be the interpreter binary.
+  Any other default outputs (`target.files`) and plain files runfiles
+  (`runfiles.files`) will be automatically included as if specified in the
+  `files` attribute.
+
+  NOTE: the runfiles of the target may not yet be properly respected/propagated
+  to consumers of the toolchain/interpreter, see
+  bazelbuild/rules_python/issues/1612
+
+For a platform runtime (i.e. `interpreter_path` being set) this attribute must
+not be set.
+""",
+        ),
+        "interpreter_path": attr.string(doc = """
+For a platform runtime, this is the absolute path of a Python interpreter on
+the target platform. For an in-build runtime this attribute must not be set.
+"""),
+        "interpreter_version_info": attr.string_dict(
+            doc = """
+Version information about the interpreter this runtime provides.
+
+If not specified, uses {obj}`--python_version`
+
+The supported keys match the names for `sys.version_info`. While the input
+values are strings, most are converted to ints. The supported keys are:
+  * major: int, the major version number
+  * minor: int, the minor version number
+  * micro: optional int, the micro version number
+  * releaselevel: optional str, the release level
+  * serial: optional int, the serial number of the release
+
+:::{versionchanged} 0.36.0
+{obj}`--python_version` determines the default value.
+:::
+""",
+            mandatory = False,
+        ),
+        "pyc_tag": attr.string(
+            doc = """
+Optional string; the tag portion of a pyc filename, e.g. the `cpython-39` infix
+of `foo.cpython-39.pyc`. See PEP 3147. If not specified, it will be computed
+from `implementation_name` and `interpreter_version_info`. If no pyc_tag is
+available, then only source-less pyc generation will function correctly.
+""",
+        ),
+        "python_version": attr.string(
+            default = "PY3",
+            values = ["PY2", "PY3"],
+            doc = """
+Whether this runtime is for Python major version 2 or 3. Valid values are `"PY2"`
+and `"PY3"`.
+
+The default value is controlled by the `--incompatible_py3_is_default` flag.
+However, in the future this attribute will be mandatory and have no default
+value.
+            """,
+        ),
+        "stage2_bootstrap_template": attr.label(
+            default = "//python/private:stage2_bootstrap_template",
+            allow_single_file = True,
+            doc = """
+The template to use when two stage bootstrapping is enabled
+
+:::{seealso}
+{obj}`PyRuntimeInfo.stage2_bootstrap_template` and {obj}`--bootstrap_impl`
+:::
+""",
+        ),
+        "stub_shebang": attr.string(
+            default = DEFAULT_STUB_SHEBANG,
+            doc = """
+"Shebang" expression prepended to the bootstrapping Python stub script
+used when executing {rule}`py_binary` targets.
+
+See https://github.com/bazelbuild/bazel/issues/8685 for
+motivation.
+
+Does not apply to Windows.
+""",
+        ),
+        "zip_main_template": attr.label(
+            default = "//python/private:zip_main_template",
+            allow_single_file = True,
+            doc = """
+The template to use for a zip's top-level `__main__.py` file.
+
+This becomes the entry point executed when `python foo.zip` is run.
+
+:::{seealso}
+The {obj}`PyRuntimeInfo.zip_main_template` field.
+:::
+""",
+        ),
+        "_python_version_flag": attr.label(
+            default = "//python/config_settings:python_version",
+        ),
+    }),
+)
+
+def _is_singleton_depset(files):
+    # Bazel 6 doesn't have this helper to optimize detecting singleton depsets.
+    if _py_builtins:
+        return _py_builtins.is_singleton_depset(files)
+    else:
+        return len(files.to_list()) == 1
+
+def _interpreter_version_info_from_version_str(version_str):
+    parts = version_str.split(".")
+    version_info = {}
+    for key in ("major", "minor", "micro"):
+        if not parts:
+            break
+        version_info[key] = parts.pop(0)
+
+    return version_info
diff --git a/python/private/common/py_test_macro_bazel.bzl b/python/private/py_test_macro_bazel.bzl
similarity index 100%
rename from python/private/common/py_test_macro_bazel.bzl
rename to python/private/py_test_macro_bazel.bzl
diff --git a/python/private/py_test_rule_bazel.bzl b/python/private/py_test_rule_bazel.bzl
new file mode 100644
index 0000000..369360d
--- /dev/null
+++ b/python/private/py_test_rule_bazel.bzl
@@ -0,0 +1,55 @@
+# Copyright 2022 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.
+"""Rule implementation of py_test for Bazel."""
+
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
+load(":attributes.bzl", "AGNOSTIC_TEST_ATTRS")
+load(":common.bzl", "maybe_add_test_execution_info")
+load(
+    ":py_executable_bazel.bzl",
+    "create_executable_rule",
+    "py_executable_bazel_impl",
+)
+
+_BAZEL_PY_TEST_ATTRS = {
+    # This *might* be a magic attribute to help C++ coverage work. There's no
+    # docs about this; see TestActionBuilder.java
+    "_collect_cc_coverage": attr.label(
+        default = "@bazel_tools//tools/test:collect_cc_coverage",
+        executable = True,
+        cfg = "exec",
+    ),
+    # This *might* be a magic attribute to help C++ coverage work. There's no
+    # docs about this; see TestActionBuilder.java
+    "_lcov_merger": attr.label(
+        default = configuration_field(fragment = "coverage", name = "output_generator"),
+        cfg = "exec",
+        executable = True,
+    ),
+}
+
+def _py_test_impl(ctx):
+    providers = py_executable_bazel_impl(
+        ctx = ctx,
+        is_test = True,
+        inherited_environment = ctx.attr.env_inherit,
+    )
+    maybe_add_test_execution_info(providers, ctx)
+    return providers
+
+py_test = create_executable_rule(
+    implementation = _py_test_impl,
+    attrs = dicts.add(AGNOSTIC_TEST_ATTRS, _BAZEL_PY_TEST_ATTRS),
+    test = True,
+)
diff --git a/python/private/py_toolchain_suite.bzl b/python/private/py_toolchain_suite.bzl
index 3fead95..a69be37 100644
--- a/python/private/py_toolchain_suite.bzl
+++ b/python/private/py_toolchain_suite.bzl
@@ -15,7 +15,7 @@
 """Create the toolchain defs in a BUILD.bazel file."""
 
 load("@bazel_skylib//lib:selects.bzl", "selects")
-load("//python/private:text_util.bzl", "render")
+load(":text_util.bzl", "render")
 load(
     ":toolchain_types.bzl",
     "EXEC_TOOLS_TOOLCHAIN_TYPE",
diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl
index ef9e6f2..6d047ad 100644
--- a/python/private/py_wheel.bzl
+++ b/python/private/py_wheel.bzl
@@ -14,9 +14,9 @@
 
 "Implementation of py_wheel rule"
 
-load("//python/private:stamp.bzl", "is_stamping_enabled")
 load(":py_package.bzl", "py_package_lib")
 load(":py_wheel_normalize_pep440.bzl", "normalize_pep440")
+load(":stamp.bzl", "is_stamping_enabled")
 
 PyWheelInfo = provider(
     doc = "Information about a wheel produced by `py_wheel`",
@@ -34,6 +34,10 @@
         default = "none",
         doc = "Python ABI tag. 'none' for pure-Python wheels.",
     ),
+    "compress": attr.bool(
+        default = True,
+        doc = "Enable compression of the final archive.",
+    ),
     "distribution": attr.string(
         mandatory = True,
         doc = """\
@@ -466,6 +470,9 @@
         args.add("--description_file", description_file)
         other_inputs.append(description_file)
 
+    if not ctx.attr.compress:
+        args.add("--no_compress")
+
     for target, filename in ctx.attr.extra_distinfo_files.items():
         target_files = target.files.to_list()
         if len(target_files) != 1:
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index 8cfd3d6..e76f9d3 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
-load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
 
 package(default_visibility = ["//:__subpackages__"])
 
@@ -59,9 +58,9 @@
     srcs = ["extension.bzl"],
     deps = [
         ":attrs_bzl",
+        ":evaluate_markers_bzl",
         ":hub_repository_bzl",
         ":parse_requirements_bzl",
-        ":evaluate_markers_bzl",
         ":parse_whl_name_bzl",
         ":pip_repository_attrs_bzl",
         ":simpleapi_download_bzl",
@@ -69,12 +68,11 @@
         ":whl_repo_name_bzl",
         "//python/private:full_version_bzl",
         "//python/private:normalize_name_bzl",
-        "//python/private:version_label_bzl",
         "//python/private:semver_bzl",
+        "//python/private:version_label_bzl",
         "@bazel_features//:features",
-    ] + [
         "@pythons_hub//:interpreters_bzl",
-    ] if BZLMOD_ENABLED else [],
+    ],
 )
 
 bzl_library(
diff --git a/python/private/pypi/config_settings.bzl b/python/private/pypi/config_settings.bzl
index 9741217..492acf1 100644
--- a/python/private/pypi/config_settings.bzl
+++ b/python/private/pypi/config_settings.bzl
@@ -109,9 +109,15 @@
 
     for python_version in [""] + python_versions:
         is_python = "is_python_{}".format(python_version or "version_unset")
-        native.alias(
+
+        # The aliases defined in @rules_python//python/config_settings may not
+        # have config settings for the versions we need, so define our own
+        # config settings instead.
+        native.config_setting(
             name = is_python,
-            actual = Label("//python/config_settings:" + is_python),
+            flag_values = {
+                Label("//python/config_settings:python_version_major_minor"): python_version,
+            },
             visibility = visibility,
         )
 
diff --git a/python/private/pypi/extension.bzl b/python/private/pypi/extension.bzl
index 77a4778..36fb20e 100644
--- a/python/private/pypi/extension.bzl
+++ b/python/private/pypi/extension.bzl
@@ -15,7 +15,7 @@
 "pip module extension for use with bzlmod"
 
 load("@bazel_features//:features.bzl", "bazel_features")
-load("@pythons_hub//:interpreters.bzl", "DEFAULT_PYTHON_VERSION", "INTERPRETER_LABELS")
+load("@pythons_hub//:interpreters.bzl", "INTERPRETER_LABELS")
 load("//python/private:auth.bzl", "AUTH_ATTRS")
 load("//python/private:normalize_name.bzl", "normalize_name")
 load("//python/private:repo_utils.bzl", "repo_utils")
@@ -151,9 +151,6 @@
 
     get_index_urls = None
     if pip_attr.experimental_index_url:
-        if pip_attr.download_only:
-            fail("Currently unsupported to use `download_only` and `experimental_index_url`")
-
         get_index_urls = lambda ctx, distributions: simpleapi_download(
             ctx,
             attr = struct(
@@ -182,6 +179,7 @@
             python_version = major_minor,
             logger = logger,
         ),
+        extra_pip_args = pip_attr.extra_pip_args,
         get_index_urls = get_index_urls,
         # NOTE @aignas 2024-08-02: , we will execute any interpreter that we find either
         # in the PATH or if specified as a label. We will configure the env
@@ -262,11 +260,11 @@
             is_exposed = False
             for requirement in requirements:
                 is_exposed = is_exposed or requirement.is_exposed
-                for distribution in requirement.whls + [requirement.sdist]:
-                    if not distribution:
-                        # sdist may be None
-                        continue
+                dists = requirement.whls
+                if not pip_attr.download_only and requirement.sdist:
+                    dists = dists + [requirement.sdist]
 
+                for distribution in dists:
                     found_something = True
                     is_hub_reproducible = False
 
@@ -275,8 +273,13 @@
                     if pip_attr.auth_patterns:
                         whl_library_args["auth_patterns"] = pip_attr.auth_patterns
 
-                    # pip is not used to download wheels and the python `whl_library` helpers are only extracting things
-                    whl_library_args.pop("extra_pip_args", None)
+                    if distribution.filename.endswith(".whl"):
+                        # pip is not used to download wheels and the python `whl_library` helpers are only extracting things
+                        whl_library_args.pop("extra_pip_args", None)
+                    else:
+                        # For sdists, they will be built by `pip`, so we still
+                        # need to pass the extra args there.
+                        pass
 
                     # This is no-op because pip is not used to download the wheel.
                     whl_library_args.pop("download_only", None)
@@ -500,7 +503,6 @@
                 key: json.encode(value)
                 for key, value in whl_map.items()
             },
-            default_version = _major_minor_version(DEFAULT_PYTHON_VERSION),
             packages = sorted(exposed_packages.get(hub_name, {})),
             groups = hub_group_map.get(hub_name),
         )
@@ -559,6 +561,11 @@
 stable.
 
 This is equivalent to `--index-url` `pip` option.
+
+:::{versionchanged} 0.37.0
+If {attr}`download_only` is set, then `sdist` archives will be discarded and `pip.parse` will
+operate in wheel-only mode.
+:::
 """,
         ),
         "experimental_index_url_overrides": attr.string_dict(
diff --git a/python/private/pypi/generate_whl_library_build_bazel.bzl b/python/private/pypi/generate_whl_library_build_bazel.bzl
index 0be6f9c..934fa00 100644
--- a/python/private/pypi/generate_whl_library_build_bazel.bzl
+++ b/python/private/pypi/generate_whl_library_build_bazel.bzl
@@ -162,7 +162,7 @@
 config_setting(
     name = "is_{name}",
     flag_values = {{
-        "@rules_python//python/config_settings:_python_version_major_minor": "3.{minor_version}",
+        "@rules_python//python/config_settings:python_version_major_minor": "3.{minor_version}",
     }},
     constraint_values = {constraint_values},
     visibility = ["//visibility:private"],
diff --git a/python/private/pypi/hub_repository.bzl b/python/private/pypi/hub_repository.bzl
index f589dd4..7afb616 100644
--- a/python/private/pypi/hub_repository.bzl
+++ b/python/private/pypi/hub_repository.bzl
@@ -35,8 +35,6 @@
             key: [whl_alias(**v) for v in json.decode(values)]
             for key, values in rctx.attr.whl_map.items()
         },
-        default_version = rctx.attr.default_version,
-        default_config_setting = "//_config:is_python_" + rctx.attr.default_version,
         requirement_cycles = rctx.attr.groups,
     )
     for path, contents in aliases.items():
@@ -67,13 +65,6 @@
 
 hub_repository = repository_rule(
     attrs = {
-        "default_version": attr.string(
-            mandatory = True,
-            doc = """\
-This is the default python version in the format of X.Y. This should match
-what is setup by the 'python' extension using the 'is_default = True'
-setting.""",
-        ),
         "groups": attr.string_list_dict(
             mandatory = False,
         ),
diff --git a/python/private/pypi/parse_requirements.bzl b/python/private/pypi/parse_requirements.bzl
index eee97d7..c72f5d4 100644
--- a/python/private/pypi/parse_requirements.bzl
+++ b/python/private/pypi/parse_requirements.bzl
@@ -48,7 +48,7 @@
             different package versions (or different packages) for different
             os, arch combinations.
         extra_pip_args (string list): Extra pip arguments to perform extra validations and to
-            be joined with args fined in files.
+            be joined with args found in files.
         get_index_urls: Callable[[ctx, list[str]], dict], a callable to get all
             of the distribution URLs from a PyPI index. Accepts ctx and
             distribution names to query.
diff --git a/python/private/pypi/patch_whl.bzl b/python/private/pypi/patch_whl.bzl
index c2c633d..74cd890 100644
--- a/python/private/pypi/patch_whl.bzl
+++ b/python/private/pypi/patch_whl.bzl
@@ -60,6 +60,9 @@
     if not rctx.delete(whl_file_zip):
         fail("Failed to remove the symlink after extracting")
 
+    if not patches:
+        fail("Trying to patch wheel without any patches")
+
     for patch_file, patch_strip in patches.items():
         rctx.patch(patch_file, strip = patch_strip)
 
diff --git a/python/private/pypi/pip_compile.bzl b/python/private/pypi/pip_compile.bzl
index a6cabf7..dc5b186 100644
--- a/python/private/pypi/pip_compile.bzl
+++ b/python/private/pypi/pip_compile.bzl
@@ -154,18 +154,11 @@
         "visibility": visibility,
     }
 
-    # setuptools (the default python build tool) attempts to find user
-    # configuration in the user's home direcotory. This seems to work fine on
-    # linux and macOS, but fails on Windows, so we conditionally provide a fake
-    # USERPROFILE env variable to allow setuptools to proceed without finding
-    # user-provided configuration.
-    kwargs["env"] = select({
-        "@@platforms//os:windows": {"USERPROFILE": "Z:\\FakeSetuptoolsHomeDirectoryHack"},
-        "//conditions:default": {},
-    }) | kwargs.get("env", {})
+    env = kwargs.pop("env", {})
 
     py_binary(
         name = name + ".update",
+        env = env,
         **attrs
     )
 
@@ -174,6 +167,15 @@
     py_test(
         name = name + "_test",
         timeout = timeout,
-        # kwargs could contain test-specific attributes like size or timeout
+        # setuptools (the default python build tool) attempts to find user
+        # configuration in the user's home direcotory. This seems to work fine on
+        # linux and macOS, but fails on Windows, so we conditionally provide a fake
+        # USERPROFILE env variable to allow setuptools to proceed without finding
+        # user-provided configuration.
+        env = select({
+            "@@platforms//os:windows": {"USERPROFILE": "Z:\\FakeSetuptoolsHomeDirectoryHack"},
+            "//conditions:default": {},
+        }) | env,
+        # kwargs could contain test-specific attributes like size
         **dict(attrs, **kwargs)
     )
diff --git a/python/private/pypi/render_pkg_aliases.bzl b/python/private/pypi/render_pkg_aliases.bzl
index 9e5158f..0086bff 100644
--- a/python/private/pypi/render_pkg_aliases.bzl
+++ b/python/private/pypi/render_pkg_aliases.bzl
@@ -73,7 +73,6 @@
 def _render_whl_library_alias(
         *,
         name,
-        default_config_setting,
         aliases,
         target_name,
         **kwargs):
@@ -97,9 +96,6 @@
     for alias in sorted(aliases, key = lambda x: x.version):
         actual = "@{repo}//:{name}".format(repo = alias.repo, name = target_name)
         selects.setdefault(actual, []).append(alias.config_setting)
-        if alias.config_setting == default_config_setting:
-            selects[actual].append("//conditions:default")
-            no_match_error = None
 
     return render.alias(
         name = name,
@@ -121,7 +117,7 @@
         **kwargs
     )
 
-def _render_common_aliases(*, name, aliases, default_config_setting = None, group_name = None):
+def _render_common_aliases(*, name, aliases, group_name = None):
     lines = [
         """load("@bazel_skylib//lib:selects.bzl", "selects")""",
         """package(default_visibility = ["//visibility:public"])""",
@@ -131,9 +127,7 @@
     if aliases:
         config_settings = sorted([v.config_setting for v in aliases if v.config_setting])
 
-    if not config_settings or default_config_setting in config_settings:
-        pass
-    else:
+    if config_settings:
         error_msg = NO_MATCH_ERROR_MESSAGE_TEMPLATE_V2.format(
             config_settings = render.indent(
                 "\n".join(config_settings),
@@ -145,10 +139,6 @@
             error_msg = error_msg,
         ))
 
-        # This is to simplify the code in _render_whl_library_alias and to ensure
-        # that we don't pass a 'default_version' that is not in 'versions'.
-        default_config_setting = None
-
     lines.append(
         render.alias(
             name = name,
@@ -159,7 +149,6 @@
         [
             _render_whl_library_alias(
                 name = name,
-                default_config_setting = default_config_setting,
                 aliases = aliases,
                 target_name = target_name,
                 visibility = ["//_groups:__subpackages__"] if name.startswith("_") else None,
@@ -188,7 +177,7 @@
 
     return "\n\n".join(lines)
 
-def render_pkg_aliases(*, aliases, default_config_setting = None, requirement_cycles = None):
+def render_pkg_aliases(*, aliases, requirement_cycles = None):
     """Create alias declarations for each PyPI package.
 
     The aliases should be appended to the pip_repository BUILD.bazel file. These aliases
@@ -198,7 +187,6 @@
     Args:
         aliases: dict, the keys are normalized distribution names and values are the
             whl_alias instances.
-        default_config_setting: the default to be used for the aliases.
         requirement_cycles: any package groups to also add.
 
     Returns:
@@ -227,7 +215,6 @@
         "{}/BUILD.bazel".format(normalize_name(name)): _render_common_aliases(
             name = normalize_name(name),
             aliases = pkg_aliases,
-            default_config_setting = default_config_setting,
             group_name = whl_group_mapping.get(normalize_name(name)),
         ).strip()
         for name, pkg_aliases in aliases.items()
@@ -278,13 +265,12 @@
         target_platforms = target_platforms,
     )
 
-def render_multiplatform_pkg_aliases(*, aliases, default_version = None, **kwargs):
+def render_multiplatform_pkg_aliases(*, aliases, **kwargs):
     """Render the multi-platform pkg aliases.
 
     Args:
         aliases: dict[str, list(whl_alias)] A list of aliases that will be
           transformed from ones having `filename` to ones having `config_setting`.
-        default_version: str, the default python version. Defaults to None.
         **kwargs: extra arguments passed to render_pkg_aliases.
 
     Returns:
@@ -302,7 +288,6 @@
     config_setting_aliases = {
         pkg: multiplatform_whl_aliases(
             aliases = pkg_aliases,
-            default_version = default_version,
             glibc_versions = flag_versions.get("glibc_versions", []),
             muslc_versions = flag_versions.get("muslc_versions", []),
             osx_versions = flag_versions.get("osx_versions", []),
@@ -317,14 +302,13 @@
     contents["_config/BUILD.bazel"] = _render_config_settings(**flag_versions)
     return contents
 
-def multiplatform_whl_aliases(*, aliases, default_version = None, **kwargs):
+def multiplatform_whl_aliases(*, aliases, **kwargs):
     """convert a list of aliases from filename to config_setting ones.
 
     Args:
         aliases: list(whl_alias): The aliases to process. Any aliases that have
             the filename set will be converted to a list of aliases, each with
             an appropriate config_setting value.
-        default_version: string | None, the default python version to use.
         **kwargs: Extra parameters passed to get_filename_config_settings.
 
     Returns:
@@ -344,7 +328,6 @@
             filename = alias.filename,
             target_platforms = alias.target_platforms,
             python_version = alias.version,
-            python_default = default_version == alias.version,
             **kwargs
         )
 
@@ -511,8 +494,7 @@
         glibc_versions,
         muslc_versions,
         osx_versions,
-        python_version = "",
-        python_default = True):
+        python_version):
     """Get the filename config settings.
 
     Args:
@@ -522,7 +504,6 @@
         muslc_versions: list[tuple[int, int]], list of versions.
         osx_versions: list[tuple[int, int]], list of versions.
         python_version: the python version to generate the config_settings for.
-        python_default: if we should include the setting when python_version is not set.
 
     Returns:
         A tuple:
@@ -573,18 +554,9 @@
         prefixes = ["sdist"]
         suffixes = [_non_versioned_platform(p) for p in target_platforms or []]
 
-    if python_default and python_version:
-        prefixes += ["cp{}_{}".format(python_version, p) for p in prefixes]
-    elif python_version:
-        prefixes = ["cp{}_{}".format(python_version, p) for p in prefixes]
-    elif python_default:
-        pass
-    else:
-        fail("BUG: got no python_version and it is not default")
-
     versioned = {
-        ":is_{}_{}".format(p, suffix): {
-            version: ":is_{}_{}".format(p, setting)
+        ":is_cp{}_{}_{}".format(python_version, p, suffix): {
+            version: ":is_cp{}_{}_{}".format(python_version, p, setting)
             for version, setting in versions.items()
         }
         for p in prefixes
@@ -592,9 +564,9 @@
     }
 
     if suffixes or versioned:
-        return [":is_{}_{}".format(p, s) for p in prefixes for s in suffixes], versioned
+        return [":is_cp{}_{}_{}".format(python_version, p, s) for p in prefixes for s in suffixes], versioned
     else:
-        return [":is_{}".format(p) for p in prefixes], setting_supported_versions
+        return [":is_cp{}_{}".format(python_version, p) for p in prefixes], setting_supported_versions
 
 def _whl_config_setting_suffixes(
         platform_tag,
diff --git a/python/private/pypi/repack_whl.py b/python/private/pypi/repack_whl.py
index 9052ac3..519631f 100644
--- a/python/private/pypi/repack_whl.py
+++ b/python/private/pypi/repack_whl.py
@@ -22,6 +22,7 @@
 from __future__ import annotations
 
 import argparse
+import csv
 import difflib
 import logging
 import pathlib
@@ -65,8 +66,8 @@
     # First get existing files by using the RECORD file
     got_files = []
     got_distinfos = []
-    for line in want_record.splitlines():
-        rec, _, _ = line.partition(",")
+    for row in csv.reader(want_record.splitlines()):
+        rec = row[0]
         path = dir / rec
 
         if not path.exists():
diff --git a/python/private/pypi/whl_library.bzl b/python/private/pypi/whl_library.bzl
index 309316b..82fe072 100644
--- a/python/private/pypi/whl_library.bzl
+++ b/python/private/pypi/whl_library.bzl
@@ -244,7 +244,10 @@
 
         repo_utils.execute_checked(
             rctx,
-            op = op_tmpl.format(name = rctx.attr.name, requirement = rctx.attr.requirement),
+            # truncate the requirement value when logging it / reporting
+            # progress since it may contain several ' --hash=sha256:...
+            # --hash=sha256:...' substrings that fill up the console
+            op = op_tmpl.format(name = rctx.attr.name, requirement = rctx.attr.requirement.split(" ", 1)[0]),
             arguments = args,
             environment = environment,
             quiet = rctx.attr.quiet,
@@ -263,15 +266,16 @@
             if whl_path.basename in patch_dst.whls:
                 patches[patch_file] = patch_dst.patch_strip
 
-        whl_path = patch_whl(
-            rctx,
-            op = "whl_library.PatchWhl({}, {})".format(rctx.attr.name, rctx.attr.requirement),
-            python_interpreter = python_interpreter,
-            whl_path = whl_path,
-            patches = patches,
-            quiet = rctx.attr.quiet,
-            timeout = rctx.attr.timeout,
-        )
+        if patches:
+            whl_path = patch_whl(
+                rctx,
+                op = "whl_library.PatchWhl({}, {})".format(rctx.attr.name, rctx.attr.requirement),
+                python_interpreter = python_interpreter,
+                whl_path = whl_path,
+                patches = patches,
+                quiet = rctx.attr.quiet,
+                timeout = rctx.attr.timeout,
+            )
 
     target_platforms = rctx.attr.experimental_target_platforms
     if target_platforms:
diff --git a/python/private/python.bzl b/python/private/python.bzl
index cedf39a..12ab4bb 100644
--- a/python/private/python.bzl
+++ b/python/private/python.bzl
@@ -228,7 +228,11 @@
         kwargs.update(py.config.kwargs.get(toolchain_info.python_version, {}))
         kwargs.update(py.config.kwargs.get(full_python_version, {}))
         kwargs.update(py.config.default)
-        python_register_toolchains(name = toolchain_info.name, **kwargs)
+        python_register_toolchains(
+            name = toolchain_info.name,
+            _internal_bzlmod_toolchain_call = True,
+            **kwargs
+        )
 
     # Create the pythons_hub repo for the interpreter meta data and the
     # the various toolchains.
@@ -236,6 +240,7 @@
         name = "pythons_hub",
         # Last toolchain is default
         default_python_version = py.default_python_version,
+        minor_mapping = py.config.minor_mapping,
         toolchain_prefixes = [
             render.toolchain_prefix(index, toolchain.name, _TOOLCHAIN_INDEX_PAD_LENGTH)
             for index, toolchain in enumerate(py.toolchains)
@@ -493,20 +498,23 @@
         _fail = _fail,
     )
 
-    minor_mapping = default.pop("minor_mapping", {})
     register_all_versions = default.pop("register_all_versions", False)
     kwargs = default.pop("kwargs", {})
 
-    if not minor_mapping:
-        versions = {}
-        for version_string in available_versions:
-            v = semver(version_string)
-            versions.setdefault("{}.{}".format(v.major, v.minor), []).append((int(v.patch), version_string))
+    versions = {}
+    for version_string in available_versions:
+        v = semver(version_string)
+        versions.setdefault("{}.{}".format(v.major, v.minor), []).append((int(v.patch), version_string))
 
-        minor_mapping = {
-            major_minor: max(subset)[1]
-            for major_minor, subset in versions.items()
-        }
+    minor_mapping = {
+        major_minor: max(subset)[1]
+        for major_minor, subset in versions.items()
+    }
+
+    # The following ensures that all of the versions will be present in the minor_mapping
+    minor_mapping_overrides = default.pop("minor_mapping", {})
+    for major_minor, full in minor_mapping_overrides.items():
+        minor_mapping[major_minor] = full
 
     return struct(
         kwargs = kwargs,
@@ -705,6 +713,10 @@
 "3.11": "3.11.4",
 }
 ```
+
+:::{versionchanged} 0.37.0
+The values in this mapping override the default values and do not replace them.
+:::
 """,
             default = {},
         ),
diff --git a/python/private/python_register_multi_toolchains.bzl b/python/private/python_register_multi_toolchains.bzl
index 68f5249..1c7138d 100644
--- a/python/private/python_register_multi_toolchains.bzl
+++ b/python/private/python_register_multi_toolchains.bzl
@@ -15,6 +15,10 @@
 """This file contains repository rules and macros to support toolchain registration.
 """
 
+# NOTE @aignas 2024-10-07:  we are not importing this from `@pythons_hub` because of this
+# leading to a backwards incompatible change - the `//python:repositories.bzl` is loading
+# from this file and it will cause a circular import loop and an error. If the users in
+# WORKSPACE world want to override the `minor_mapping`, they will have to pass an argument.
 load("//python:versions.bzl", "MINOR_MAPPING")
 load(":python_register_toolchains.bzl", "python_register_toolchains")
 load(":toolchains_repo.bzl", "multi_toolchain_aliases")
diff --git a/python/private/python_register_toolchains.bzl b/python/private/python_register_toolchains.bzl
index d20e049..64b66d5 100644
--- a/python/private/python_register_toolchains.bzl
+++ b/python/private/python_register_toolchains.bzl
@@ -23,7 +23,6 @@
     "TOOL_VERSIONS",
     "get_release_info",
 )
-load(":bzlmod_enabled.bzl", "BZLMOD_ENABLED")
 load(":coverage_deps.bzl", "coverage_dep")
 load(":full_version.bzl", "full_version")
 load(":python_repository.bzl", "python_repository")
@@ -75,9 +74,8 @@
             version.
         **kwargs: passed to each {obj}`python_repository` call.
     """
-
-    if BZLMOD_ENABLED:
-        # you cannot used native.register_toolchains when using bzlmod.
+    bzlmod_toolchain_call = kwargs.pop("_internal_bzlmod_toolchain_call", False)
+    if bzlmod_toolchain_call:
         register_toolchains = False
 
     base_url = kwargs.pop("base_url", DEFAULT_RELEASE_BASE_URL)
@@ -169,7 +167,7 @@
     )
 
     # in bzlmod we write out our own toolchain repos
-    if BZLMOD_ENABLED:
+    if bzlmod_toolchain_call:
         return
 
     toolchains_repo(
diff --git a/python/private/python_repository.bzl b/python/private/python_repository.bzl
index 2710299..e44bdd1 100644
--- a/python/private/python_repository.bzl
+++ b/python/private/python_repository.bzl
@@ -143,12 +143,11 @@
         # dyld lookup errors. To fix, set the full path to the dylib as
         # it appears in the Bazel workspace as its LC_ID_DYLIB using
         # the `install_name_tool` bundled with macOS.
-        dylib = "lib/libpython{}.dylib".format(python_short_version)
-        full_dylib_path = rctx.path(dylib)
+        dylib = "libpython{}.dylib".format(python_short_version)
         repo_utils.execute_checked(
             rctx,
             op = "python_repository.FixUpDyldIdPath",
-            arguments = [repo_utils.which_checked(rctx, "install_name_tool"), "-id", full_dylib_path, dylib],
+            arguments = [repo_utils.which_checked(rctx, "install_name_tool"), "-id", "@rpath/{}".format(dylib), "lib/{}".format(dylib)],
             logger = logger,
         )
 
diff --git a/python/private/pythons_hub.bzl b/python/private/pythons_hub.bzl
index da6c80d..fdaad60 100644
--- a/python/private/pythons_hub.bzl
+++ b/python/private/pythons_hub.bzl
@@ -14,10 +14,8 @@
 
 "Repo rule used by bzlmod extension to create a repo that has a map of Python interpreters and their labels"
 
-load(
-    "//python/private:toolchains_repo.bzl",
-    "python_toolchain_build_file_content",
-)
+load(":text_util.bzl", "render")
+load(":toolchains_repo.bzl", "python_toolchain_build_file_content")
 
 def _have_same_length(*lists):
     if not lists:
@@ -34,6 +32,12 @@
     visibility = ["@rules_python//:__subpackages__"],
 )
 
+bzl_library(
+    name = "versions_bzl",
+    srcs = ["versions.bzl"],
+    visibility = ["@rules_python//:__subpackages__"],
+)
+
 {toolchains}
 """
 
@@ -82,6 +86,12 @@
     "{name}_host": Label("@{name}_host//:python"),
 """
 
+_versions_bzl_template = """
+DEFAULT_PYTHON_VERSION = "{default_python_version}"
+MINOR_MAPPING = {minor_mapping}
+PYTHON_VERSIONS = {python_versions}
+"""
+
 def _hub_repo_impl(rctx):
     # Create the various toolchain definitions and
     # write them to the BUILD file.
@@ -107,8 +117,22 @@
     rctx.file(
         "interpreters.bzl",
         _interpreters_bzl_template.format(
-            interpreter_labels = interpreter_labels,
+            # TODO @aignas 2024-09-28: before 1.0 remove the value from here
             default_python_version = rctx.attr.default_python_version,
+            interpreter_labels = interpreter_labels,
+        ),
+        executable = False,
+    )
+
+    rctx.file(
+        "versions.bzl",
+        _versions_bzl_template.format(
+            default_python_version = rctx.attr.default_python_version,
+            minor_mapping = render.dict(rctx.attr.minor_mapping),
+            python_versions = rctx.attr.python_versions or render.list(sorted({
+                v: None
+                for v in rctx.attr.toolchain_python_versions
+            })),
         ),
         executable = False,
     )
@@ -125,6 +149,14 @@
             doc = "Default Python version for the build in `X.Y` or `X.Y.Z` format.",
             mandatory = True,
         ),
+        "minor_mapping": attr.string_dict(
+            doc = "The minor mapping of the `X.Y` to `X.Y.Z` format that is used in config settings.",
+            mandatory = True,
+        ),
+        "python_versions": attr.string_list(
+            doc = "The list of python versions to include in the `interpreters.bzl` if the toolchains are not specified. Used in `WORKSPACE` builds.",
+            mandatory = False,
+        ),
         "toolchain_prefixes": attr.string_list(
             doc = "List prefixed for the toolchains",
             mandatory = True,
diff --git a/python/private/reexports.bzl b/python/private/reexports.bzl
index ea39ac9..e9d2ded 100644
--- a/python/private/reexports.bzl
+++ b/python/private/reexports.bzl
@@ -30,11 +30,12 @@
 different name. Then we can load it from elsewhere.
 """
 
-# Don't use underscore prefix, since that would make the symbol local to this
-# file only. Use a non-conventional name to emphasize that this is not a public
-# symbol.
-# buildifier: disable=name-conventions
-BuiltinPyInfo = PyInfo
+load("@rules_python_internal//:rules_python_config.bzl", "config")
 
+# NOTE: May be None (Bazel 8 autoloading rules_python)
 # buildifier: disable=name-conventions
-BuiltinPyRuntimeInfo = PyRuntimeInfo
+BuiltinPyInfo = config.BuiltinPyInfo
+
+# NOTE: May be None (Bazel 8 autoloading rules_python)
+# buildifier: disable=name-conventions
+BuiltinPyRuntimeInfo = config.BuiltinPyRuntimeInfo
diff --git a/python/private/common/semantics.bzl b/python/private/semantics.bzl
similarity index 100%
rename from python/private/common/semantics.bzl
rename to python/private/semantics.bzl
diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl
index 4fae987..d21e46a 100644
--- a/python/private/toolchains_repo.bzl
+++ b/python/private/toolchains_repo.bzl
@@ -30,8 +30,8 @@
     "PLATFORMS",
     "WINDOWS_NAME",
 )
-load("//python/private:repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
-load("//python/private:text_util.bzl", "render")
+load(":repo_utils.bzl", "REPO_DEBUG_ENV_VAR", "repo_utils")
+load(":text_util.bzl", "render")
 
 def get_repository_name(repository_workspace):
     dummy_label = "//:_"
diff --git a/python/private/util.bzl b/python/private/util.bzl
index 16b8ff8..033920d 100644
--- a/python/private/util.bzl
+++ b/python/private/util.bzl
@@ -15,6 +15,7 @@
 """Functionality shared by multiple pieces of code."""
 
 load("@bazel_skylib//lib:types.bzl", "types")
+load("@rules_python_internal//:rules_python_config.bzl", "config")
 
 def copy_propagating_kwargs(from_kwargs, into_kwargs = None):
     """Copies args that must be compatible between two targets with a dependency relationship.
@@ -60,7 +61,8 @@
     Returns:
         The same `attrs` object, but modified.
     """
-    add_tag(attrs, _MIGRATION_TAG)
+    if not config.enable_pystar:
+        add_tag(attrs, _MIGRATION_TAG)
     return attrs
 
 def add_tag(attrs, tag):
@@ -84,6 +86,19 @@
     else:
         attrs["tags"] = [tag]
 
+# Helper to make the provider definitions not crash under Bazel 5.4:
+# Bazel 5.4 doesn't support the `init` arg of `provider()`, so we have to
+# not pass that when using Bazel 5.4. But, not passing the `init` arg
+# changes the return value from a two-tuple to a single value, which then
+# breaks Bazel 6+ code.
+# This isn't actually used under Bazel 5.4, so just stub out the values
+# to get past the loading phase.
+def define_bazel_6_provider(doc, fields, **kwargs):
+    """Define a provider, or a stub for pre-Bazel 7."""
+    if not IS_BAZEL_6_OR_HIGHER:
+        return provider("Stub, not used", fields = []), None
+    return provider(doc = doc, fields = fields, **kwargs)
+
 IS_BAZEL_7_OR_HIGHER = hasattr(native, "starlark_doc_extract")
 
 # Bazel 5.4 has a bug where every access of testing.ExecutionInfo is a
diff --git a/python/private/whl_filegroup/extract_wheel_files.py b/python/private/whl_filegroup/extract_wheel_files.py
index e81e6a3..5b799c9 100644
--- a/python/private/whl_filegroup/extract_wheel_files.py
+++ b/python/private/whl_filegroup/extract_wheel_files.py
@@ -1,12 +1,13 @@
 """Extract files from a wheel's RECORD."""
 
+import csv
 import re
 import sys
 import zipfile
 from collections.abc import Iterable
 from pathlib import Path
 
-WhlRecord = dict[str, tuple[str, int]]
+WhlRecord = Iterable[str]
 
 
 def get_record(whl_path: Path) -> WhlRecord:
@@ -20,18 +21,13 @@
     except ValueError:
         raise RuntimeError(f"{whl_path} doesn't contain exactly one .dist-info/RECORD")
     record_lines = zipf.read(record_file).decode().splitlines()
-    return {
-        file: (filehash, int(filelen))
-        for line in record_lines
-        for file, filehash, filelen in [line.split(",")]
-        if filehash  # Skip RECORD itself, which has no hash or length
-    }
+    return (row[0] for row in csv.reader(record_lines))
 
 
 def get_files(whl_record: WhlRecord, regex_pattern: str) -> list[str]:
     """Get files in a wheel that match a regex pattern."""
     p = re.compile(regex_pattern)
-    return [filepath for filepath in whl_record.keys() if re.match(p, filepath)]
+    return [filepath for filepath in whl_record if re.match(p, filepath)]
 
 
 def extract_files(whl_path: Path, files: Iterable[str], outdir: Path) -> None:
diff --git a/python/py_binary.bzl b/python/py_binary.bzl
index f7f68e6..3496108 100644
--- a/python/py_binary.bzl
+++ b/python/py_binary.bzl
@@ -15,9 +15,9 @@
 """Public entry point for py_binary."""
 
 load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("//python/private:py_binary_macro_bazel.bzl", _starlark_py_binary = "py_binary")
 load("//python/private:register_extension_info.bzl", "register_extension_info")
 load("//python/private:util.bzl", "add_migration_tag")
-load("//python/private/common:py_binary_macro_bazel.bzl", _starlark_py_binary = "py_binary")
 
 # buildifier: disable=native-python
 _py_binary_impl = _starlark_py_binary if config.enable_pystar else native.py_binary
@@ -26,9 +26,8 @@
     """Creates an executable Python program.
 
     This is the public macro wrapping the underlying rule. Args are forwarded
-    on as-is unless otherwise specified. See
-    the underlying {bzl:obj}`py_binary rule<//python/private/common:py_binary_rule_bazel.bzl%py_binary>`
-    for detailed attribute documentation.
+    on as-is unless otherwise specified. See the underlying {rule}`py_binary`
+    rule for detailed attribute documentation.
 
     This macro affects the following args:
     * `python_version`: cannot be `PY2`
@@ -36,8 +35,7 @@
     * `tags`: May have special marker values added, if not already present.
 
     Args:
-      **attrs: Rule attributes forwarded onto the underlying
-          {bzl:obj}`py_binary rule<//python/private/common:py_binary_rule_bazel.bzl%py_binary>`
+      **attrs: Rule attributes forwarded onto the underlying {rule}`py_binary`.
     """
     if attrs.get("python_version") == "PY2":
         fail("Python 2 is no longer supported: https://github.com/bazelbuild/rules_python/issues/886")
diff --git a/python/py_cc_link_params_info.bzl b/python/py_cc_link_params_info.bzl
index 42d8daf..02eff71 100644
--- a/python/py_cc_link_params_info.bzl
+++ b/python/py_cc_link_params_info.bzl
@@ -1,6 +1,10 @@
 """Public entry point for PyCcLinkParamsInfo."""
 
 load("@rules_python_internal//:rules_python_config.bzl", "config")
-load("//python/private/common:providers.bzl", _starlark_PyCcLinkParamsProvider = "PyCcLinkParamsProvider")
+load("//python/private:py_cc_link_params_info.bzl", _starlark_PyCcLinkParamsInfo = "PyCcLinkParamsInfo")
 
-PyCcLinkParamsInfo = _starlark_PyCcLinkParamsProvider if config.enable_pystar else PyCcLinkParamsProvider
+PyCcLinkParamsInfo = (
+    _starlark_PyCcLinkParamsInfo if (
+        config.enable_pystar or config.BuiltinPyCcLinkParamsProvider == None
+    ) else config.BuiltinPyCcLinkParamsProvider
+)
diff --git a/python/py_info.bzl b/python/py_info.bzl
index 0af35ac..5697f58 100644
--- a/python/py_info.bzl
+++ b/python/py_info.bzl
@@ -15,7 +15,7 @@
 """Public entry point for PyInfo."""
 
 load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("//python/private:py_info.bzl", _starlark_PyInfo = "PyInfo")
 load("//python/private:reexports.bzl", "BuiltinPyInfo")
-load("//python/private/common:providers.bzl", _starlark_PyInfo = "PyInfo")
 
-PyInfo = _starlark_PyInfo if config.enable_pystar else BuiltinPyInfo
+PyInfo = _starlark_PyInfo if config.enable_pystar or BuiltinPyInfo == None else BuiltinPyInfo
diff --git a/python/py_library.bzl b/python/py_library.bzl
index 3b9ddd1..4ec1da4 100644
--- a/python/py_library.bzl
+++ b/python/py_library.bzl
@@ -15,9 +15,9 @@
 """Public entry point for py_library."""
 
 load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("//python/private:py_library_macro_bazel.bzl", _starlark_py_library = "py_library")
 load("//python/private:register_extension_info.bzl", "register_extension_info")
 load("//python/private:util.bzl", "add_migration_tag")
-load("//python/private/common:py_library_macro_bazel.bzl", _starlark_py_library = "py_library")
 
 # buildifier: disable=native-python
 _py_library_impl = _starlark_py_library if config.enable_pystar else native.py_library
@@ -27,16 +27,14 @@
 
     This is the public macro wrapping the underlying rule. Args are forwarded
     on as-is unless otherwise specified. See
-    {bzl:obj}`py_library <//python/private/common:py_library_rule_bazel.bzl%py_library>`
-    for detailed attribute documentation.
+    {rule}`py_library` for detailed attribute documentation.
 
     This macro affects the following args:
     * `srcs_version`: cannot be `PY2` or `PY2ONLY`
     * `tags`: May have special marker values added, if not already present.
 
     Args:
-      **attrs: Rule attributes forwarded onto
-          {bzl:obj}`py_library <//python/private/common:py_library_rule_bazel.bzl%py_library>`
+      **attrs: Rule attributes forwarded onto {rule}`py_library`.
     """
     if attrs.get("srcs_version") in ("PY2", "PY2ONLY"):
         fail("Python 2 is no longer supported: https://github.com/bazelbuild/rules_python/issues/886")
diff --git a/python/py_runtime.bzl b/python/py_runtime.bzl
index 9c8cd00..2c44523 100644
--- a/python/py_runtime.bzl
+++ b/python/py_runtime.bzl
@@ -14,8 +14,8 @@
 
 """Public entry point for py_runtime."""
 
+load("//python/private:py_runtime_macro.bzl", _starlark_py_runtime = "py_runtime")
 load("//python/private:util.bzl", "IS_BAZEL_6_OR_HIGHER", "add_migration_tag")
-load("//python/private/common:py_runtime_macro.bzl", _starlark_py_runtime = "py_runtime")
 
 # buildifier: disable=native-python
 _py_runtime_impl = _starlark_py_runtime if IS_BAZEL_6_OR_HIGHER else native.py_runtime
@@ -25,7 +25,7 @@
 
     This is the public macro wrapping the underlying rule. Args are forwarded
     on as-is unless otherwise specified. See
-    {bzl:obj}`py_runtime <//python/private/common:py_runtime_rule.bzl%py_runtime>`
+    {rule}`py_runtime`
     for detailed attribute documentation.
 
     This macro affects the following args:
@@ -34,8 +34,7 @@
     * `tags`: May have special marker values added, if not already present.
 
     Args:
-      **attrs: Rule attributes forwarded onto
-          {bzl:obj}`py_runtime <//python/private/common:py_runtime_rule.bzl%py_runtime>`
+      **attrs: Rule attributes forwarded onto {rule}`py_runtime`.
     """
     if attrs.get("python_version") == "PY2":
         fail("Python 2 is no longer supported: see https://github.com/bazelbuild/rules_python/issues/886")
diff --git a/python/py_runtime_info.bzl b/python/py_runtime_info.bzl
index e88e0c0..3a31c0f 100644
--- a/python/py_runtime_info.bzl
+++ b/python/py_runtime_info.bzl
@@ -15,7 +15,7 @@
 """Public entry point for PyRuntimeInfo."""
 
 load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("//python/private:py_runtime_info.bzl", _starlark_PyRuntimeInfo = "PyRuntimeInfo")
 load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
-load("//python/private/common:providers.bzl", _starlark_PyRuntimeInfo = "PyRuntimeInfo")
 
 PyRuntimeInfo = _starlark_PyRuntimeInfo if config.enable_pystar else BuiltinPyRuntimeInfo
diff --git a/python/py_test.bzl b/python/py_test.bzl
index 8f93b27..2aa93ff 100644
--- a/python/py_test.bzl
+++ b/python/py_test.bzl
@@ -15,9 +15,9 @@
 """Public entry point for py_test."""
 
 load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("//python/private:py_test_macro_bazel.bzl", _starlark_py_test = "py_test")
 load("//python/private:register_extension_info.bzl", "register_extension_info")
 load("//python/private:util.bzl", "add_migration_tag")
-load("//python/private/common:py_test_macro_bazel.bzl", _starlark_py_test = "py_test")
 
 # buildifier: disable=native-python
 _py_test_impl = _starlark_py_test if config.enable_pystar else native.py_test
@@ -27,8 +27,7 @@
 
     This is the public macro wrapping the underlying rule. Args are forwarded
     on as-is unless otherwise specified. See
-    {bzl:obj}`py_test <//python/private/common:py_test_rule_bazel.bzl%py_test>`
-    for detailed attribute documentation.
+    {rule}`py_test` for detailed attribute documentation.
 
     This macro affects the following args:
     * `python_version`: cannot be `PY2`
@@ -36,8 +35,7 @@
     * `tags`: May have special marker values added, if not already present.
 
     Args:
-      **attrs: Rule attributes forwarded onto
-          {bzl:obj}`py_test <//python/private/common:py_test_rule_bazel.bzl%py_test>`
+      **attrs: Rule attributes forwarded onto {rule}`py_test`.
     """
     if attrs.get("python_version") == "PY2":
         fail("Python 2 is no longer supported: https://github.com/bazelbuild/rules_python/issues/886")
diff --git a/python/versions.bzl b/python/versions.bzl
index c97c1cc..ae017e3 100644
--- a/python/versions.bzl
+++ b/python/versions.bzl
@@ -130,6 +130,17 @@
         },
         "strip_prefix": "python",
     },
+    "3.8.20": {
+        "url": "20241002/cpython-{python_version}+20241002-{platform}-{build}.tar.gz",
+        "sha256": {
+            "aarch64-apple-darwin": "2ddfc04bdb3e240f30fb782fa1deec6323799d0e857e0b63fa299218658fd3d4",
+            "aarch64-unknown-linux-gnu": "9d8798f9e79e0fc0f36fcb95bfa28a1023407d51a8ea5944b4da711f1f75f1ed",
+            "x86_64-apple-darwin": "68d060cd373255d2ca5b8b3441363d5aa7cc45b0c11bbccf52b1717c2b5aa8bb",
+            "x86_64-pc-windows-msvc": "41b6709fec9c56419b7de1940d1f87fa62045aff81734480672dcb807eedc47e",
+            "x86_64-unknown-linux-gnu": "285e141c36f88b2e9357654c5f77d1f8fb29cc25132698fe35bb30d787f38e87",
+        },
+        "strip_prefix": "python",
+    },
     "3.9.10": {
         "url": "20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz",
         "sha256": {
@@ -225,6 +236,19 @@
         },
         "strip_prefix": "python",
     },
+    "3.9.20": {
+        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "sha256": {
+            "aarch64-apple-darwin": "dde4c3662e8b4ea336af12b94e7963d4c9b4b847e6f4a5a2921d801fbc75d55c",
+            "aarch64-unknown-linux-gnu": "adb22acc4f5417ecb6113e4beb98f1a1492bcf631b3d3094135f60d1c6794e07",
+            "ppc64le-unknown-linux-gnu": "abc12738616d3d87e878cd022c4d6a3d7cb6c130a6f3859996ce758a90c8abae",
+            "s390x-unknown-linux-gnu": "bb037b3b266524df5a27f384755b2eab397837b3c955041145434261248a731d",
+            "x86_64-apple-darwin": "980fd160c8a3e7839d808055b9497e653bd7be94dcc9cae6db0ddcb343bc5ad6",
+            "x86_64-pc-windows-msvc": "dc12754f52b7cfcdded91c10953a96ed7d9b08eff54623ee5b819cec13f4715a",
+            "x86_64-unknown-linux-gnu": "ddae7e904f5ecdff4c8993eb5256fbcec1e477923b40ec0515ffc77706dc2951",
+        },
+        "strip_prefix": "python",
+    },
     "3.10.2": {
         "url": "20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz",
         "sha256": {
@@ -331,6 +355,19 @@
         },
         "strip_prefix": "python",
     },
+    "3.10.15": {
+        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "sha256": {
+            "aarch64-apple-darwin": "6bfed646145b9f1f512bbf3c37de8a29fae3544559c501185f552c3b92dc270b",
+            "aarch64-unknown-linux-gnu": "51f08e2132dca177ac90175536118b3c01c106ec253b93db04e3ca7484525d00",
+            "ppc64le-unknown-linux-gnu": "44b05f1f831fbef00b36f5d6ef82f308e32d3dee58e1272d1fac26004ce7c76f",
+            "s390x-unknown-linux-gnu": "793bd6c565bd24b6db8e573d599492c6fddbaee43e4b4aeef240ada1105287d7",
+            "x86_64-apple-darwin": "df1324c960b9023cfebfd2716f69af57156d823a4d286d8e67ffc4f876309611",
+            "x86_64-pc-windows-msvc": "c519cb6bbb8caf508e3f3b91a3dd633b4bebdf84217ab34033a10c902b8a8519",
+            "x86_64-unknown-linux-gnu": "5e07b34c66fbd99f1e2f06d3d42aed04c0f2991e66c1d171fb43e04b7ae71ad5",
+        },
+        "strip_prefix": "python",
+    },
     "3.11.1": {
         "url": "20230116/cpython-{python_version}+20230116-{platform}-{build}.tar.gz",
         "sha256": {
@@ -432,6 +469,19 @@
         },
         "strip_prefix": "python",
     },
+    "3.11.10": {
+        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "sha256": {
+            "aarch64-apple-darwin": "ecdc9c042b8f97bff211fcf9425bc51c96acd4037df1565964e89816f2c9564d",
+            "aarch64-unknown-linux-gnu": "320635e957e13d2e10d70a3031563d032fae9e40e60e5ec32bc353643fae1335",
+            "ppc64le-unknown-linux-gnu": "7eed40dc5751046e2164b1a3f08f177c2c965064f1e3b0f84c00f3f715d099ca",
+            "s390x-unknown-linux-gnu": "eb86c655159d6f7b5fb245d9017f23aa388b5423f21caefeaee54469446ef9f2",
+            "x86_64-apple-darwin": "a618c086e0514f681523947e2b66a4dc0c6560f91c36faa072fa6787455df9ea",
+            "x86_64-pc-windows-msvc": "2cab4d2ee0c9313923c9b11297e23b1876ecb79ce6ad6de0b8b48baf8519ab67",
+            "x86_64-unknown-linux-gnu": "ff121f14ed113c9da83a45f76c3cf41976fb4419fe406d5cc7066765761c6a4e",
+        },
+        "strip_prefix": "python",
+    },
     "3.12.0": {
         "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz",
         "sha256": {
@@ -497,15 +547,42 @@
         },
         "strip_prefix": "python",
     },
+    "3.12.7": {
+        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "sha256": {
+            "aarch64-apple-darwin": "dd07d467f1d533b93d06e4d2ff88b91f491329510c6434297b88b584641bff5d",
+            "aarch64-unknown-linux-gnu": "ce3230da53aacb17ff77e912170786f47db4a446d4acb6cde7c397953a032bca",
+            "ppc64le-unknown-linux-gnu": "27d3cba42e94593c49f8610dcadd74f5b731c78f04ebabc2b0e1ba031ec09441",
+            "s390x-unknown-linux-gnu": "1e28e0fc9cd1fa0365a149c715c44d3030b2c989ca397fc074809b943449df41",
+            "x86_64-apple-darwin": "2347bf53ed3623645bed35adfca950b2c5291e3a759ec6c7765aa707b5dc866b",
+            "x86_64-pc-windows-msvc": "4ed1a146c66c7dbd85b87df69b17afc166ea7d70056aaf59a49c3d987a030d3b",
+            "x86_64-unknown-linux-gnu": "adbda1f3b77d7b65a551206e34a225375f408f9823e2e11df4c332aaecb8714b",
+        },
+        "strip_prefix": "python",
+    },
+    "3.13.0": {
+        "url": "20241008/cpython-{python_version}+20241008-{platform}-{build}.tar.gz",
+        "sha256": {
+            "aarch64-apple-darwin": "5d3cb8d7ca4cfbbe7ae1f118f26be112ee417d982fab8c6d85cfd8ccccf70718",
+            "aarch64-unknown-linux-gnu": "c1142af8f2c85923d2ba8201a35b913bb903a5d15f052c38bbecf2f49e2342dc",
+            "ppc64le-unknown-linux-gnu": "1be64a330499fed4e1f864b97eef5445b0e4abc0559ae45df3108981800cf998",
+            "s390x-unknown-linux-gnu": "c0b1cc51426feadaa932fdd9afd9a9af789916e128e48ac8909f9a269bbbd749",
+            "x86_64-apple-darwin": "b58ca12d9ae14bbd79f9e5cf4b748211ff1953e59abeac63b0f4e8e49845669f",
+            "x86_64-pc-windows-msvc": "c7651a7a575104f47c808902b020168057f3ad80f277e54cecfaf79a9ff50e22",
+            "x86_64-unknown-linux-gnu": "455200e1a202e9d9ef4b630c04af701c0a91dcaa6462022efc76893fc762ec95",
+        },
+        "strip_prefix": "python",
+    },
 }
 
 # buildifier: disable=unsorted-dict-items
 MINOR_MAPPING = {
-    "3.8": "3.8.19",
-    "3.9": "3.9.19",
-    "3.10": "3.10.14",
-    "3.11": "3.11.9",
-    "3.12": "3.12.4",
+    "3.8": "3.8.20",
+    "3.9": "3.9.20",
+    "3.10": "3.10.15",
+    "3.11": "3.11.10",
+    "3.12": "3.12.7",
+    "3.13": "3.13.0",
 }
 
 PLATFORMS = {
diff --git a/sphinxdocs/docs/sphinx-bzl.md b/sphinxdocs/docs/sphinx-bzl.md
index 73ae138..8376f60 100644
--- a/sphinxdocs/docs/sphinx-bzl.md
+++ b/sphinxdocs/docs/sphinx-bzl.md
@@ -227,6 +227,11 @@
 MyST notation.
 :::
 
+Directives can be nested, but [the inner directives must have **fewer** colons
+than outer
+directives](https://myst-parser.readthedocs.io/en/latest/syntax/roles-and-directives.html#nesting-directives).
+
+
 :::{rst:directive} .. bzl:currentfile:: file
 
 This directive indicates the Bazel file that objects defined in the current
@@ -237,21 +242,87 @@
 :::
 
 
-:::{rst:directive} .. bzl:target:: target
+:::::{rst:directive} .. bzl:target:: target
 
 Documents a target. It takes no directive options. The format of `target`
 can either be a fully qualified label (`//foo:bar`), or the base target name
 relative to `{bzl:currentfile}`.
 
-```
+````
 :::{bzl:target} //foo:target
 
 My docs
 :::
-```
+````
+
+:::::
 
 :::{rst:directive} .. bzl:flag:: target
 
 Documents a flag. It has the same format as `{bzl:target}`
 :::
 
+::::::{rst:directive} .. bzl:typedef:: typename
+
+Documents a user-defined structural "type".  These are typically generated by
+the {obj}`sphinx_stardoc` rule after following [User-defined types] to create a
+struct with a `TYPEDEF` field, but can also be manually defined if there's
+no natural place for it in code, e.g. some ad-hoc structural type.
+
+`````
+::::{bzl:typedef} Square
+Doc about Square
+
+:::{bzl:field} width
+:type: int
+:::
+
+:::{bzl:function} new(size)
+  ...
+:::
+
+:::{bzl:function} area()
+  ...
+:::
+::::
+`````
+
+Note that MyST requires the number of colons for the outer typedef directive
+to be greater than the inner directives. Otherwise, only the first nested
+directive is parsed as part of the typedef, but subsequent ones are not.
+::::::
+
+:::::{rst:directive} .. bzl:field:: fieldname
+
+Documents a field of an object. These are nested within some other directive,
+typically `{bzl:typedef}`
+
+Directive options:
+* `:type:` specifies the type of the field
+
+````
+:::{bzl:field} fieldname
+:type: int | None | str
+
+Doc about field
+:::
+````
+:::::
+
+:::::{rst:directive} .. bzl:provider-field:: fieldname
+
+Documents a field of a provider. The directive itself is autogenerated by
+`sphinx_stardoc`, but the content is simply the documentation string specified
+in the provider's field.
+
+Directive options:
+* `:type:` specifies the type of the field
+
+````
+:::{bzl:provider-field} fieldname
+:type: depset[File] | None
+
+Doc about the provider field
+:::
+````
+:::::
diff --git a/sphinxdocs/docs/starlark-docgen.md b/sphinxdocs/docs/starlark-docgen.md
index d131607..ba4ab51 100644
--- a/sphinxdocs/docs/starlark-docgen.md
+++ b/sphinxdocs/docs/starlark-docgen.md
@@ -73,3 +73,90 @@
    deps = ...
 )
 ```
+
+## User-defined types
+
+While Starlark doesn't have user-defined types as a first-class concept, it's
+still possible to create such objects using `struct` and lambdas. For the
+purposes of documentation, they can be documented by creating a module-level
+`struct` with matching fields *and* also a field named `TYPEDEF`. When the
+`sphinx_stardoc` rule sees a struct with a `TYPEDEF` field, it generates doc
+using the {rst:directive}`bzl:typedef` directive and puts all the struct's fields
+within the typedef. The net result is the rendered docs look similar to how
+a class would be documented in other programming languages.
+
+For example, a the Starlark implemenation of a `Square` object with a `area()`
+method would look like:
+
+```
+
+def _Square_typedef():
+    """A square with fixed size.
+
+    :::{field} width
+    :type: int
+    :::
+    """
+
+def _Square_new(width):
+    """Creates a Square.
+
+    Args:
+        width: {type}`int` width of square
+
+    Returns:
+        {type}`Square`
+    """
+    self = struct(
+        area = lambda *a, **k: _Square_area(self, *a, **k),
+        width = width
+    )
+    return self
+
+def _Square_area(self, ):
+   """Tells the area of the square."""
+   return self.width * self.width
+
+Square = struct(
+  TYPEDEF = _Square_typedef,
+  new = _Square_new,
+  area = _Square_area,
+)
+```
+
+This will then genereate markdown that looks like:
+
+```
+::::{bzl:typedef} Square
+A square with fixed size
+
+:::{bzl:field} width
+:type: int
+:::
+:::{bzl:function} new()
+...args etc from _Square_new...
+:::
+:::{bzl:function} area()
+...args etc from _Square_area...
+:::
+::::
+```
+
+Which renders as:
+
+:::{bzl:currentfile} //example:square.bzl
+:::
+
+::::{bzl:typedef} Square
+A square with fixed size
+
+:::{bzl:field} width
+:type: int
+:::
+:::{bzl:function} new()
+...
+:::
+:::{bzl:function} area()
+...
+:::
+::::
diff --git a/sphinxdocs/private/proto_to_markdown.py b/sphinxdocs/private/proto_to_markdown.py
index d667eec..18fbd12 100644
--- a/sphinxdocs/private/proto_to_markdown.py
+++ b/sphinxdocs/private/proto_to_markdown.py
@@ -96,6 +96,15 @@
         self._module = module
         self._out_stream = out_stream
         self._public_load_path = public_load_path
+        self._typedef_stack = []
+
+    def _get_colons(self):
+        # There's a weird behavior where increasing colon indents doesn't
+        # parse as nested objects correctly, so we have to reduce the
+        # number of colons based on the indent level
+        indent = 10 - len(self._typedef_stack)
+        assert indent >= 0
+        return ":::" + ":" * indent
 
     def render(self):
         self._render_module(self._module)
@@ -115,11 +124,10 @@
             "\n\n",
         )
 
-        # Sort the objects by name
         objects = itertools.chain(
             ((r.rule_name, r, self._render_rule) for r in module.rule_info),
             ((p.provider_name, p, self._render_provider) for p in module.provider_info),
-            ((f.function_name, f, self._render_func) for f in module.func_info),
+            ((f.function_name, f, self._process_func_info) for f in module.func_info),
             ((a.aspect_name, a, self._render_aspect) for a in module.aspect_info),
             (
                 (m.extension_name, m, self._render_module_extension)
@@ -130,13 +138,31 @@
                 for r in module.repository_rule_info
             ),
         )
+        # Sort by name, ignoring case. The `.TYPEDEF` string is removed so
+        # that the .TYPEDEF entries come before what is in the typedef.
+        objects = sorted(objects, key=lambda v: v[0].removesuffix(".TYPEDEF").lower())
 
-        objects = sorted(objects, key=lambda v: v[0].lower())
-
-        for _, obj, func in objects:
-            func(obj)
+        for name, obj, func in objects:
+            self._process_object(name, obj, func)
             self._write("\n")
 
+        # Close any typedefs
+        while self._typedef_stack:
+            self._typedef_stack.pop()
+            self._render_typedef_end()
+
+    def _process_object(self, name, obj, renderer):
+        # The trailing doc is added to prevent matching a common prefix
+        typedef_group = name.removesuffix(".TYPEDEF") + "."
+        while self._typedef_stack and not typedef_group.startswith(
+            self._typedef_stack[-1]
+        ):
+            self._typedef_stack.pop()
+            self._render_typedef_end()
+        renderer(obj)
+        if name.endswith(".TYPEDEF"):
+            self._typedef_stack.append(typedef_group)
+
     def _render_aspect(self, aspect: stardoc_output_pb2.AspectInfo):
         _sort_attributes_inplace(aspect.attribute)
         self._write("::::::{bzl:aspect} ", aspect.aspect_name, "\n\n")
@@ -156,7 +182,7 @@
         for tag in mod_ext.tag_class:
             tag_name = f"{mod_ext.extension_name}.{tag.tag_name}"
             tag_name = f"{tag.tag_name}"
-            self._write(":::::{bzl:tag-class} ", tag_name, "\n\n")
+            self._write(":::::{bzl:tag-class} ")
 
             _sort_attributes_inplace(tag.attribute)
             self._render_signature(
@@ -166,7 +192,12 @@
                 get_default=lambda a: a.default_value,
             )
 
-            self._write(tag.doc_string.strip(), "\n\n")
+            if doc_string := tag.doc_string.strip():
+                self._write(doc_string, "\n\n")
+            # Ensure a newline between the directive and the doc fields,
+            # otherwise they get parsed as directive options instead.
+            if not doc_string and tag.attribute:
+                self.write("\n")
             self._render_attributes(tag.attribute)
             self._write(":::::\n")
         self._write("::::::\n")
@@ -242,14 +273,39 @@
             # Rather than error, give some somewhat understandable value.
             return _AttributeType.Name(attr.type)
 
+    def _process_func_info(self, func):
+        if func.function_name.endswith(".TYPEDEF"):
+            self._render_typedef_start(func)
+        else:
+            self._render_func(func)
+
+    def _render_typedef_start(self, func):
+        self._write(
+            self._get_colons(),
+            "{bzl:typedef} ",
+            func.function_name.removesuffix(".TYPEDEF"),
+            "\n",
+        )
+        if func.doc_string:
+            self._write(func.doc_string.strip(), "\n")
+
+    def _render_typedef_end(self):
+        self._write(self._get_colons(), "\n\n")
+
     def _render_func(self, func: stardoc_output_pb2.StarlarkFunctionInfo):
-        self._write("::::::{bzl:function} ")
+        self._write(self._get_colons(), "{bzl:function} ")
 
         parameters = self._render_func_signature(func)
 
-        self._write(func.doc_string.strip(), "\n\n")
+        doc_string = func.doc_string.strip()
+        if doc_string:
+            self._write(doc_string, "\n\n")
 
         if parameters:
+            # Ensure a newline between the directive and the doc fields,
+            # otherwise they get parsed as directive options instead.
+            if not doc_string:
+                self._write("\n")
             for param in parameters:
                 self._write(f":arg {param.name}:\n")
                 if param.default_value:
@@ -268,10 +324,13 @@
             self._write(":::::{deprecated}: unknown\n")
             self._write("  ", _indent_block_text(func.deprecated.doc_string), "\n")
             self._write(":::::\n")
-        self._write("::::::\n")
+        self._write(self._get_colons(), "\n")
 
     def _render_func_signature(self, func):
-        self._write(f"{func.function_name}(")
+        func_name = func.function_name
+        if self._typedef_stack:
+            func_name = func.function_name.removeprefix(self._typedef_stack[-1])
+        self._write(f"{func_name}(")
         # TODO: Have an "is method" directive in the docstring to decide if
         # the self parameter should be removed.
         parameters = [param for param in func.parameter if param.name != "self"]
diff --git a/sphinxdocs/src/sphinx_bzl/bzl.py b/sphinxdocs/src/sphinx_bzl/bzl.py
index 54b1285..90fb109 100644
--- a/sphinxdocs/src/sphinx_bzl/bzl.py
+++ b/sphinxdocs/src/sphinx_bzl/bzl.py
@@ -424,7 +424,7 @@
         return [wrapper]
 
 
-class _BzlField(_BzlXrefField, docfields.Field):
+class _BzlDocField(_BzlXrefField, docfields.Field):
     """A non-repeated field with xref support."""
 
 
@@ -623,6 +623,7 @@
         relative_name = relative_name.strip()
 
         name_prefix, _, base_symbol_name = relative_name.rpartition(".")
+
         if name_prefix:
             # Respect whatever the signature wanted
             display_prefix = name_prefix
@@ -819,6 +820,28 @@
     """Abstract base class for objects that are callable."""
 
 
+class _BzlTypedef(_BzlObject):
+    """Documents a typedef.
+
+    A typedef describes objects with well known attributes.
+
+    `````
+    ::::{bzl:typedef} Square
+
+    :::{bzl:field} width
+    :type: int
+    :::
+
+    :::{bzl:function} new(size)
+    :::
+
+    :::{bzl:function} area()
+    :::
+    ::::
+    `````
+    """
+
+
 class _BzlProvider(_BzlObject):
     """Documents a provider type.
 
@@ -837,7 +860,7 @@
     """
 
 
-class _BzlProviderField(_BzlObject):
+class _BzlField(_BzlObject):
     """Documents a field of a provider.
 
     Fields can optionally have a type specified using the `:type:` option.
@@ -872,6 +895,10 @@
         return alt_names
 
 
+class _BzlProviderField(_BzlField):
+    pass
+
+
 class _BzlRepositoryRule(_BzlCallable):
     """Documents a repository rule.
 
@@ -951,7 +978,7 @@
             rolename="attr",
             can_collapse=False,
         ),
-        _BzlField(
+        _BzlDocField(
             "provides",
             label="Provides",
             has_arg=False,
@@ -1078,13 +1105,13 @@
     """
 
     doc_field_types = [
-        _BzlField(
+        _BzlDocField(
             "os-dependent",
             label="OS Dependent",
             has_arg=False,
             names=["os-dependent"],
         ),
-        _BzlField(
+        _BzlDocField(
             "arch-dependent",
             label="Arch Dependent",
             has_arg=False,
@@ -1448,7 +1475,8 @@
         # Providers are close enough to types that we include "type". This
         # also makes :type: Foo work in directive options.
         "provider": domains.ObjType("provider", "provider", "type", "obj"),
-        "provider-field": domains.ObjType("provider field", "field", "obj"),
+        "provider-field": domains.ObjType("provider field", "provider-field", "obj"),
+        "field": domains.ObjType("field", "field", "obj"),
         "repo-rule": domains.ObjType("repository rule", "repo_rule", "obj"),
         "rule": domains.ObjType("rule", "rule", "obj"),
         "tag-class": domains.ObjType("tag class", "tag_class", "obj"),
@@ -1457,6 +1485,7 @@
         "flag": domains.ObjType("flag", "flag", "target", "obj"),
         # types are objects that have a constructor and methods/attrs
         "type": domains.ObjType("type", "type", "obj"),
+        "typedef": domains.ObjType("typedef", "typedef", "type", "obj"),
     }
 
     # This controls:
@@ -1483,7 +1512,9 @@
         "function": _BzlFunction,
         "module-extension": _BzlModuleExtension,
         "provider": _BzlProvider,
+        "typedef": _BzlTypedef,
         "provider-field": _BzlProviderField,
+        "field": _BzlField,
         "repo-rule": _BzlRepositoryRule,
         "rule": _BzlRule,
         "tag-class": _BzlTagClass,
diff --git a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py
index 3b664a5..66e3224 100644
--- a/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py
+++ b/sphinxdocs/tests/proto_to_markdown/proto_to_markdown_test.py
@@ -193,6 +193,78 @@
         self.assertIn('{default-value}`"@repo//pkg:file.bzl"`', actual)
         self.assertIn("{default-value}`'<function foo from //bar:baz.bzl>'", actual)
 
+    def test_render_typedefs(self):
+        proto_text = """
+file: "@repo//pkg:foo.bzl"
+func_info: { function_name: "Zeta.TYPEDEF" }
+func_info: { function_name: "Carl.TYPEDEF" }
+func_info: { function_name: "Carl.ns.Alpha.TYPEDEF" }
+func_info: { function_name: "Beta.TYPEDEF" }
+func_info: { function_name: "Beta.Sub.TYPEDEF" }
+"""
+        actual = self._render(proto_text)
+        self.assertIn("\n:::::::::::::{bzl:typedef} Beta\n", actual)
+        self.assertIn("\n::::::::::::{bzl:typedef} Beta.Sub\n", actual)
+        self.assertIn("\n:::::::::::::{bzl:typedef} Carl\n", actual)
+        self.assertIn("\n::::::::::::{bzl:typedef} Carl.ns.Alpha\n", actual)
+        self.assertIn("\n:::::::::::::{bzl:typedef} Zeta\n", actual)
+
+    def test_render_func_no_doc_with_args(self):
+        proto_text = """
+file: "@repo//pkg:foo.bzl"
+func_info: {
+  function_name: "func"
+  parameter: {
+    name: "param"
+    doc_string: "param_doc"
+  }
+}
+"""
+        actual = self._render(proto_text)
+        expected = """
+:::::::::::::{bzl:function} func(*param)
+
+:arg param:
+  param_doc
+
+:::::::::::::
+"""
+        self.assertIn(expected, actual)
+
+    def test_render_module_extension(self):
+        proto_text = """
+file: "@repo//pkg:foo.bzl"
+module_extension_info: {
+  extension_name: "bzlmod_ext"
+  tag_class: {
+    tag_name: "bzlmod_ext_tag_a"
+    doc_string: "BZLMOD_EXT_TAG_A_DOC_STRING"
+    attribute: {
+      name: "attr1",
+      doc_string: "attr1doc"
+      type: STRING_LIST
+    }
+  }
+}
+"""
+        actual = self._render(proto_text)
+        expected = """
+:::::{bzl:tag-class} bzlmod_ext_tag_a(attr1)
+
+BZLMOD_EXT_TAG_A_DOC_STRING
+
+:attr attr1:
+  {type}`list[str]`
+  attr1doc
+  :::{bzl:attr-info} Info
+  :::
+
+
+:::::
+::::::
+"""
+        self.assertIn(expected, actual)
+
 
 if __name__ == "__main__":
     absltest.main()
diff --git a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
index 3741e41..60a5e8d 100644
--- a/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
+++ b/sphinxdocs/tests/sphinx_stardoc/BUILD.bazel
@@ -42,7 +42,10 @@
 
 sphinx_stardocs(
     name = "simple_bzl_docs",
-    srcs = [":bzl_rule_bzl"],
+    srcs = [
+        ":bzl_rule_bzl",
+        ":bzl_typedef_bzl",
+    ],
     target_compatible_with = _TARGET_COMPATIBLE_WITH,
 )
 
@@ -76,6 +79,11 @@
     deps = [":func_and_providers_bzl"],
 )
 
+bzl_library(
+    name = "bzl_typedef_bzl",
+    srcs = ["bzl_typedef.bzl"],
+)
+
 sphinx_build_binary(
     name = "sphinx-build",
     tags = ["manual"],  # Only needed as part of sphinx doc building
diff --git a/sphinxdocs/tests/sphinx_stardoc/bzl_typedef.bzl b/sphinxdocs/tests/sphinx_stardoc/bzl_typedef.bzl
new file mode 100644
index 0000000..5afd0bf
--- /dev/null
+++ b/sphinxdocs/tests/sphinx_stardoc/bzl_typedef.bzl
@@ -0,0 +1,46 @@
+"""Module doc for bzl_typedef."""
+
+def _Square_typedef():
+    """Represents a square
+
+    :::{field} width
+    :type: int
+    The length of the sides
+    :::
+
+    """
+
+def _Square_new(width):
+    """Creates a square.
+
+    Args:
+        width: {type}`int` the side size
+
+    Returns:
+        {type}`Square`
+    """
+
+    # buildifier: disable=uninitialized
+    self = struct(
+        area = lambda *a, **k: _Square_area(self, *a, **k),
+        width = width,
+    )
+    return self
+
+def _Square_area(self):
+    """Tells the area
+
+    Args:
+        self: implicitly added
+
+    Returns:
+        {type}`int`
+    """
+    return self.width * self.width
+
+# buildifier: disable=name-conventions
+Square = struct(
+    TYPEDEF = _Square_typedef,
+    new = _Square_new,
+    area = _Square_area,
+)
diff --git a/sphinxdocs/tests/sphinx_stardoc/typedef.md b/sphinxdocs/tests/sphinx_stardoc/typedef.md
new file mode 100644
index 0000000..08c4aa2
--- /dev/null
+++ b/sphinxdocs/tests/sphinx_stardoc/typedef.md
@@ -0,0 +1,32 @@
+:::{default-domain} bzl
+:::
+
+:::{bzl:currentfile} //lang:typedef.bzl
+:::
+
+
+# Typedef
+
+below is a provider
+
+:::::::::{bzl:typedef} MyType
+
+my type doc
+
+:::{bzl:function} method(a, b)
+
+:arg a:
+  {type}`depset[str]`
+  arg a doc
+:arg b: ami2 doc
+  {type}`None | depset[File]`
+  arg b doc
+:::
+
+:::{bzl:field} field
+:type: str
+
+field doc
+:::
+
+:::::::::
diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel
index e7dbef6..0fb8e88 100644
--- a/tests/BUILD.bazel
+++ b/tests/BUILD.bazel
@@ -1,4 +1,6 @@
 load("@bazel_skylib//rules:build_test.bzl", "build_test")
+load("@rules_shell//shell:sh_test.bzl", "sh_test")
+load("//:version.bzl", "BAZEL_VERSION")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -25,3 +27,29 @@
         "//python/entry_points:py_console_script_binary_bzl",
     ],
 )
+
+genrule(
+    name = "assert_bazelversion",
+    srcs = ["//:.bazelversion"],
+    outs = ["assert_bazelversion_test.sh"],
+    cmd = """\
+set -o errexit -o nounset -o pipefail
+current=$$(cat "$(execpath //:.bazelversion)")
+cat > "$@" <<EOF
+#!/usr/bin/env bash
+set -o errexit -o nounset -o pipefail
+if [[ \"$${{current}}\" != \"{expected}\" ]]; then
+    >&2 echo "ERROR: current bazel version '$${{current}}' is not the expected '{expected}'"
+    exit 1
+fi
+EOF
+""".format(
+        expected = BAZEL_VERSION,
+    ),
+    executable = True,
+)
+
+sh_test(
+    name = "assert_bazelversion_test",
+    srcs = [":assert_bazelversion_test.sh"],
+)
diff --git a/tests/api/py_common/BUILD.bazel b/tests/api/py_common/BUILD.bazel
new file mode 100644
index 0000000..0930037
--- /dev/null
+++ b/tests/api/py_common/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(":py_common_tests.bzl", "py_common_test_suite")
+
+py_common_test_suite(name = "py_common_tests")
diff --git a/tests/api/py_common/py_common_tests.bzl b/tests/api/py_common/py_common_tests.bzl
new file mode 100644
index 0000000..572028b
--- /dev/null
+++ b/tests/api/py_common/py_common_tests.bzl
@@ -0,0 +1,68 @@
+# 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.
+"""py_common tests."""
+
+load("@rules_python_internal//:rules_python_config.bzl", "config")
+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/api:api.bzl", _py_common = "py_common")
+load("//tests/support:py_info_subject.bzl", "py_info_subject")
+
+_tests = []
+
+def _test_merge_py_infos(name):
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_subject",
+        srcs = ["f1.py", "f1.pyc", "f2.py", "f2.pyc"],
+    )
+    analysis_test(
+        name = name,
+        impl = _test_merge_py_infos_impl,
+        target = name + "_subject",
+        attrs = _py_common.API_ATTRS,
+    )
+
+def _test_merge_py_infos_impl(env, target):
+    f1_py, f1_pyc, f2_py, f2_pyc = target[DefaultInfo].files.to_list()
+
+    py_common = _py_common.get(env.ctx)
+
+    py1 = py_common.PyInfoBuilder()
+    if config.enable_pystar:
+        py1.direct_pyc_files.add(f1_pyc)
+    py1.transitive_sources.add(f1_py)
+
+    py2 = py_common.PyInfoBuilder()
+    if config.enable_pystar:
+        py1.direct_pyc_files.add(f2_pyc)
+    py2.transitive_sources.add(f2_py)
+
+    actual = py_info_subject(
+        py_common.merge_py_infos([py2.build()], direct = [py1.build()]),
+        meta = env.expect.meta,
+    )
+
+    actual.transitive_sources().contains_exactly([f1_py.path, f2_py.path])
+    if config.enable_pystar:
+        actual.direct_pyc_files().contains_exactly([f1_pyc.path, f2_pyc.path])
+
+_tests.append(_test_merge_py_infos)
+
+def py_common_test_suite(name):
+    test_suite(
+        name = name,
+        tests = _tests,
+    )
diff --git a/tests/base_rules/precompile/precompile_tests.bzl b/tests/base_rules/precompile/precompile_tests.bzl
index 4c0f936..895f2d3 100644
--- a/tests/base_rules/precompile/precompile_tests.bzl
+++ b/tests/base_rules/precompile/precompile_tests.bzl
@@ -26,11 +26,10 @@
 load("//tests/support:py_info_subject.bzl", "py_info_subject")
 load(
     "//tests/support:support.bzl",
+    "ADD_SRCS_TO_RUNFILES",
     "CC_TOOLCHAIN",
     "EXEC_TOOLS_TOOLCHAIN",
     "PRECOMPILE",
-    "PRECOMPILE_ADD_TO_RUNFILES",
-    "PRECOMPILE_SOURCE_RETENTION",
     "PY_TOOLCHAINS",
 )
 
@@ -44,7 +43,7 @@
 
 _tests = []
 
-def _test_precompile_enabled_setup(name, py_rule, **kwargs):
+def _test_executable_precompile_attr_enabled_setup(name, py_rule, **kwargs):
     if not rp_config.enable_pystar:
         rt_util.skip_test(name = name)
         return
@@ -53,31 +52,43 @@
         name = name + "_subject",
         precompile = "enabled",
         srcs = ["main.py"],
-        deps = [name + "_lib"],
+        deps = [name + "_lib1"],
         **kwargs
     )
     rt_util.helper_target(
         py_library,
-        name = name + "_lib",
-        srcs = ["lib.py"],
+        name = name + "_lib1",
+        srcs = ["lib1.py"],
+        precompile = "enabled",
+        deps = [name + "_lib2"],
+    )
+
+    # 2nd order target to verify propagation
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib2",
+        srcs = ["lib2.py"],
         precompile = "enabled",
     )
     analysis_test(
         name = name,
-        impl = _test_precompile_enabled_impl,
+        impl = _test_executable_precompile_attr_enabled_impl,
         target = name + "_subject",
         config_settings = _COMMON_CONFIG_SETTINGS,
     )
 
-def _test_precompile_enabled_impl(env, target):
+def _test_executable_precompile_attr_enabled_impl(env, target):
     target = env.expect.that_target(target)
     runfiles = target.runfiles()
-    runfiles.contains_predicate(
+    runfiles_contains_at_least_predicates(runfiles, [
         matching.str_matches("__pycache__/main.fakepy-45.pyc"),
-    )
-    runfiles.contains_predicate(
+        matching.str_matches("__pycache__/lib1.fakepy-45.pyc"),
+        matching.str_matches("__pycache__/lib2.fakepy-45.pyc"),
         matching.str_matches("/main.py"),
-    )
+        matching.str_matches("/lib1.py"),
+        matching.str_matches("/lib2.py"),
+    ])
+
     target.default_outputs().contains_at_least_predicates([
         matching.file_path_matches("__pycache__/main.fakepy-45.pyc"),
         matching.file_path_matches("/main.py"),
@@ -88,23 +99,85 @@
     ])
     py_info.transitive_pyc_files().contains_exactly([
         "{package}/__pycache__/main.fakepy-45.pyc",
-        "{package}/__pycache__/lib.fakepy-45.pyc",
+        "{package}/__pycache__/lib1.fakepy-45.pyc",
+        "{package}/__pycache__/lib2.fakepy-45.pyc",
     ])
 
 def _test_precompile_enabled_py_binary(name):
-    _test_precompile_enabled_setup(name = name, py_rule = py_binary, main = "main.py")
+    _test_executable_precompile_attr_enabled_setup(name = name, py_rule = py_binary, main = "main.py")
 
 _tests.append(_test_precompile_enabled_py_binary)
 
 def _test_precompile_enabled_py_test(name):
-    _test_precompile_enabled_setup(name = name, py_rule = py_test, main = "main.py")
+    _test_executable_precompile_attr_enabled_setup(name = name, py_rule = py_test, main = "main.py")
 
 _tests.append(_test_precompile_enabled_py_test)
 
-def _test_precompile_enabled_py_library(name):
-    _test_precompile_enabled_setup(name = name, py_rule = py_library)
+def _test_precompile_enabled_py_library_setup(name, impl, config_settings):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    rt_util.helper_target(
+        py_library,
+        name = name + "_subject",
+        srcs = ["lib.py"],
+        precompile = "enabled",
+    )
+    analysis_test(
+        name = name,
+        impl = impl,  #_test_precompile_enabled_py_library_impl,
+        target = name + "_subject",
+        config_settings = _COMMON_CONFIG_SETTINGS | config_settings,
+    )
 
-_tests.append(_test_precompile_enabled_py_library)
+def _test_precompile_enabled_py_library_common_impl(env, target):
+    target = env.expect.that_target(target)
+
+    target.default_outputs().contains_at_least_predicates([
+        matching.file_path_matches("__pycache__/lib.fakepy-45.pyc"),
+        matching.file_path_matches("/lib.py"),
+    ])
+    py_info = target.provider(PyInfo, factory = py_info_subject)
+    py_info.direct_pyc_files().contains_exactly([
+        "{package}/__pycache__/lib.fakepy-45.pyc",
+    ])
+    py_info.transitive_pyc_files().contains_exactly([
+        "{package}/__pycache__/lib.fakepy-45.pyc",
+    ])
+
+def _test_precompile_enabled_py_library_add_to_runfiles_disabled(name):
+    _test_precompile_enabled_py_library_setup(
+        name = name,
+        impl = _test_precompile_enabled_py_library_add_to_runfiles_disabled_impl,
+        config_settings = {
+            ADD_SRCS_TO_RUNFILES: "disabled",
+        },
+    )
+
+def _test_precompile_enabled_py_library_add_to_runfiles_disabled_impl(env, target):
+    _test_precompile_enabled_py_library_common_impl(env, target)
+    runfiles = env.expect.that_target(target).runfiles()
+    runfiles.contains_exactly([])
+
+_tests.append(_test_precompile_enabled_py_library_add_to_runfiles_disabled)
+
+def _test_precompile_enabled_py_library_add_to_runfiles_enabled(name):
+    _test_precompile_enabled_py_library_setup(
+        name = name,
+        impl = _test_precompile_enabled_py_library_add_to_runfiles_enabled_impl,
+        config_settings = {
+            ADD_SRCS_TO_RUNFILES: "enabled",
+        },
+    )
+
+def _test_precompile_enabled_py_library_add_to_runfiles_enabled_impl(env, target):
+    _test_precompile_enabled_py_library_common_impl(env, target)
+    runfiles = env.expect.that_target(target).runfiles()
+    runfiles.contains_exactly([
+        "{workspace}/{package}/lib.py",
+    ])
+
+_tests.append(_test_precompile_enabled_py_library_add_to_runfiles_enabled)
 
 def _test_pyc_only(name):
     if not rp_config.enable_pystar:
@@ -117,12 +190,19 @@
         srcs = ["main.py"],
         main = "main.py",
         precompile_source_retention = "omit_source",
+        pyc_collection = "include_pyc",
+        deps = [name + "_lib"],
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib",
+        srcs = ["lib.py"],
+        precompile_source_retention = "omit_source",
     )
     analysis_test(
         name = name,
         impl = _test_pyc_only_impl,
         config_settings = _COMMON_CONFIG_SETTINGS | {
-            ##PRECOMPILE_SOURCE_RETENTION: "omit_source",
             PRECOMPILE: "enabled",
         },
         target = name + "_subject",
@@ -136,9 +216,15 @@
     runfiles.contains_predicate(
         matching.str_matches("/main.pyc"),
     )
+    runfiles.contains_predicate(
+        matching.str_matches("/lib.pyc"),
+    )
     runfiles.not_contains_predicate(
         matching.str_endswith("/main.py"),
     )
+    runfiles.not_contains_predicate(
+        matching.str_endswith("/lib.py"),
+    )
     target.default_outputs().contains_at_least_predicates([
         matching.file_path_matches("/main.pyc"),
     ])
@@ -146,130 +232,6 @@
         matching.file_basename_equals("main.py"),
     )
 
-def _test_precompile_if_generated(name):
-    if not rp_config.enable_pystar:
-        rt_util.skip_test(name = name)
-        return
-    rt_util.helper_target(
-        py_binary,
-        name = name + "_subject",
-        srcs = [
-            "main.py",
-            rt_util.empty_file("generated1.py"),
-        ],
-        main = "main.py",
-        precompile = "if_generated_source",
-    )
-    analysis_test(
-        name = name,
-        impl = _test_precompile_if_generated_impl,
-        target = name + "_subject",
-        config_settings = _COMMON_CONFIG_SETTINGS,
-    )
-
-_tests.append(_test_precompile_if_generated)
-
-def _test_precompile_if_generated_impl(env, target):
-    target = env.expect.that_target(target)
-    runfiles = target.runfiles()
-    runfiles.contains_predicate(
-        matching.str_matches("/__pycache__/generated1.fakepy-45.pyc"),
-    )
-    runfiles.not_contains_predicate(
-        matching.str_matches("main.*pyc"),
-    )
-    target.default_outputs().contains_at_least_predicates([
-        matching.file_path_matches("/__pycache__/generated1.fakepy-45.pyc"),
-    ])
-    target.default_outputs().not_contains_predicate(
-        matching.file_path_matches("main.*pyc"),
-    )
-
-def _test_omit_source_if_generated_source(name):
-    if not rp_config.enable_pystar:
-        rt_util.skip_test(name = name)
-        return
-    rt_util.helper_target(
-        py_binary,
-        name = name + "_subject",
-        srcs = [
-            "main.py",
-            rt_util.empty_file("generated2.py"),
-        ],
-        main = "main.py",
-        precompile = "enabled",
-    )
-    analysis_test(
-        name = name,
-        impl = _test_omit_source_if_generated_source_impl,
-        target = name + "_subject",
-        config_settings = _COMMON_CONFIG_SETTINGS | {
-            PRECOMPILE_SOURCE_RETENTION: "omit_if_generated_source",
-        },
-    )
-
-_tests.append(_test_omit_source_if_generated_source)
-
-def _test_omit_source_if_generated_source_impl(env, target):
-    target = env.expect.that_target(target)
-    runfiles = target.runfiles()
-    runfiles.contains_predicate(
-        matching.str_matches("/generated2.pyc"),
-    )
-    runfiles.contains_predicate(
-        matching.str_matches("__pycache__/main.fakepy-45.pyc"),
-    )
-    target.default_outputs().contains_at_least_predicates([
-        matching.file_path_matches("generated2.pyc"),
-    ])
-    target.default_outputs().contains_predicate(
-        matching.file_path_matches("__pycache__/main.fakepy-45.pyc"),
-    )
-
-def _test_precompile_add_to_runfiles_decided_elsewhere(name):
-    if not rp_config.enable_pystar:
-        rt_util.skip_test(name = name)
-        return
-    rt_util.helper_target(
-        py_binary,
-        name = name + "_binary",
-        srcs = ["bin.py"],
-        main = "bin.py",
-        deps = [name + "_lib"],
-        pyc_collection = "include_pyc",
-    )
-    rt_util.helper_target(
-        py_library,
-        name = name + "_lib",
-        srcs = ["lib.py"],
-    )
-    analysis_test(
-        name = name,
-        impl = _test_precompile_add_to_runfiles_decided_elsewhere_impl,
-        targets = {
-            "binary": name + "_binary",
-            "library": name + "_lib",
-        },
-        config_settings = _COMMON_CONFIG_SETTINGS | {
-            PRECOMPILE_ADD_TO_RUNFILES: "decided_elsewhere",
-            PRECOMPILE: "enabled",
-        },
-    )
-
-_tests.append(_test_precompile_add_to_runfiles_decided_elsewhere)
-
-def _test_precompile_add_to_runfiles_decided_elsewhere_impl(env, targets):
-    env.expect.that_target(targets.binary).runfiles().contains_at_least([
-        "{workspace}/{package}/__pycache__/bin.fakepy-45.pyc",
-        "{workspace}/{package}/__pycache__/lib.fakepy-45.pyc",
-        "{workspace}/{package}/bin.py",
-        "{workspace}/{package}/lib.py",
-    ])
-
-    env.expect.that_target(targets.library).runfiles().contains_exactly([
-        "{workspace}/{package}/lib.py",
-    ])
-
 def _test_precompiler_action(name):
     if not rp_config.enable_pystar:
         rt_util.skip_test(name = name)
@@ -306,6 +268,288 @@
         "PYTHONSAFEPATH": "1",
     })
 
+def _setup_precompile_flag_pyc_collection_attr_interaction(
+        *,
+        name,
+        pyc_collection_attr,
+        precompile_flag,
+        test_impl):
+    rt_util.helper_target(
+        py_binary,
+        name = name + "_bin",
+        srcs = ["bin.py"],
+        main = "bin.py",
+        precompile = "disabled",
+        pyc_collection = pyc_collection_attr,
+        deps = [
+            name + "_lib_inherit",
+            name + "_lib_enabled",
+            name + "_lib_disabled",
+        ],
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib_inherit",
+        srcs = ["lib_inherit.py"],
+        precompile = "inherit",
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib_enabled",
+        srcs = ["lib_enabled.py"],
+        precompile = "enabled",
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib_disabled",
+        srcs = ["lib_disabled.py"],
+        precompile = "disabled",
+    )
+    analysis_test(
+        name = name,
+        impl = test_impl,
+        target = name + "_bin",
+        config_settings = _COMMON_CONFIG_SETTINGS | {
+            PRECOMPILE: precompile_flag,
+        },
+    )
+
+def _verify_runfiles(contains_patterns, not_contains_patterns):
+    def _verify_runfiles_impl(env, target):
+        runfiles = env.expect.that_target(target).runfiles()
+        for pattern in contains_patterns:
+            runfiles.contains_predicate(matching.str_matches(pattern))
+        for pattern in not_contains_patterns:
+            runfiles.not_contains_predicate(
+                matching.str_matches(pattern),
+            )
+
+    return _verify_runfiles_impl
+
+def _test_precompile_flag_enabled_pyc_collection_attr_include_pyc(name):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    _setup_precompile_flag_pyc_collection_attr_interaction(
+        name = name,
+        precompile_flag = "enabled",
+        pyc_collection_attr = "include_pyc",
+        test_impl = _verify_runfiles(
+            contains_patterns = [
+                "__pycache__/lib_enabled.*.pyc",
+                "__pycache__/lib_inherit.*.pyc",
+            ],
+            not_contains_patterns = [
+                "/bin*.pyc",
+                "/lib_disabled*.pyc",
+            ],
+        ),
+    )
+
+_tests.append(_test_precompile_flag_enabled_pyc_collection_attr_include_pyc)
+
+# buildifier: disable=function-docstring-header
+def _test_precompile_flag_enabled_pyc_collection_attr_disabled(name):
+    """Verify that a binary can opt-out of using implicit pycs even when
+    precompiling is enabled by default.
+    """
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    _setup_precompile_flag_pyc_collection_attr_interaction(
+        name = name,
+        precompile_flag = "enabled",
+        pyc_collection_attr = "disabled",
+        test_impl = _verify_runfiles(
+            contains_patterns = [
+                "__pycache__/lib_enabled.*.pyc",
+            ],
+            not_contains_patterns = [
+                "/bin*.pyc",
+                "/lib_disabled*.pyc",
+                "/lib_inherit.*.pyc",
+            ],
+        ),
+    )
+
+_tests.append(_test_precompile_flag_enabled_pyc_collection_attr_disabled)
+
+# buildifier: disable=function-docstring-header
+def _test_precompile_flag_disabled_pyc_collection_attr_include_pyc(name):
+    """Verify that a binary can opt-in to using pycs even when precompiling is
+    disabled by default."""
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    _setup_precompile_flag_pyc_collection_attr_interaction(
+        name = name,
+        precompile_flag = "disabled",
+        pyc_collection_attr = "include_pyc",
+        test_impl = _verify_runfiles(
+            contains_patterns = [
+                "__pycache__/lib_enabled.*.pyc",
+                "__pycache__/lib_inherit.*.pyc",
+            ],
+            not_contains_patterns = [
+                "/bin*.pyc",
+                "/lib_disabled*.pyc",
+            ],
+        ),
+    )
+
+_tests.append(_test_precompile_flag_disabled_pyc_collection_attr_include_pyc)
+
+def _test_precompile_flag_disabled_pyc_collection_attr_disabled(name):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    _setup_precompile_flag_pyc_collection_attr_interaction(
+        name = name,
+        precompile_flag = "disabled",
+        pyc_collection_attr = "disabled",
+        test_impl = _verify_runfiles(
+            contains_patterns = [
+                "__pycache__/lib_enabled.*.pyc",
+            ],
+            not_contains_patterns = [
+                "/bin*.pyc",
+                "/lib_disabled*.pyc",
+                "/lib_inherit.*.pyc",
+            ],
+        ),
+    )
+
+_tests.append(_test_precompile_flag_disabled_pyc_collection_attr_disabled)
+
+# buildifier: disable=function-docstring-header
+def _test_pyc_collection_disabled_library_omit_source(name):
+    """Verify that, when a binary doesn't include implicit pyc files, libraries
+    that set omit_source still have the py source file included.
+    """
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    rt_util.helper_target(
+        py_binary,
+        name = name + "_subject",
+        srcs = ["bin.py"],
+        main = "bin.py",
+        deps = [name + "_lib"],
+        pyc_collection = "disabled",
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib",
+        srcs = ["lib.py"],
+        precompile = "inherit",
+        precompile_source_retention = "omit_source",
+    )
+    analysis_test(
+        name = name,
+        impl = _test_pyc_collection_disabled_library_omit_source_impl,
+        target = name + "_subject",
+        config_settings = _COMMON_CONFIG_SETTINGS,
+    )
+
+def _test_pyc_collection_disabled_library_omit_source_impl(env, target):
+    contains_patterns = [
+        "/lib.py",
+        "/bin.py",
+    ]
+    not_contains_patterns = [
+        "/lib.*pyc",
+        "/bin.*pyc",
+    ]
+    runfiles = env.expect.that_target(target).runfiles()
+    for pattern in contains_patterns:
+        runfiles.contains_predicate(matching.str_matches(pattern))
+    for pattern in not_contains_patterns:
+        runfiles.not_contains_predicate(
+            matching.str_matches(pattern),
+        )
+
+_tests.append(_test_pyc_collection_disabled_library_omit_source)
+
+def _test_pyc_collection_include_dep_omit_source(name):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    rt_util.helper_target(
+        py_binary,
+        name = name + "_subject",
+        srcs = ["bin.py"],
+        main = "bin.py",
+        deps = [name + "_lib"],
+        precompile = "disabled",
+        pyc_collection = "include_pyc",
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib",
+        srcs = ["lib.py"],
+        precompile = "inherit",
+        precompile_source_retention = "omit_source",
+    )
+    analysis_test(
+        name = name,
+        impl = _test_pyc_collection_include_dep_omit_source_impl,
+        target = name + "_subject",
+        config_settings = _COMMON_CONFIG_SETTINGS,
+    )
+
+def _test_pyc_collection_include_dep_omit_source_impl(env, target):
+    contains_patterns = [
+        "/lib.pyc",
+    ]
+    not_contains_patterns = [
+        "/lib.py",
+    ]
+    runfiles = env.expect.that_target(target).runfiles()
+    for pattern in contains_patterns:
+        runfiles.contains_predicate(matching.str_endswith(pattern))
+    for pattern in not_contains_patterns:
+        runfiles.not_contains_predicate(
+            matching.str_endswith(pattern),
+        )
+
+_tests.append(_test_pyc_collection_include_dep_omit_source)
+
+def _test_precompile_attr_inherit_pyc_collection_disabled_precompile_flag_enabled(name):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    rt_util.helper_target(
+        py_binary,
+        name = name + "_subject",
+        srcs = ["bin.py"],
+        main = "bin.py",
+        precompile = "inherit",
+        pyc_collection = "disabled",
+    )
+    analysis_test(
+        name = name,
+        impl = _test_precompile_attr_inherit_pyc_collection_disabled_precompile_flag_enabled_impl,
+        target = name + "_subject",
+        config_settings = _COMMON_CONFIG_SETTINGS | {
+            PRECOMPILE: "enabled",
+        },
+    )
+
+def _test_precompile_attr_inherit_pyc_collection_disabled_precompile_flag_enabled_impl(env, target):
+    target = env.expect.that_target(target)
+    target.runfiles().not_contains_predicate(
+        matching.str_matches("/bin.*pyc"),
+    )
+    target.default_outputs().not_contains_predicate(
+        matching.file_path_matches("/bin.*pyc"),
+    )
+
+_tests.append(_test_precompile_attr_inherit_pyc_collection_disabled_precompile_flag_enabled)
+
+def runfiles_contains_at_least_predicates(runfiles, predicates):
+    for predicate in predicates:
+        runfiles.contains_predicate(predicate)
+
 def precompile_test_suite(name):
     test_suite(
         name = name,
diff --git a/tests/base_rules/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl
index 873349f..3cc6dfb 100644
--- a/tests/base_rules/py_executable_base_tests.bzl
+++ b/tests/base_rules/py_executable_base_tests.bzl
@@ -19,14 +19,13 @@
 load("@rules_testing//lib:truth.bzl", "matching")
 load("@rules_testing//lib:util.bzl", rt_util = "util")
 load("//python:py_executable_info.bzl", "PyExecutableInfo")
+load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")  # buildifier: disable=bzl-visibility
 load("//python/private:util.bzl", "IS_BAZEL_7_OR_HIGHER")  # buildifier: disable=bzl-visibility
 load("//tests/base_rules:base_tests.bzl", "create_base_tests")
 load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util")
 load("//tests/support:py_executable_info_subject.bzl", "PyExecutableInfoSubject")
 load("//tests/support:support.bzl", "CC_TOOLCHAIN", "CROSSTOOL_TOP", "LINUX_X86_64", "WINDOWS_X86_64")
 
-_BuiltinPyRuntimeInfo = PyRuntimeInfo
-
 _tests = []
 
 def _test_basic_windows(name, config):
@@ -359,9 +358,10 @@
     # Make sure that the rules_python loaded symbol is provided.
     env.expect.that_target(target).has_provider(RulesPythonPyRuntimeInfo)
 
-    # For compatibility during the transition, the builtin PyRuntimeInfo should
-    # also be provided.
-    env.expect.that_target(target).has_provider(_BuiltinPyRuntimeInfo)
+    if BuiltinPyRuntimeInfo != None:
+        # For compatibility during the transition, the builtin PyRuntimeInfo should
+        # also be provided.
+        env.expect.that_target(target).has_provider(BuiltinPyRuntimeInfo)
 
 _tests.append(_test_py_runtime_info_provided)
 
diff --git a/tests/base_rules/py_info/BUILD.bazel b/tests/base_rules/py_info/BUILD.bazel
new file mode 100644
index 0000000..69f0bda
--- /dev/null
+++ b/tests/base_rules/py_info/BUILD.bazel
@@ -0,0 +1,23 @@
+# 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(":py_info_tests.bzl", "py_info_test_suite")
+
+filegroup(
+    name = "some_runfiles",
+    data = ["runfile1.txt"],
+    tags = ["manual"],
+)
+
+py_info_test_suite(name = "py_info_tests")
diff --git a/tests/base_rules/py_info/py_info_tests.bzl b/tests/base_rules/py_info/py_info_tests.bzl
new file mode 100644
index 0000000..0f46d12
--- /dev/null
+++ b/tests/base_rules/py_info/py_info_tests.bzl
@@ -0,0 +1,211 @@
+# 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.
+"""Tests for py_info."""
+
+load("@rules_python_internal//:rules_python_config.bzl", "config")
+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:py_info.bzl", "PyInfo")
+load("//python/private:py_info.bzl", "PyInfoBuilder")  # buildifier: disable=bzl-visibility
+load("//python/private:reexports.bzl", "BuiltinPyInfo")  # buildifier: disable=bzl-visibility
+load("//tests/support:py_info_subject.bzl", "py_info_subject")
+
+def _provide_py_info_impl(ctx):
+    kwargs = {
+        "direct_pyc_files": depset(ctx.files.direct_pyc_files),
+        "imports": depset(ctx.attr.imports),
+        "transitive_pyc_files": depset(ctx.files.transitive_pyc_files),
+        "transitive_sources": depset(ctx.files.transitive_sources),
+    }
+    if ctx.attr.has_py2_only_sources != -1:
+        kwargs["has_py2_only_sources"] = bool(ctx.attr.has_py2_only_sources)
+    if ctx.attr.has_py3_only_sources != -1:
+        kwargs["has_py2_only_sources"] = bool(ctx.attr.has_py2_only_sources)
+
+    providers = []
+    if config.enable_pystar:
+        providers.append(PyInfo(**kwargs))
+
+    # Handle Bazel 6 or if Bazel autoloading is enabled
+    if not config.enable_pystar or PyInfo != BuiltinPyInfo:
+        providers.append(BuiltinPyInfo(**{
+            k: kwargs[k]
+            for k in (
+                "transitive_sources",
+                "has_py2_only_sources",
+                "has_py3_only_sources",
+                "uses_shared_libraries",
+                "imports",
+            )
+            if k in kwargs
+        }))
+    return providers
+
+provide_py_info = rule(
+    implementation = _provide_py_info_impl,
+    attrs = {
+        "direct_pyc_files": attr.label_list(allow_files = True),
+        "has_py2_only_sources": attr.int(default = -1),
+        "has_py3_only_sources": attr.int(default = -1),
+        "imports": attr.string_list(),
+        "transitive_pyc_files": attr.label_list(allow_files = True),
+        "transitive_sources": attr.label_list(allow_files = True),
+    },
+)
+
+_tests = []
+
+def _test_py_info_create(name):
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_files",
+        srcs = ["trans.py", "direct.pyc", "trans.pyc"],
+    )
+    analysis_test(
+        name = name,
+        target = name + "_files",
+        impl = _test_py_info_create_impl,
+    )
+
+def _test_py_info_create_impl(env, target):
+    trans_py, direct_pyc, trans_pyc = target[DefaultInfo].files.to_list()
+    actual = PyInfo(
+        has_py2_only_sources = True,
+        has_py3_only_sources = True,
+        imports = depset(["import-path"]),
+        transitive_sources = depset([trans_py]),
+        uses_shared_libraries = True,
+        **(dict(
+            direct_pyc_files = depset([direct_pyc]),
+            transitive_pyc_files = depset([trans_pyc]),
+        ) if config.enable_pystar else {})
+    )
+
+    subject = py_info_subject(actual, meta = env.expect.meta)
+    subject.uses_shared_libraries().equals(True)
+    subject.has_py2_only_sources().equals(True)
+    subject.has_py3_only_sources().equals(True)
+    subject.transitive_sources().contains_exactly(["tests/base_rules/py_info/trans.py"])
+    subject.imports().contains_exactly(["import-path"])
+    if config.enable_pystar:
+        subject.direct_pyc_files().contains_exactly(["tests/base_rules/py_info/direct.pyc"])
+        subject.transitive_pyc_files().contains_exactly(["tests/base_rules/py_info/trans.pyc"])
+
+_tests.append(_test_py_info_create)
+
+def _test_py_info_builder(name):
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_misc",
+        srcs = ["trans.py", "direct.pyc", "trans.pyc"],
+    )
+
+    py_info_targets = {}
+    for n in range(1, 7):
+        py_info_name = "{}_py{}".format(name, n)
+        py_info_targets["py{}".format(n)] = py_info_name
+        rt_util.helper_target(
+            provide_py_info,
+            name = py_info_name,
+            transitive_sources = ["py{}-trans.py".format(n)],
+            direct_pyc_files = ["py{}-direct.pyc".format(n)],
+            imports = ["py{}import".format(n)],
+            transitive_pyc_files = ["py{}-trans.pyc".format(n)],
+        )
+    analysis_test(
+        name = name,
+        impl = _test_py_info_builder_impl,
+        targets = {
+            "misc": name + "_misc",
+        } | py_info_targets,
+    )
+
+def _test_py_info_builder_impl(env, targets):
+    trans, direct_pyc, trans_pyc = targets.misc[DefaultInfo].files.to_list()
+    builder = PyInfoBuilder()
+    builder.direct_pyc_files.add(direct_pyc)
+    builder.merge_has_py2_only_sources(True)
+    builder.merge_has_py3_only_sources(True)
+    builder.imports.add("import-path")
+    builder.transitive_pyc_files.add(trans_pyc)
+    builder.transitive_sources.add(trans)
+    builder.merge_uses_shared_libraries(True)
+
+    builder.merge_target(targets.py1)
+    builder.merge_targets([targets.py2])
+
+    builder.merge(targets.py3[PyInfo], direct = [targets.py4[PyInfo]])
+    builder.merge_all([targets.py5[PyInfo]], direct = [targets.py6[PyInfo]])
+
+    def check(actual):
+        subject = py_info_subject(actual, meta = env.expect.meta)
+
+        subject.uses_shared_libraries().equals(True)
+        subject.has_py2_only_sources().equals(True)
+        subject.has_py3_only_sources().equals(True)
+
+        subject.transitive_sources().contains_exactly([
+            "tests/base_rules/py_info/trans.py",
+            "tests/base_rules/py_info/py1-trans.py",
+            "tests/base_rules/py_info/py2-trans.py",
+            "tests/base_rules/py_info/py3-trans.py",
+            "tests/base_rules/py_info/py4-trans.py",
+            "tests/base_rules/py_info/py5-trans.py",
+            "tests/base_rules/py_info/py6-trans.py",
+        ])
+        subject.imports().contains_exactly([
+            "import-path",
+            "py1import",
+            "py2import",
+            "py3import",
+            "py4import",
+            "py5import",
+            "py6import",
+        ])
+        if hasattr(actual, "direct_pyc_files"):
+            subject.direct_pyc_files().contains_exactly([
+                "tests/base_rules/py_info/direct.pyc",
+                "tests/base_rules/py_info/py4-direct.pyc",
+                "tests/base_rules/py_info/py6-direct.pyc",
+            ])
+            subject.transitive_pyc_files().contains_exactly([
+                "tests/base_rules/py_info/trans.pyc",
+                "tests/base_rules/py_info/py1-trans.pyc",
+                "tests/base_rules/py_info/py2-trans.pyc",
+                "tests/base_rules/py_info/py3-trans.pyc",
+                "tests/base_rules/py_info/py4-trans.pyc",
+                "tests/base_rules/py_info/py5-trans.pyc",
+                "tests/base_rules/py_info/py6-trans.pyc",
+            ])
+
+    check(builder.build())
+    if BuiltinPyInfo != None:
+        check(builder.build_builtin_py_info())
+
+    builder.set_has_py2_only_sources(False)
+    builder.set_has_py3_only_sources(False)
+    builder.set_uses_shared_libraries(False)
+
+    env.expect.that_bool(builder.get_has_py2_only_sources()).equals(False)
+    env.expect.that_bool(builder.get_has_py3_only_sources()).equals(False)
+    env.expect.that_bool(builder.get_uses_shared_libraries()).equals(False)
+
+_tests.append(_test_py_info_builder)
+
+def py_info_test_suite(name):
+    test_suite(
+        name = name,
+        tests = _tests,
+    )
diff --git a/tests/builders/BUILD.bazel b/tests/builders/BUILD.bazel
new file mode 100644
index 0000000..3ad0c3e
--- /dev/null
+++ b/tests/builders/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(":builders_tests.bzl", "builders_test_suite")
+
+builders_test_suite(name = "builders_test_suite")
diff --git a/tests/builders/builders_tests.bzl b/tests/builders/builders_tests.bzl
new file mode 100644
index 0000000..f1d596e
--- /dev/null
+++ b/tests/builders/builders_tests.bzl
@@ -0,0 +1,116 @@
+# 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.
+"""Tests for py_info."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load("@rules_testing//lib:truth.bzl", "subjects")
+load("@rules_testing//lib:util.bzl", rt_util = "util")
+load("//python/private:builders.bzl", "builders")  # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _test_depset_builder(name):
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_files",
+    )
+    analysis_test(
+        name = name,
+        target = name + "_files",
+        impl = _test_depset_builder_impl,
+    )
+
+def _test_depset_builder_impl(env, target):
+    _ = target  # @unused
+    builder = builders.DepsetBuilder()
+    builder.set_order("preorder")
+    builder.add("one")
+    builder.add(["two"])
+    builder.add(depset(["three"]))
+    builder.add([depset(["four"])])
+
+    env.expect.that_str(builder.get_order()).equals("preorder")
+
+    actual = builder.build()
+
+    env.expect.that_collection(actual).contains_exactly([
+        "one",
+        "two",
+        "three",
+        "four",
+    ]).in_order()
+
+_tests.append(_test_depset_builder)
+
+def _test_runfiles_builder(name):
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_files",
+        srcs = ["f1.txt", "f2.txt", "f3.txt", "f4.txt", "f5.txt"],
+    )
+    rt_util.helper_target(
+        native.filegroup,
+        name = name + "_runfiles",
+        data = ["runfile.txt"],
+    )
+    analysis_test(
+        name = name,
+        impl = _test_runfiles_builder_impl,
+        targets = {
+            "files": name + "_files",
+            "runfiles": name + "_runfiles",
+        },
+    )
+
+def _test_runfiles_builder_impl(env, targets):
+    ctx = env.ctx
+
+    f1, f2, f3, f4, f5 = targets.files[DefaultInfo].files.to_list()
+    builder = builders.RunfilesBuilder()
+    builder.add(f1)
+    builder.add([f2])
+    builder.add(depset([f3]))
+
+    rf1 = ctx.runfiles([f4])
+    rf2 = ctx.runfiles([f5])
+    builder.add(rf1)
+    builder.add([rf2])
+
+    builder.add_targets([targets.runfiles])
+
+    builder.root_symlinks["root_link"] = f1
+    builder.symlinks["regular_link"] = f1
+
+    actual = builder.build(ctx)
+
+    subject = subjects.runfiles(actual, meta = env.expect.meta)
+    subject.contains_exactly([
+        "root_link",
+        "{workspace}/regular_link",
+        "{workspace}/tests/builders/f1.txt",
+        "{workspace}/tests/builders/f2.txt",
+        "{workspace}/tests/builders/f3.txt",
+        "{workspace}/tests/builders/f4.txt",
+        "{workspace}/tests/builders/f5.txt",
+        "{workspace}/tests/builders/runfile.txt",
+    ])
+
+_tests.append(_test_runfiles_builder)
+
+def builders_test_suite(name):
+    test_suite(
+        name = name,
+        tests = _tests,
+    )
diff --git a/tests/config_settings/construct_config_settings_tests.bzl b/tests/config_settings/construct_config_settings_tests.bzl
index 9e6b6e1..087efbb 100644
--- a/tests/config_settings/construct_config_settings_tests.bzl
+++ b/tests/config_settings/construct_config_settings_tests.bzl
@@ -13,7 +13,7 @@
 # limitations under the License.
 """Tests for construction of Python version matching config settings."""
 
-load("@//python:versions.bzl", "MINOR_MAPPING")
+load("@pythons_hub//:versions.bzl", "MINOR_MAPPING")
 load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
 load("@rules_testing//lib:test_suite.bzl", "test_suite")
 load("@rules_testing//lib:truth.bzl", "subjects")
@@ -167,7 +167,7 @@
                 "@platforms//os:" + os,
             ],
             flag_values = {
-                "//python/config_settings:_python_version_major_minor": "3.11",
+                "//python/config_settings:python_version_major_minor": "3.11",
             },
         )
 
@@ -178,7 +178,7 @@
                 "@platforms//cpu:" + cpu,
             ],
             flag_values = {
-                "//python/config_settings:_python_version_major_minor": "3.11",
+                "//python/config_settings:python_version_major_minor": "3.11",
             },
         )
 
@@ -198,7 +198,7 @@
                 "@platforms//os:" + os,
             ],
             flag_values = {
-                "//python/config_settings:_python_version_major_minor": "3.11",
+                "//python/config_settings:python_version_major_minor": "3.11",
             },
         )
 
diff --git a/tests/pypi/config_settings/config_settings_tests.bzl b/tests/pypi/config_settings/config_settings_tests.bzl
index 87e18b4..a77fa5b 100644
--- a/tests/pypi/config_settings/config_settings_tests.bzl
+++ b/tests/pypi/config_settings/config_settings_tests.bzl
@@ -55,6 +55,8 @@
     config_settings = dict(config_settings)
     if not config_settings:
         fail("For reproducibility on different platforms, the config setting must be specified")
+    python_version, default_value = _flag.python_version("3.7.10")
+    config_settings.setdefault(python_version, default_value)
 
     analysis_test(
         name = name,
@@ -75,7 +77,7 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_sdist": "sdist",
+            "is_cp3.7_sdist": "sdist",
         },
         want = "sdist",
     )
@@ -86,7 +88,7 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_sdist": "sdist",
+            "is_cp3.7_sdist": "sdist",
         },
         config_settings = [
             _flag.platform("linux_aarch64"),
@@ -101,7 +103,7 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_sdist": "sdist",
+            "is_cp3.7_sdist": "sdist",
         },
         config_settings = [
             _flag.platform("linux_aarch64"),
@@ -118,8 +120,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py_none_any": "whl",
-            "is_sdist": "sdist",
+            "is_cp3.7_py_none_any": "whl",
+            "is_cp3.7_sdist": "sdist",
         },
         want = "whl",
     )
@@ -130,8 +132,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py_none_any": "whl",
-            "is_sdist": "sdist",
+            "is_cp3.7_py_none_any": "whl",
+            "is_cp3.7_sdist": "sdist",
         },
         config_settings = [
             _flag.platform("linux_aarch64"),
@@ -146,8 +148,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py_none_any": "whl",
-            "is_sdist": "sdist",
+            "is_cp3.7_py_none_any": "whl",
+            "is_cp3.7_sdist": "sdist",
         },
         config_settings = [
             _flag.platform("linux_aarch64"),
@@ -162,8 +164,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py3_none_any": "whl",
-            "is_py_none_any": "basic_whl",
+            "is_cp3.7_py3_none_any": "whl",
+            "is_cp3.7_py_none_any": "basic_whl",
         },
         want = "whl",
     )
@@ -174,8 +176,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py3_none_any": "whl",
-            "is_py_none_any": "basic_whl",
+            "is_cp3.7_py3_none_any": "whl",
+            "is_cp3.7_py_none_any": "basic_whl",
         },
         config_settings = [
             _flag.platform("linux_aarch64"),
@@ -190,7 +192,7 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py3_none_any": "whl",
+            "is_cp3.7_py3_none_any": "whl",
         },
         config_settings = [
             _flag.platform("linux_aarch64"),
@@ -205,8 +207,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py3_abi3_any": "abi_whl",
-            "is_py3_none_any": "whl",
+            "is_cp3.7_py3_abi3_any": "abi_whl",
+            "is_cp3.7_py3_none_any": "whl",
         },
         want = "abi_whl",
     )
@@ -217,9 +219,9 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py3_none_any": "default_whl",
-            "is_py3_none_any_linux_aarch64": "whl",
-            "is_py3_none_any_linux_x86_64": "amd64_whl",
+            "is_cp3.7_py3_none_any": "default_whl",
+            "is_cp3.7_py3_none_any_linux_aarch64": "whl",
+            "is_cp3.7_py3_none_any_linux_x86_64": "amd64_whl",
         },
         want = "whl",
     )
@@ -230,9 +232,9 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_cp3x_none_any": "cp",
-            "is_py3_abi3_any": "py3_abi3",
-            "is_py3_none_any": "py3",
+            "is_cp3.7_cp3x_none_any": "cp",
+            "is_cp3.7_py3_abi3_any": "py3_abi3",
+            "is_cp3.7_py3_none_any": "py3",
         },
         want = "cp",
     )
@@ -243,8 +245,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_cp3x_abi3_any": "cp",
-            "is_py3_abi3_any": "py3",
+            "is_cp3.7_cp3x_abi3_any": "cp",
+            "is_cp3.7_py3_abi3_any": "py3",
         },
         want = "cp",
     )
@@ -258,7 +260,6 @@
             "is_cp3.10_cp3x_none_any": "cp310",
             "is_cp3.8_cp3x_none_any": "cp38",
             "is_cp3.9_cp3x_none_any": "cp39",
-            "is_cp3x_none_any": "cp_default",
         },
         want = "cp310",
         config_settings = [
@@ -319,11 +320,11 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py3_abi3_any": "better_default_whl",
-            "is_py3_abi3_any_linux_aarch64": "better_default_any_whl",
-            "is_py3_none_any": "default_whl",
-            "is_py3_none_any_linux_aarch64": "whl",
-            "is_py3_none_linux_aarch64": "platform_whl",
+            "is_cp3.7_py3_abi3_any": "better_default_whl",
+            "is_cp3.7_py3_abi3_any_linux_aarch64": "better_default_any_whl",
+            "is_cp3.7_py3_none_any": "default_whl",
+            "is_cp3.7_py3_none_any_linux_aarch64": "whl",
+            "is_cp3.7_py3_none_linux_aarch64": "platform_whl",
         },
         want = "platform_whl",
     )
@@ -334,8 +335,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_py3_abi3_linux_aarch64": "abi3_platform",
-            "is_py3_none_linux_aarch64": "platform",
+            "is_cp3.7_py3_abi3_linux_aarch64": "abi3_platform",
+            "is_cp3.7_py3_none_linux_aarch64": "platform",
         },
         want = "abi3_platform",
     )
@@ -346,8 +347,8 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_cp3x_cp_manylinux_aarch64": "glibc",
-            "is_py3_abi3_linux_aarch64": "abi3_platform",
+            "is_cp3.7_cp3x_cp_manylinux_aarch64": "glibc",
+            "is_cp3.7_py3_abi3_linux_aarch64": "abi3_platform",
         },
         want = "glibc",
     )
@@ -358,9 +359,9 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_cp3x_cp_manylinux_2_14_aarch64": "glibc",
-            "is_cp3x_cp_manylinux_2_17_aarch64": "glibc",
-            "is_py3_abi3_linux_aarch64": "abi3_platform",
+            "is_cp3.7_cp3x_cp_manylinux_2_14_aarch64": "glibc",
+            "is_cp3.7_cp3x_cp_manylinux_2_17_aarch64": "glibc",
+            "is_cp3.7_py3_abi3_linux_aarch64": "abi3_platform",
         },
         want = "glibc",
         config_settings = [
@@ -378,8 +379,8 @@
         dist = {
             # Code using the conditions will need to construct selects, which
             # do the version matching correctly.
-            "is_cp3x_cp_manylinux_2_14_aarch64": "2_14_whl_via_2_14_branch",
-            "is_cp3x_cp_manylinux_2_17_aarch64": "2_14_whl_via_2_17_branch",
+            "is_cp3.7_cp3x_cp_manylinux_2_14_aarch64": "2_14_whl_via_2_14_branch",
+            "is_cp3.7_cp3x_cp_manylinux_2_17_aarch64": "2_14_whl_via_2_17_branch",
         },
         want = "2_14_whl_via_2_17_branch",
         config_settings = [
@@ -395,7 +396,7 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_cp3x_cp_musllinux_aarch64": "musl",
+            "is_cp3.7_cp3x_cp_musllinux_aarch64": "musl",
         },
         want = "musl",
         config_settings = [
@@ -410,7 +411,7 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_cp3x_cp_windows_x86_64": "whl",
+            "is_cp3.7_cp3x_cp_windows_x86_64": "whl",
         },
         want = "whl",
         config_settings = [
@@ -425,8 +426,8 @@
         name = name,
         dist = {
             # We prefer arch specific whls over universal
-            "is_cp3x_cp_osx_x86_64": "whl",
-            "is_cp3x_cp_osx_x86_64_universal2": "universal_whl",
+            "is_cp3.7_cp3x_cp_osx_x86_64": "whl",
+            "is_cp3.7_cp3x_cp_osx_x86_64_universal2": "universal_whl",
         },
         want = "whl",
         config_settings = [
@@ -441,7 +442,7 @@
         name = name,
         dist = {
             # We default to universal if only that exists
-            "is_cp3x_cp_osx_x86_64_universal2": "whl",
+            "is_cp3.7_cp3x_cp_osx_x86_64_universal2": "whl",
         },
         want = "whl",
         config_settings = [
@@ -456,8 +457,8 @@
         name = name,
         dist = {
             # If we prefer universal, then we use that
-            "is_cp3x_cp_osx_x86_64": "whl",
-            "is_cp3x_cp_osx_x86_64_universal2": "universal",
+            "is_cp3.7_cp3x_cp_osx_x86_64": "whl",
+            "is_cp3.7_cp3x_cp_osx_x86_64_universal2": "universal",
         },
         want = "universal",
         config_settings = [
@@ -474,7 +475,7 @@
         dist = {
             # Similarly to the libc version, the user of the config settings will have to
             # construct the select so that the version selection is correct.
-            "is_cp3x_cp_osx_10_9_x86_64": "whl",
+            "is_cp3.7_cp3x_cp_osx_10_9_x86_64": "whl",
         },
         want = "whl",
         config_settings = [
@@ -489,7 +490,7 @@
     _analysis_test(
         name = name,
         dist = {
-            "is_" + f: f
+            "is_cp3.7_" + f: f
             for f in [
                 "{py}_{abi}_{plat}".format(py = valid_py, abi = valid_abi, plat = valid_plat)
                 # we have py2.py3, py3, cp3x
@@ -528,7 +529,7 @@
 
     config_settings(
         name = "dummy",
-        python_versions = ["3.8", "3.9", "3.10"],
+        python_versions = ["3.7", "3.8", "3.9", "3.10"],
         glibc_versions = [(2, 14), (2, 17)],
         muslc_versions = [(1, 1)],
         osx_versions = [(10, 9), (11, 0)],
diff --git a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl
index a860681..9453011 100644
--- a/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl
+++ b/tests/pypi/generate_whl_library_build_bazel/generate_whl_library_build_bazel_tests.bzl
@@ -160,7 +160,7 @@
 config_setting(
     name = "is_python_3.10_linux_ppc",
     flag_values = {
-        "@rules_python//python/config_settings:_python_version_major_minor": "3.10",
+        "@rules_python//python/config_settings:python_version_major_minor": "3.10",
     },
     constraint_values = [
         "@platforms//cpu:ppc",
@@ -172,7 +172,7 @@
 config_setting(
     name = "is_python_3.9_anyos_aarch64",
     flag_values = {
-        "@rules_python//python/config_settings:_python_version_major_minor": "3.9",
+        "@rules_python//python/config_settings:python_version_major_minor": "3.9",
     },
     constraint_values = ["@platforms//cpu:aarch64"],
     visibility = ["//visibility:private"],
@@ -181,7 +181,7 @@
 config_setting(
     name = "is_python_3.9_linux_anyarch",
     flag_values = {
-        "@rules_python//python/config_settings:_python_version_major_minor": "3.9",
+        "@rules_python//python/config_settings:python_version_major_minor": "3.9",
     },
     constraint_values = ["@platforms//os:linux"],
     visibility = ["//visibility:private"],
diff --git a/tests/pypi/integration/BUILD.bazel b/tests/pypi/integration/BUILD.bazel
index f846bfb..9ea8dce 100644
--- a/tests/pypi/integration/BUILD.bazel
+++ b/tests/pypi/integration/BUILD.bazel
@@ -1,5 +1,5 @@
 load("@bazel_skylib//rules:build_test.bzl", "build_test")
-load("@dev_pip//:requirements.bzl", "all_requirements")
+load("@rules_python_publish_deps//:requirements.bzl", "all_requirements")
 load(":transitions.bzl", "transition_rule")
 
 build_test(
diff --git a/tests/pypi/parse_requirements/parse_requirements_tests.bzl b/tests/pypi/parse_requirements/parse_requirements_tests.bzl
index 25d2961..c719ad6 100644
--- a/tests/pypi/parse_requirements/parse_requirements_tests.bzl
+++ b/tests/pypi/parse_requirements/parse_requirements_tests.bzl
@@ -22,6 +22,11 @@
         "requirements_direct": """\
 foo[extra] @ https://some-url
 """,
+        "requirements_extra_args": """\
+--index-url=example.org
+
+foo[extra]==0.0.1 --hash=sha256:deadbeef
+""",
         "requirements_linux": """\
 foo==0.0.3 --hash=sha256:deadbaaf
 """,
@@ -93,6 +98,43 @@
 
 _tests.append(_test_simple)
 
+def _test_extra_pip_args(env):
+    got = parse_requirements(
+        ctx = _mock_ctx(),
+        requirements_by_platform = {
+            "requirements_extra_args": ["linux_x86_64"],
+        },
+        extra_pip_args = ["--trusted-host=example.org"],
+    )
+    env.expect.that_dict(got).contains_exactly({
+        "foo": [
+            struct(
+                distribution = "foo",
+                extra_pip_args = ["--index-url=example.org", "--trusted-host=example.org"],
+                requirement_line = "foo[extra]==0.0.1 --hash=sha256:deadbeef",
+                srcs = struct(
+                    requirement = "foo[extra]==0.0.1",
+                    shas = ["deadbeef"],
+                    version = "0.0.1",
+                ),
+                target_platforms = [
+                    "linux_x86_64",
+                ],
+                whls = [],
+                sdist = None,
+                is_exposed = True,
+            ),
+        ],
+    })
+    env.expect.that_str(
+        select_requirement(
+            got["foo"],
+            platform = "linux_x86_64",
+        ).srcs.version,
+    ).equals("0.0.1")
+
+_tests.append(_test_extra_pip_args)
+
 def _test_dupe_requirements(env):
     got = parse_requirements(
         ctx = _mock_ctx(),
diff --git a/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl b/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl
index 09a0631..9de309b 100644
--- a/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl
+++ b/tests/pypi/render_pkg_aliases/render_pkg_aliases_test.bzl
@@ -110,7 +110,6 @@
 def _test_bzlmod_aliases(env):
     # Use this function as it is used in pip_repository
     actual = render_multiplatform_pkg_aliases(
-        default_config_setting = "//:my_config_setting",
         aliases = {
             "bar-baz": [
                 whl_alias(version = "3.2", repo = "pypi_32_bar_baz", config_setting = "//:my_config_setting"),
@@ -124,6 +123,23 @@
 
 package(default_visibility = ["//visibility:public"])
 
+_NO_MATCH_ERROR = \"\"\"\\
+No matching wheel for current configuration's Python version.
+
+The current build configuration's Python version doesn't match any of the Python
+wheels available for this wheel. This wheel supports the following Python
+configuration settings:
+    //:my_config_setting
+
+To determine the current configuration's Python version, run:
+    `bazel config <config id>` (shown further below)
+and look for
+    rules_python//python/config_settings:python_version
+
+If the value is missing, then the "default" Python version is being used,
+which has a "null" version value and will not match version constraints.
+\"\"\"
+
 alias(
     name = "bar_baz",
     actual = ":pkg",
@@ -133,11 +149,9 @@
     name = "pkg",
     actual = selects.with_or(
         {
-            (
-                "//:my_config_setting",
-                "//conditions:default",
-            ): "@pypi_32_bar_baz//:pkg",
+            "//:my_config_setting": "@pypi_32_bar_baz//:pkg",
         },
+        no_match_error = _NO_MATCH_ERROR,
     ),
 )
 
@@ -145,11 +159,9 @@
     name = "whl",
     actual = selects.with_or(
         {
-            (
-                "//:my_config_setting",
-                "//conditions:default",
-            ): "@pypi_32_bar_baz//:whl",
+            "//:my_config_setting": "@pypi_32_bar_baz//:whl",
         },
+        no_match_error = _NO_MATCH_ERROR,
     ),
 )
 
@@ -157,11 +169,9 @@
     name = "data",
     actual = selects.with_or(
         {
-            (
-                "//:my_config_setting",
-                "//conditions:default",
-            ): "@pypi_32_bar_baz//:data",
+            "//:my_config_setting": "@pypi_32_bar_baz//:data",
         },
+        no_match_error = _NO_MATCH_ERROR,
     ),
 )
 
@@ -169,11 +179,9 @@
     name = "dist_info",
     actual = selects.with_or(
         {
-            (
-                "//:my_config_setting",
-                "//conditions:default",
-            ): "@pypi_32_bar_baz//:dist_info",
+            "//:my_config_setting": "@pypi_32_bar_baz//:dist_info",
         },
+        no_match_error = _NO_MATCH_ERROR,
     ),
 )"""
 
@@ -198,7 +206,6 @@
 
 def _test_bzlmod_aliases_with_no_default_version(env):
     actual = render_multiplatform_pkg_aliases(
-        default_config_setting = None,
         aliases = {
             "bar-baz": [
                 whl_alias(
@@ -291,106 +298,8 @@
 
 _tests.append(_test_bzlmod_aliases_with_no_default_version)
 
-def _test_bzlmod_aliases_for_non_root_modules(env):
-    actual = render_pkg_aliases(
-        # NOTE @aignas 2024-01-17: if the default X.Y version coincides with the
-        # versions that are used in the root module, then this would be the same as
-        # as _test_bzlmod_aliases.
-        #
-        # However, if the root module uses a different default version than the
-        # non-root module, then we will have a no-match-error because the
-        # default_config_setting is not in the list of the versions in the
-        # whl_map.
-        default_config_setting = "//_config:is_python_3.3",
-        aliases = {
-            "bar-baz": [
-                whl_alias(version = "3.2", repo = "pypi_32_bar_baz"),
-                whl_alias(version = "3.1", repo = "pypi_31_bar_baz"),
-            ],
-        },
-    )
-
-    want_key = "bar_baz/BUILD.bazel"
-    want_content = """\
-load("@bazel_skylib//lib:selects.bzl", "selects")
-
-package(default_visibility = ["//visibility:public"])
-
-_NO_MATCH_ERROR = \"\"\"\\
-No matching wheel for current configuration's Python version.
-
-The current build configuration's Python version doesn't match any of the Python
-wheels available for this wheel. This wheel supports the following Python
-configuration settings:
-    //_config:is_python_3.1
-    //_config:is_python_3.2
-
-To determine the current configuration's Python version, run:
-    `bazel config <config id>` (shown further below)
-and look for
-    rules_python//python/config_settings:python_version
-
-If the value is missing, then the "default" Python version is being used,
-which has a "null" version value and will not match version constraints.
-\"\"\"
-
-alias(
-    name = "bar_baz",
-    actual = ":pkg",
-)
-
-alias(
-    name = "pkg",
-    actual = selects.with_or(
-        {
-            "//_config:is_python_3.1": "@pypi_31_bar_baz//:pkg",
-            "//_config:is_python_3.2": "@pypi_32_bar_baz//:pkg",
-        },
-        no_match_error = _NO_MATCH_ERROR,
-    ),
-)
-
-alias(
-    name = "whl",
-    actual = selects.with_or(
-        {
-            "//_config:is_python_3.1": "@pypi_31_bar_baz//:whl",
-            "//_config:is_python_3.2": "@pypi_32_bar_baz//:whl",
-        },
-        no_match_error = _NO_MATCH_ERROR,
-    ),
-)
-
-alias(
-    name = "data",
-    actual = selects.with_or(
-        {
-            "//_config:is_python_3.1": "@pypi_31_bar_baz//:data",
-            "//_config:is_python_3.2": "@pypi_32_bar_baz//:data",
-        },
-        no_match_error = _NO_MATCH_ERROR,
-    ),
-)
-
-alias(
-    name = "dist_info",
-    actual = selects.with_or(
-        {
-            "//_config:is_python_3.1": "@pypi_31_bar_baz//:dist_info",
-            "//_config:is_python_3.2": "@pypi_32_bar_baz//:dist_info",
-        },
-        no_match_error = _NO_MATCH_ERROR,
-    ),
-)"""
-
-    env.expect.that_collection(actual.keys()).contains_exactly([want_key])
-    env.expect.that_str(actual[want_key]).equals(want_content)
-
-_tests.append(_test_bzlmod_aliases_for_non_root_modules)
-
 def _test_aliases_are_created_for_all_wheels(env):
     actual = render_pkg_aliases(
-        default_config_setting = "//_config:is_python_3.2",
         aliases = {
             "bar": [
                 whl_alias(version = "3.1", repo = "pypi_31_bar"),
@@ -414,7 +323,6 @@
 
 def _test_aliases_with_groups(env):
     actual = render_pkg_aliases(
-        default_config_setting = "//_config:is_python_3.2",
         aliases = {
             "bar": [
                 whl_alias(version = "3.1", repo = "pypi_31_bar"),
@@ -555,13 +463,12 @@
         *,
         filename,
         want,
+        python_version,
         want_versions = {},
         target_platforms = [],
         glibc_versions = [],
         muslc_versions = [],
-        osx_versions = [],
-        python_version = "",
-        python_default = True):
+        osx_versions = []):
     got, got_default_version_settings = get_filename_config_settings(
         filename = filename,
         target_platforms = target_platforms,
@@ -569,7 +476,6 @@
         muslc_versions = muslc_versions,
         osx_versions = osx_versions,
         python_version = python_version,
-        python_default = python_default,
     )
     env.expect.that_collection(got).contains_exactly(want)
     env.expect.that_dict(got_default_version_settings).contains_exactly(want_versions)
@@ -580,42 +486,21 @@
         _test_config_settings(
             env,
             filename = "foo-0.0.1" + ext,
-            want = [":is_sdist"],
+            python_version = "3.2",
+            want = [":is_cp3.2_sdist"],
         )
 
     ext = ".zip"
     _test_config_settings(
         env,
         filename = "foo-0.0.1" + ext,
-        target_platforms = [
-            "linux_aarch64",
-        ],
-        want = [":is_sdist_linux_aarch64"],
-    )
-
-    _test_config_settings(
-        env,
-        filename = "foo-0.0.1" + ext,
         python_version = "3.2",
-        want = [
-            ":is_sdist",
-            ":is_cp3.2_sdist",
-        ],
-    )
-
-    _test_config_settings(
-        env,
-        filename = "foo-0.0.1" + ext,
-        python_version = "3.2",
-        python_default = True,
         target_platforms = [
             "linux_aarch64",
             "linux_x86_64",
         ],
         want = [
-            ":is_sdist_linux_aarch64",
             ":is_cp3.2_sdist_linux_aarch64",
-            ":is_sdist_linux_x86_64",
             ":is_cp3.2_sdist_linux_x86_64",
         ],
     )
@@ -626,25 +511,8 @@
     _test_config_settings(
         env,
         filename = "foo-0.0.1-py2.py3-none-any.whl",
-        want = [":is_py_none_any"],
-    )
-
-    _test_config_settings(
-        env,
-        filename = "foo-0.0.1-py2.py3-none-any.whl",
-        target_platforms = [
-            "linux_aarch64",
-        ],
-        want = [":is_py_none_any_linux_aarch64"],
-    )
-
-    _test_config_settings(
-        env,
-        filename = "foo-0.0.1-py2.py3-none-any.whl",
         python_version = "3.2",
-        python_default = True,
         want = [
-            ":is_py_none_any",
             ":is_cp3.2_py_none_any",
         ],
     )
@@ -653,13 +521,10 @@
         env,
         filename = "foo-0.0.1-py2.py3-none-any.whl",
         python_version = "3.2",
-        python_default = False,
         target_platforms = [
             "osx_x86_64",
         ],
-        want = [
-            ":is_cp3.2_py_none_any_osx_x86_64",
-        ],
+        want = [":is_cp3.2_py_none_any_osx_x86_64"],
     )
 
 _tests.append(_test_py2_py3_none_any)
@@ -668,14 +533,16 @@
     _test_config_settings(
         env,
         filename = "foo-0.0.1-py3-none-any.whl",
-        want = [":is_py3_none_any"],
+        python_version = "3.1",
+        want = [":is_cp3.1_py3_none_any"],
     )
 
     _test_config_settings(
         env,
         filename = "foo-0.0.1-py3-none-any.whl",
+        python_version = "3.1",
         target_platforms = ["linux_x86_64"],
-        want = [":is_py3_none_any_linux_x86_64"],
+        want = [":is_cp3.1_py3_none_any_linux_x86_64"],
     )
 
 _tests.append(_test_py3_none_any)
@@ -684,19 +551,20 @@
     _test_config_settings(
         env,
         filename = "foo-0.0.1-py3-none-macosx_10_9_universal2.whl",
+        python_version = "3.1",
         osx_versions = [
             (10, 9),
             (11, 0),
         ],
         want = [],
         want_versions = {
-            ":is_py3_none_osx_aarch64_universal2": {
-                (10, 9): ":is_py3_none_osx_10_9_aarch64_universal2",
-                (11, 0): ":is_py3_none_osx_11_0_aarch64_universal2",
+            ":is_cp3.1_py3_none_osx_aarch64_universal2": {
+                (10, 9): ":is_cp3.1_py3_none_osx_10_9_aarch64_universal2",
+                (11, 0): ":is_cp3.1_py3_none_osx_11_0_aarch64_universal2",
             },
-            ":is_py3_none_osx_x86_64_universal2": {
-                (10, 9): ":is_py3_none_osx_10_9_x86_64_universal2",
-                (11, 0): ":is_py3_none_osx_11_0_x86_64_universal2",
+            ":is_cp3.1_py3_none_osx_x86_64_universal2": {
+                (10, 9): ":is_cp3.1_py3_none_osx_10_9_x86_64_universal2",
+                (11, 0): ":is_cp3.1_py3_none_osx_11_0_x86_64_universal2",
             },
         },
     )
@@ -707,20 +575,8 @@
     _test_config_settings(
         env,
         filename = "foo-0.0.1-cp37-abi3-linux_x86_64.whl",
-        want = [
-            ":is_cp3x_abi3_linux_x86_64",
-        ],
-    )
-
-    _test_config_settings(
-        env,
-        filename = "foo-0.0.1-cp37-abi3-linux_x86_64.whl",
-        python_version = "3.2",
-        python_default = True,
-        want = [
-            ":is_cp3x_abi3_linux_x86_64",
-            ":is_cp3.2_cp3x_abi3_linux_x86_64",
-        ],
+        python_version = "3.7",
+        want = [":is_cp3.7_cp3x_abi3_linux_x86_64"],
     )
 
 _tests.append(_test_cp37_abi3_linux_x86_64)
@@ -729,9 +585,8 @@
     _test_config_settings(
         env,
         filename = "foo-0.0.1-cp37-abi3-windows_x86_64.whl",
-        want = [
-            ":is_cp3x_abi3_windows_x86_64",
-        ],
+        python_version = "3.7",
+        want = [":is_cp3.7_cp3x_abi3_windows_x86_64"],
     )
 
 _tests.append(_test_cp37_abi3_windows_x86_64)
@@ -740,6 +595,7 @@
     _test_config_settings(
         env,
         filename = "foo-0.0.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl",
+        python_version = "3.7",
         glibc_versions = [
             (2, 16),
             (2, 17),
@@ -747,9 +603,9 @@
         ],
         want = [],
         want_versions = {
-            ":is_cp3x_abi3_manylinux_x86_64": {
-                (2, 17): ":is_cp3x_abi3_manylinux_2_17_x86_64",
-                (2, 18): ":is_cp3x_abi3_manylinux_2_18_x86_64",
+            ":is_cp3.7_cp3x_abi3_manylinux_x86_64": {
+                (2, 17): ":is_cp3.7_cp3x_abi3_manylinux_2_17_x86_64",
+                (2, 18): ":is_cp3.7_cp3x_abi3_manylinux_2_18_x86_64",
             },
         },
     )
@@ -761,6 +617,7 @@
     _test_config_settings(
         env,
         filename = "foo-0.0.1-cp37-cp37-manylinux_2_17_arm64.musllinux_1_1_arm64.whl",
+        python_version = "3.7",
         glibc_versions = [
             (2, 16),
             (2, 17),
@@ -771,12 +628,12 @@
         ],
         want = [],
         want_versions = {
-            ":is_cp3x_cp_manylinux_aarch64": {
-                (2, 17): ":is_cp3x_cp_manylinux_2_17_aarch64",
-                (2, 18): ":is_cp3x_cp_manylinux_2_18_aarch64",
+            ":is_cp3.7_cp3x_cp_manylinux_aarch64": {
+                (2, 17): ":is_cp3.7_cp3x_cp_manylinux_2_17_aarch64",
+                (2, 18): ":is_cp3.7_cp3x_cp_manylinux_2_18_aarch64",
             },
-            ":is_cp3x_cp_musllinux_aarch64": {
-                (1, 1): ":is_cp3x_cp_musllinux_1_1_aarch64",
+            ":is_cp3.7_cp3x_cp_musllinux_aarch64": {
+                (1, 1): ":is_cp3.7_cp3x_cp_musllinux_1_1_aarch64",
             },
         },
     )
@@ -785,7 +642,7 @@
 
 def _test_multiplatform_whl_aliases_empty(env):
     # Check that we still work with an empty requirements.txt
-    got = multiplatform_whl_aliases(aliases = [], default_version = None)
+    got = multiplatform_whl_aliases(aliases = [])
     env.expect.that_collection(got).contains_exactly([])
 
 _tests.append(_test_multiplatform_whl_aliases_empty)
@@ -798,7 +655,7 @@
             version = "3.1",
         ),
     ]
-    got = multiplatform_whl_aliases(aliases = aliases, default_version = None)
+    got = multiplatform_whl_aliases(aliases = aliases)
     env.expect.that_collection(got).contains_exactly(aliases)
 
 _tests.append(_test_multiplatform_whl_aliases_nofilename)
@@ -827,7 +684,6 @@
     ]
     got = multiplatform_whl_aliases(
         aliases = aliases,
-        default_version = "3.1",
         glibc_versions = [],
         muslc_versions = [],
         osx_versions = [],
@@ -837,9 +693,6 @@
         whl_alias(config_setting = "//_config:is_cp3.1_py3_none_any_linux_aarch64", repo = "foo-0.0.2", version = "3.1"),
         whl_alias(config_setting = "//_config:is_cp3.1_py3_none_any_linux_x86_64", repo = "foo-0.0.2", version = "3.1"),
         whl_alias(config_setting = "//_config:is_cp3.2_py3_none_any", repo = "foo-py3-0.0.3", version = "3.2"),
-        whl_alias(config_setting = "//_config:is_py3_none_any", repo = "foo-py3-0.0.1", version = "3.1"),
-        whl_alias(config_setting = "//_config:is_py3_none_any_linux_aarch64", repo = "foo-0.0.2", version = "3.1"),
-        whl_alias(config_setting = "//_config:is_py3_none_any_linux_x86_64", repo = "foo-0.0.2", version = "3.1"),
     ]
     env.expect.that_collection(got).contains_exactly(want)
 
@@ -865,7 +718,6 @@
     ]
     got = multiplatform_whl_aliases(
         aliases = aliases,
-        default_version = None,
         glibc_versions = [(2, 17), (2, 18)],
         muslc_versions = [(1, 1), (1, 2)],
         osx_versions = [],
@@ -931,7 +783,6 @@
 
                 got_aliases = multiplatform_whl_aliases(
                     aliases = aliases,
-                    default_version = None,
                     glibc_versions = kwargs.get("glibc_versions", []),
                     muslc_versions = kwargs.get("muslc_versions", []),
                     osx_versions = kwargs.get("osx_versions", []),
diff --git a/tests/python/python_tests.bzl b/tests/python/python_tests.bzl
index 101313d..4050430 100644
--- a/tests/python/python_tests.bzl
+++ b/tests/python/python_tests.bzl
@@ -14,8 +14,8 @@
 
 ""
 
+load("@pythons_hub//:versions.bzl", "MINOR_MAPPING")
 load("@rules_testing//lib:test_suite.bzl", "test_suite")
-load("//python:versions.bzl", "MINOR_MAPPING")
 load("//python/private:python.bzl", "parse_modules")  # buildifier: disable=bzl-visibility
 
 _tests = []
@@ -451,6 +451,7 @@
         "url": {"aarch64-unknown-linux-gnu": ["something.org", "else.org"]},
     })
     env.expect.that_dict(py.config.minor_mapping).contains_exactly({
+        "3.12": "3.12.4",  # The `minor_mapping` will be overriden only for the missing keys
         "3.13": "3.13.0",
     })
     env.expect.that_collection(py.toolchains).contains_exactly([
diff --git a/tests/support/sh_py_run_test.bzl b/tests/support/sh_py_run_test.bzl
index 32df5b8..066f091 100644
--- a/tests/support/sh_py_run_test.bzl
+++ b/tests/support/sh_py_run_test.bzl
@@ -17,6 +17,7 @@
 without the overhead of a bazel-in-bazel integration test.
 """
 
+load("@rules_shell//shell:sh_test.bzl", "sh_test")
 load("//python:py_binary.bzl", "py_binary")
 load("//python:py_test.bzl", "py_test")
 load("//python/private:toolchain_types.bzl", "TARGET_TOOLCHAIN_TYPE")  # buildifier: disable=bzl-visibility
@@ -149,7 +150,7 @@
 
 def sh_py_run_test(*, name, sh_src, py_src, **kwargs):
     bin_name = "_{}_bin".format(name)
-    native.sh_test(
+    sh_test(
         name = name,
         srcs = [sh_src],
         data = [bin_name],
diff --git a/tests/support/support.bzl b/tests/support/support.bzl
index 150ca7f..7358a6b 100644
--- a/tests/support/support.bzl
+++ b/tests/support/support.bzl
@@ -32,9 +32,9 @@
 
 # str() around Label() is necessary because rules_testing's config_settings
 # doesn't accept yet Label objects.
+ADD_SRCS_TO_RUNFILES = str(Label("//python/config_settings:add_srcs_to_runfiles"))
 EXEC_TOOLS_TOOLCHAIN = str(Label("//python/config_settings:exec_tools_toolchain"))
 PRECOMPILE = str(Label("//python/config_settings:precompile"))
-PRECOMPILE_ADD_TO_RUNFILES = str(Label("//python/config_settings:precompile_add_to_runfiles"))
 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"))
diff --git a/tests/whl_filegroup/extract_wheel_files_test.py b/tests/whl_filegroup/extract_wheel_files_test.py
index 2ea175b..434899d 100644
--- a/tests/whl_filegroup/extract_wheel_files_test.py
+++ b/tests/whl_filegroup/extract_wheel_files_test.py
@@ -10,44 +10,36 @@
 class WheelRecordTest(unittest.TestCase):
     def test_get_wheel_record(self) -> None:
         record = extract_wheel_files.get_record(_WHEEL)
-        expected = {
-            "examples/wheel/lib/data.txt": (
-                "sha256=9vJKEdfLu8bZRArKLroPZJh1XKkK3qFMXiM79MBL2Sg",
-                12,
-            ),
-            "examples/wheel/lib/module_with_data.py": (
-                "sha256=8s0Khhcqz3yVsBKv2IB5u4l4TMKh7-c_V6p65WVHPms",
-                637,
-            ),
-            "examples/wheel/lib/simple_module.py": (
-                "sha256=z2hwciab_XPNIBNH8B1Q5fYgnJvQTeYf0ZQJpY8yLLY",
-                637,
-            ),
-            "examples/wheel/main.py": (
-                "sha256=sgg5iWN_9inYBjm6_Zw27hYdmo-l24fA-2rfphT-IlY",
-                909,
-            ),
-            "example_minimal_package-0.0.1.dist-info/WHEEL": (
-                "sha256=sobxWSyDDkdg_rinUth-jxhXHqoNqlmNMJY3aTZn2Us",
-                91,
-            ),
-            "example_minimal_package-0.0.1.dist-info/METADATA": (
-                "sha256=cfiQ2hFJhCKCUgbwtAwWG0fhW6NTzw4cr1uKOBcV_IM",
-                76,
-            ),
-        }
+        expected = (
+            "examples/wheel/lib/data,with,commas.txt",
+            "examples/wheel/lib/data.txt",
+            "examples/wheel/lib/module_with_data.py",
+            "examples/wheel/lib/simple_module.py",
+            "examples/wheel/main.py",
+            "example_minimal_package-0.0.1.dist-info/WHEEL",
+            "example_minimal_package-0.0.1.dist-info/METADATA",
+            "example_minimal_package-0.0.1.dist-info/RECORD",
+        )
         self.maxDiff = None
-        self.assertDictEqual(record, expected)
+        self.assertEqual(list(record), list(expected))
 
     def test_get_files(self) -> None:
         pattern = "(examples/wheel/lib/.*\.txt$|.*main)"
         record = extract_wheel_files.get_record(_WHEEL)
         files = extract_wheel_files.get_files(record, pattern)
-        expected = ["examples/wheel/lib/data.txt", "examples/wheel/main.py"]
+        expected = [
+            "examples/wheel/lib/data,with,commas.txt",
+            "examples/wheel/lib/data.txt",
+            "examples/wheel/main.py",
+        ]
         self.assertEqual(files, expected)
 
     def test_extract(self) -> None:
-        files = {"examples/wheel/lib/data.txt", "examples/wheel/main.py"}
+        files = {
+            "examples/wheel/lib/data,with,commas.txt",
+            "examples/wheel/lib/data.txt",
+            "examples/wheel/main.py",
+        }
         with tempfile.TemporaryDirectory() as tmpdir:
             outdir = Path(tmpdir)
             extract_wheel_files.extract_files(_WHEEL, files, outdir)
diff --git a/tools/private/update_deps/update_coverage_deps.py b/tools/private/update_deps/update_coverage_deps.py
index 6b837b9..a856b7a 100755
--- a/tools/private/update_deps/update_coverage_deps.py
+++ b/tools/private/update_deps/update_coverage_deps.py
@@ -131,7 +131,7 @@
         "--py",
         nargs="+",
         type=str,
-        default=["cp38", "cp39", "cp310", "cp311", "cp312"],
+        default=["cp38", "cp39", "cp310", "cp311", "cp312", "cp313"],
         help="Supported python versions",
     )
     parser.add_argument(
diff --git a/tools/publish/requirements_darwin.txt b/tools/publish/requirements_darwin.txt
index dd4ac40..ddd47c7 100644
--- a/tools/publish/requirements_darwin.txt
+++ b/tools/publish/requirements_darwin.txt
@@ -8,9 +8,9 @@
     --hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \
     --hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4
     # via readme-renderer
-certifi==2024.7.4 \
-    --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
-    --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
+certifi==2024.8.30 \
+    --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \
+    --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9
     # via requests
 charset-normalizer==3.3.2 \
     --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
@@ -104,13 +104,13 @@
     --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
     --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
     # via requests
-docutils==0.19 \
-    --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \
-    --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc
+docutils==0.21.2 \
+    --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \
+    --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2
     # via readme-renderer
-idna==3.7 \
-    --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
-    --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
+idna==3.10 \
+    --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
+    --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
     # via requests
 importlib-metadata==6.0.0 \
     --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \
diff --git a/tools/publish/requirements_windows.txt b/tools/publish/requirements_windows.txt
index 7e210c9..e9e523a 100644
--- a/tools/publish/requirements_windows.txt
+++ b/tools/publish/requirements_windows.txt
@@ -8,9 +8,9 @@
     --hash=sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414 \
     --hash=sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4
     # via readme-renderer
-certifi==2024.7.4 \
-    --hash=sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b \
-    --hash=sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90
+certifi==2024.8.30 \
+    --hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \
+    --hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9
     # via requests
 charset-normalizer==3.3.2 \
     --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
@@ -104,13 +104,13 @@
     --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
     --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
     # via requests
-docutils==0.19 \
-    --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \
-    --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc
+docutils==0.21.2 \
+    --hash=sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f \
+    --hash=sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2
     # via readme-renderer
-idna==3.7 \
-    --hash=sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc \
-    --hash=sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0
+idna==3.10 \
+    --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
+    --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
     # via requests
 importlib-metadata==6.0.0 \
     --hash=sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad \
diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py
index 68578b8..23b18ec 100644
--- a/tools/wheelmaker.py
+++ b/tools/wheelmaker.py
@@ -16,7 +16,9 @@
 
 import argparse
 import base64
+import csv
 import hashlib
+import io
 import os
 import re
 import stat
@@ -208,14 +210,23 @@
         """Write RECORD file to the distribution."""
         record_path = self.distinfo_path("RECORD")
         entries = self._record + [(record_path, b"", b"")]
-        contents = b""
-        for filename, digest, size in entries:
-            if isinstance(filename, str):
-                filename = filename.lstrip("/").encode("utf-8", "surrogateescape")
-            contents += b"%s,%s,%s\n" % (filename, digest, size)
+        with io.StringIO() as contents_io:
+            writer = csv.writer(contents_io, lineterminator="\n")
+            for filename, digest, size in entries:
+                if isinstance(filename, str):
+                    filename = filename.lstrip("/")
+                writer.writerow(
+                    (
+                        c
+                        if isinstance(c, str)
+                        else c.decode("utf-8", "surrogateescape")
+                        for c in (filename, digest, size)
+                    )
+                )
 
-        self.add_string(record_path, contents)
-        return contents
+            contents = contents_io.getvalue()
+            self.add_string(record_path, contents)
+            return contents.encode("utf-8", "surrogateescape")
 
 
 class WheelMaker(object):
@@ -227,6 +238,7 @@
         python_tag,
         abi,
         platform,
+        compress,
         outfile=None,
         strip_path_prefixes=None,
     ):
@@ -238,6 +250,7 @@
         self._platform = platform
         self._outfile = outfile
         self._strip_path_prefixes = strip_path_prefixes
+        self._compress = compress
         self._wheelname_fragment_distribution_name = escape_filename_distribution_name(
             self._name
         )
@@ -254,6 +267,7 @@
             mode="w",
             distribution_prefix=self._distribution_prefix,
             strip_path_prefixes=self._strip_path_prefixes,
+            compression=zipfile.ZIP_DEFLATED if self._compress else zipfile.ZIP_STORED,
         )
         return self
 
@@ -389,6 +403,11 @@
         "--out", type=str, default=None, help="Override name of ouptut file"
     )
     output_group.add_argument(
+        "--no_compress",
+        action="store_true",
+        help="Disable compression of the final archive",
+    )
+    output_group.add_argument(
         "--name_file",
         type=Path,
         help="A file where the canonical name of the " "wheel will be written",
@@ -516,6 +535,7 @@
         platform=arguments.platform,
         outfile=arguments.out,
         strip_path_prefixes=strip_prefixes,
+        compress=not arguments.no_compress,
     ) as maker:
         for package_filename, real_filename in all_files:
             maker.add_file(package_filename, real_filename)