internal: copy Starlark rule implementation from Bazel (#1418)

This is a copy of the Starlark implementation of the Python rules from
Bazel. This code isn't
loaded and won't work as-is. Modifications to make it work will be made
in
subsequent changes. It's almost pristine; changes are made to satisfy
the
buildifier check.

Work towards #1069
diff --git a/python/private/common/attributes.bzl b/python/private/common/attributes.bzl
new file mode 100644
index 0000000..7e28ed9
--- /dev/null
+++ b/python/private/common/attributes.bzl
@@ -0,0 +1,227 @@
+# 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.
+"""Attributes for Python rules."""
+
+load(":common/cc/cc_info.bzl", _CcInfo = "CcInfo")
+load(":common/python/common.bzl", "union_attrs")
+load(":common/python/providers.bzl", "PyInfo")
+load(
+    ":common/python/semantics.bzl",
+    "DEPS_ATTR_ALLOW_RULES",
+    "PLATFORMS_LOCATION",
+    "SRCS_ATTR_ALLOW_FILES",
+    "TOOLS_REPO",
+)
+
+PackageSpecificationInfo = _builtins.toplevel.PackageSpecificationInfo
+
+_STAMP_VALUES = [-1, 0, 1]
+
+def create_stamp_attr(**kwargs):
+    return {"stamp": attr.int(values = _STAMP_VALUES, **kwargs)}
+
+def create_srcs_attr(*, mandatory):
+    return {
+        "srcs": attr.label_list(
+            # Google builds change the set of allowed files.
+            allow_files = SRCS_ATTR_ALLOW_FILES,
+            mandatory = mandatory,
+            # Necessary for --compile_one_dependency to work.
+            flags = ["DIRECT_COMPILE_TIME_INPUT"],
+        ),
+    }
+
+SRCS_VERSION_ALL_VALUES = ["PY2", "PY2ONLY", "PY2AND3", "PY3", "PY3ONLY"]
+SRCS_VERSION_NON_CONVERSION_VALUES = ["PY2AND3", "PY2ONLY", "PY3ONLY"]
+
+def create_srcs_version_attr(values):
+    return {
+        "srcs_version": attr.string(
+            default = "PY2AND3",
+            values = values,
+        ),
+    }
+
+def copy_common_binary_kwargs(kwargs):
+    return {
+        key: kwargs[key]
+        for key in BINARY_ATTR_NAMES
+        if key in kwargs
+    }
+
+def copy_common_test_kwargs(kwargs):
+    return {
+        key: kwargs[key]
+        for key in TEST_ATTR_NAMES
+        if key in kwargs
+    }
+
+CC_TOOLCHAIN = {
+    # NOTE: The `cc_helper.find_cpp_toolchain()` function expects the attribute
+    # name to be this name.
+    "_cc_toolchain": attr.label(default = "@" + TOOLS_REPO + "//tools/cpp:current_cc_toolchain"),
+}
+
+# The common "data" attribute definition.
+DATA_ATTRS = {
+    # NOTE: The "flags" attribute is deprecated, but there isn't an alternative
+    # way to specify that constraints should be ignored.
+    "data": attr.label_list(
+        allow_files = True,
+        flags = ["SKIP_CONSTRAINTS_OVERRIDE"],
+    ),
+}
+
+NATIVE_RULES_ALLOWLIST_ATTRS = {
+    "_native_rules_allowlist": attr.label(
+        default = configuration_field(
+            fragment = "py",
+            name = "native_rules_allowlist",
+        ),
+        providers = [PackageSpecificationInfo],
+    ),
+}
+
+# Attributes common to all rules.
+COMMON_ATTRS = union_attrs(
+    DATA_ATTRS,
+    NATIVE_RULES_ALLOWLIST_ATTRS,
+    {
+        # NOTE: This attribute is deprecated and slated for removal.
+        "distribs": attr.string_list(),
+        # TODO(b/148103851): This attribute is deprecated and slated for
+        # removal.
+        # NOTE: The license attribute is missing in some Java integration tests,
+        # so fallback to a regular string_list for that case.
+        # buildifier: disable=attr-license
+        "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(),
+    },
+    allow_none = True,
+)
+
+# Attributes common to rules accepting Python sources and deps.
+PY_SRCS_ATTRS = union_attrs(
+    {
+        "deps": attr.label_list(
+            providers = [[PyInfo], [_CcInfo]],
+            # TODO(b/228692666): Google-specific; remove these allowances once
+            # the depot is cleaned up.
+            allow_rules = DEPS_ATTR_ALLOW_RULES,
+        ),
+        # Required attribute, but details vary by rule.
+        # Use create_srcs_attr to create one.
+        "srcs": None,
+        # NOTE: In Google, this attribute is deprecated, and can only
+        # effectively be PY3 or PY3ONLY. Externally, with Bazel, this attribute
+        # has a separate story.
+        # Required attribute, but the details vary by rule.
+        # Use create_srcs_version_attr to create one.
+        "srcs_version": None,
+    },
+    allow_none = True,
+)
+
+# Attributes specific to Python executable-equivalent rules. Such rules may not
+# accept Python sources (e.g. some packaged-version of a py_test/py_binary), but
+# still accept Python source-agnostic settings.
+AGNOSTIC_EXECUTABLE_ATTRS = union_attrs(
+    DATA_ATTRS,
+    {
+        "env": attr.string_dict(
+            doc = """\
+Dictionary of strings; optional; values are subject to `$(location)` and "Make
+variable" substitution.
+
+Specifies additional environment variables to set when the target is executed by
+`test` or `run`.
+""",
+        ),
+        # The value is required, but varies by rule and/or rule type. Use
+        # create_stamp_attr to create one.
+        "stamp": None,
+    },
+    allow_none = True,
+)
+
+# Attributes specific to Python test-equivalent executable rules. Such rules may
+# not accept Python sources (e.g. some packaged-version of a py_test/py_binary),
+# but still accept Python source-agnostic settings.
+AGNOSTIC_TEST_ATTRS = union_attrs(
+    AGNOSTIC_EXECUTABLE_ATTRS,
+    # Tests have stamping disabled by default.
+    create_stamp_attr(default = 0),
+    {
+        "env_inherit": attr.string_list(
+            doc = """\
+List of strings; optional
+
+Specifies additional environment variables to inherit from the external
+environment when the test is executed by bazel test.
+""",
+        ),
+        # TODO(b/176993122): Remove when Bazel automatically knows to run on darwin.
+        "_apple_constraints": attr.label_list(
+            default = [
+                PLATFORMS_LOCATION + "/os:ios",
+                PLATFORMS_LOCATION + "/os:macos",
+                PLATFORMS_LOCATION + "/os:tvos",
+                PLATFORMS_LOCATION + "/os:visionos",
+                PLATFORMS_LOCATION + "/os:watchos",
+            ],
+        ),
+    },
+)
+
+# Attributes specific to Python binary-equivalent executable rules. Such rules may
+# not accept Python sources (e.g. some packaged-version of a py_test/py_binary),
+# but still accept Python source-agnostic settings.
+AGNOSTIC_BINARY_ATTRS = union_attrs(
+    AGNOSTIC_EXECUTABLE_ATTRS,
+    create_stamp_attr(default = -1),
+)
+
+# Attribute names common to all Python rules
+COMMON_ATTR_NAMES = [
+    "compatible_with",
+    "deprecation",
+    "distribs",  # NOTE: Currently common to all rules, but slated for removal
+    "exec_compatible_with",
+    "exec_properties",
+    "features",
+    "restricted_to",
+    "tags",
+    "target_compatible_with",
+    # NOTE: The testonly attribute requires careful handling: None/unset means
+    # to use the `package(default_testonly`) value, which isn't observable
+    # during the loading phase.
+    "testonly",
+    "toolchains",
+    "visibility",
+] + COMMON_ATTRS.keys()
+
+# Attribute names common to all test=True rules
+TEST_ATTR_NAMES = COMMON_ATTR_NAMES + [
+    "args",
+    "size",
+    "timeout",
+    "flaky",
+    "shard_count",
+    "local",
+] + AGNOSTIC_TEST_ATTRS.keys()
+
+# Attribute names common to all executable=True rules
+BINARY_ATTR_NAMES = COMMON_ATTR_NAMES + [
+    "args",
+    "output_licenses",  # NOTE: Common to all rules, but slated for removal
+] + AGNOSTIC_BINARY_ATTRS.keys()
diff --git a/python/private/common/attributes_bazel.bzl b/python/private/common/attributes_bazel.bzl
new file mode 100644
index 0000000..f87245d
--- /dev/null
+++ b/python/private/common/attributes_bazel.bzl
@@ -0,0 +1,30 @@
+# 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.
+"""Attributes specific to the Bazel implementation of the Python rules."""
+
+IMPORTS_ATTRS = {
+    "imports": attr.string_list(
+        doc = """
+List of import directories to be added to the PYTHONPATH.
+
+Subject to "Make variable" substitution. These import directories will be added
+for this rule and all rules that depend on it (note: not the rules this rule
+depends on. Each directory will be added to `PYTHONPATH` by `py_binary` rules
+that depend on this rule. The strings are repo-runfiles-root relative,
+
+Absolute paths (paths that start with `/`) and paths that references a path
+above the execution root are not allowed and will result in an error.
+""",
+    ),
+}
diff --git a/python/private/common/common.bzl b/python/private/common/common.bzl
new file mode 100644
index 0000000..97ed3e3
--- /dev/null
+++ b/python/private/common/common.bzl
@@ -0,0 +1,528 @@
+# 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.
+"""Various things common to Bazel and Google rule implementations."""
+
+load(":common/cc/cc_helper.bzl", "cc_helper")
+load(
+    ":common/python/providers.bzl",
+    "PyInfo",
+)
+load(
+    ":common/python/semantics.bzl",
+    "NATIVE_RULES_MIGRATION_FIX_CMD",
+    "NATIVE_RULES_MIGRATION_HELP_URL",
+    "TOOLS_REPO",
+)
+
+_testing = _builtins.toplevel.testing
+_platform_common = _builtins.toplevel.platform_common
+_coverage_common = _builtins.toplevel.coverage_common
+_py_builtins = _builtins.internal.py_builtins
+PackageSpecificationInfo = _builtins.toplevel.PackageSpecificationInfo
+
+TOOLCHAIN_TYPE = "@" + TOOLS_REPO + "//tools/python:toolchain_type"
+
+# Extensions without the dot
+_PYTHON_SOURCE_EXTENSIONS = ["py"]
+
+# NOTE: Must stay in sync with the value used in rules_python
+_MIGRATION_TAG = "__PYTHON_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__"
+
+def create_binary_semantics_struct(
+        *,
+        create_executable,
+        get_cc_details_for_binary,
+        get_central_uncachable_version_file,
+        get_coverage_deps,
+        get_debugger_deps,
+        get_extra_common_runfiles_for_binary,
+        get_extra_providers,
+        get_extra_write_build_data_env,
+        get_interpreter_path,
+        get_imports,
+        get_native_deps_dso_name,
+        get_native_deps_user_link_flags,
+        get_stamp_flag,
+        maybe_precompile,
+        should_build_native_deps_dso,
+        should_create_init_files,
+        should_include_build_data):
+    """Helper to ensure a semantics struct has all necessary fields.
+
+    Call this instead of a raw call to `struct(...)`; it'll help ensure all
+    the necessary functions are being correctly provided.
+
+    Args:
+        create_executable: Callable; creates a binary's executable output. See
+            py_executable.bzl#py_executable_base_impl for details.
+        get_cc_details_for_binary: Callable that returns a `CcDetails` struct; see
+            `create_cc_detail_struct`.
+        get_central_uncachable_version_file: Callable that returns an optional
+            Artifact; this artifact is special: it is never cached and is a copy
+            of `ctx.version_file`; see py_builtins.copy_without_caching
+        get_coverage_deps: Callable that returns a list of Targets for making
+            coverage work; only called if coverage is enabled.
+        get_debugger_deps: Callable that returns a list of Targets that provide
+            custom debugger support; only called for target-configuration.
+        get_extra_common_runfiles_for_binary: Callable that returns a runfiles
+            object of extra runfiles a binary should include.
+        get_extra_providers: Callable that returns extra providers; see
+            py_executable.bzl#_create_providers for details.
+        get_extra_write_build_data_env: Callable that returns a dict[str, str]
+            of additional environment variable to pass to build data generation.
+        get_interpreter_path: Callable that returns an optional string, which is
+            the path to the Python interpreter to use for running the binary.
+        get_imports: Callable that returns a list of the target's import
+            paths (from the `imports` attribute, so just the target's own import
+            path strings, not from dependencies).
+        get_native_deps_dso_name: Callable that returns a string, which is the
+            basename (with extension) of the native deps DSO library.
+        get_native_deps_user_link_flags: Callable that returns a list of strings,
+            which are any extra linker flags to pass onto the native deps DSO
+            linking action.
+        get_stamp_flag: Callable that returns bool of if the --stamp flag was
+            enabled or not.
+        maybe_precompile: Callable that may optional precompile the input `.py`
+            sources and returns the full set of desired outputs derived from
+            the source files (e.g., both py and pyc, only one of them, etc).
+        should_build_native_deps_dso: Callable that returns bool; True if
+            building a native deps DSO is supported, False if not.
+        should_create_init_files: Callable that returns bool; True if
+            `__init__.py` files should be generated, False if not.
+        should_include_build_data: Callable that returns bool; True if
+            build data should be generated, False if not.
+    Returns:
+        A "BinarySemantics" struct.
+    """
+    return struct(
+        # keep-sorted
+        create_executable = create_executable,
+        get_cc_details_for_binary = get_cc_details_for_binary,
+        get_central_uncachable_version_file = get_central_uncachable_version_file,
+        get_coverage_deps = get_coverage_deps,
+        get_debugger_deps = get_debugger_deps,
+        get_extra_common_runfiles_for_binary = get_extra_common_runfiles_for_binary,
+        get_extra_providers = get_extra_providers,
+        get_extra_write_build_data_env = get_extra_write_build_data_env,
+        get_imports = get_imports,
+        get_interpreter_path = get_interpreter_path,
+        get_native_deps_dso_name = get_native_deps_dso_name,
+        get_native_deps_user_link_flags = get_native_deps_user_link_flags,
+        get_stamp_flag = get_stamp_flag,
+        maybe_precompile = maybe_precompile,
+        should_build_native_deps_dso = should_build_native_deps_dso,
+        should_create_init_files = should_create_init_files,
+        should_include_build_data = should_include_build_data,
+    )
+
+def create_library_semantics_struct(
+        *,
+        get_cc_info_for_library,
+        get_imports,
+        maybe_precompile):
+    """Create a `LibrarySemantics` struct.
+
+    Call this instead of a raw call to `struct(...)`; it'll help ensure all
+    the necessary functions are being correctly provided.
+
+    Args:
+        get_cc_info_for_library: Callable that returns a CcInfo for the library;
+            see py_library_impl for arg details.
+        get_imports: Callable; see create_binary_semantics_struct.
+        maybe_precompile: Callable; see create_binary_semantics_struct.
+    Returns:
+        a `LibrarySemantics` struct.
+    """
+    return struct(
+        # keep sorted
+        get_cc_info_for_library = get_cc_info_for_library,
+        get_imports = get_imports,
+        maybe_precompile = maybe_precompile,
+    )
+
+def create_cc_details_struct(
+        *,
+        cc_info_for_propagating,
+        cc_info_for_self_link,
+        cc_info_with_extra_link_time_libraries,
+        extra_runfiles,
+        cc_toolchain):
+    """Creates a CcDetails struct.
+
+    Args:
+        cc_info_for_propagating: CcInfo that is propagated out of the target
+            by returning it within a PyCcLinkParamsProvider object.
+        cc_info_for_self_link: CcInfo that is used when linking for the
+            binary (or its native deps DSO) itself. This may include extra
+            information that isn't propagating (e.g. a custom malloc)
+        cc_info_with_extra_link_time_libraries: CcInfo of extra link time
+            libraries that MUST come after `cc_info_for_self_link` (or possibly
+            always last; not entirely clear) when passed to
+            `link.linking_contexts`.
+        extra_runfiles: runfiles of extra files needed at runtime, usually as
+            part of `cc_info_with_extra_link_time_libraries`; should be added to
+            runfiles.
+        cc_toolchain: CcToolchain that should be used when building.
+
+    Returns:
+        A `CcDetails` struct.
+    """
+    return struct(
+        cc_info_for_propagating = cc_info_for_propagating,
+        cc_info_for_self_link = cc_info_for_self_link,
+        cc_info_with_extra_link_time_libraries = cc_info_with_extra_link_time_libraries,
+        extra_runfiles = extra_runfiles,
+        cc_toolchain = cc_toolchain,
+    )
+
+def create_executable_result_struct(*, extra_files_to_build, output_groups):
+    """Creates a `CreateExecutableResult` struct.
+
+    This is the return value type of the semantics create_executable function.
+
+    Args:
+        extra_files_to_build: depset of File; additional files that should be
+            included as default outputs.
+        output_groups: dict[str, depset[File]]; additional output groups that
+            should be returned.
+
+    Returns:
+        A `CreateExecutableResult` struct.
+    """
+    return struct(
+        extra_files_to_build = extra_files_to_build,
+        output_groups = output_groups,
+    )
+
+def union_attrs(*attr_dicts, allow_none = False):
+    """Helper for combining and building attriute dicts for rules.
+
+    Similar to dict.update, except:
+      * Duplicate keys raise an error if they aren't equal. This is to prevent
+        unintentionally replacing an attribute with a potentially incompatible
+        definition.
+      * None values are special: They mean the attribute is required, but the
+        value should be provided by another attribute dict (depending on the
+        `allow_none` arg).
+    Args:
+        *attr_dicts: The dicts to combine.
+        allow_none: bool, if True, then None values are allowed. If False,
+            then one of `attrs_dicts` must set a non-None value for keys
+            with a None value.
+
+    Returns:
+        dict of attributes.
+    """
+    result = {}
+    missing = {}
+    for attr_dict in attr_dicts:
+        for attr_name, value in attr_dict.items():
+            if value == None and not allow_none:
+                if attr_name not in result:
+                    missing[attr_name] = None
+            else:
+                if attr_name in missing:
+                    missing.pop(attr_name)
+
+                if attr_name not in result or result[attr_name] == None:
+                    result[attr_name] = value
+                elif value != None and result[attr_name] != value:
+                    fail("Duplicate attribute name: '{}': existing={}, new={}".format(
+                        attr_name,
+                        result[attr_name],
+                        value,
+                    ))
+
+                # Else, they're equal, so do nothing. This allows merging dicts
+                # that both define the same key from a common place.
+
+    if missing and not allow_none:
+        fail("Required attributes missing: " + csv(missing.keys()))
+    return result
+
+def csv(values):
+    """Convert a list of strings to comma separated value string."""
+    return ", ".join(sorted(values))
+
+def filter_to_py_srcs(srcs):
+    """Filters .py files from the given list of files"""
+
+    # TODO(b/203567235): Get the set of recognized extensions from
+    # elsewhere, as there may be others. e.g. Bazel recognizes .py3
+    # as a valid extension.
+    return [f for f in srcs if f.extension == "py"]
+
+def collect_imports(ctx, semantics):
+    return depset(direct = semantics.get_imports(ctx), transitive = [
+        dep[PyInfo].imports
+        for dep in ctx.attr.deps
+        if PyInfo in dep
+    ])
+
+def collect_runfiles(ctx, files):
+    """Collects the necessary files from the rule's context.
+
+    This presumes the ctx is for a py_binary, py_test, or py_library rule.
+
+    Args:
+        ctx: rule ctx
+        files: depset of extra files to include in the runfiles.
+    Returns:
+        runfiles necessary for the ctx's target.
+    """
+    return ctx.runfiles(
+        transitive_files = files,
+        # This little arg carries a lot of weight, but because Starlark doesn't
+        # have a way to identify if a target is just a File, the equivalent
+        # logic can't be re-implemented in pure-Starlark.
+        #
+        # Under the hood, it calls the Java `Runfiles#addRunfiles(ctx,
+        # DEFAULT_RUNFILES)` method, which is the what the Java implementation
+        # of the Python rules originally did, and the details of how that method
+        # works have become relied on in various ways. Specifically, what it
+        # does is visit the srcs, deps, and data attributes in the following
+        # ways:
+        #
+        # For each target in the "data" attribute...
+        #   If the target is a File, then add that file to the runfiles.
+        #   Otherwise, add the target's **data runfiles** to the runfiles.
+        #
+        # Note that, contray to best practice, the default outputs of the
+        # targets in `data` are *not* added, nor are the default runfiles.
+        #
+        # This ends up being important for several reasons, some of which are
+        # specific to Google-internal features of the rules.
+        #   * For Python executables, we have to use `data_runfiles` to avoid
+        #     conflicts for the build data files. Such files have
+        #     target-specific content, but uses a fixed location, so if a
+        #     binary has another binary in `data`, and both try to specify a
+        #     file for that file path, then a warning is printed and an
+        #     arbitrary one will be used.
+        #   * For rules with _entirely_ different sets of files in data runfiles
+        #     vs default runfiles vs default outputs. For example,
+        #     proto_library: documented behavior of this rule is that putting it
+        #     in the `data` attribute will cause the transitive closure of
+        #     `.proto` source files to be included. This set of sources is only
+        #     in the `data_runfiles` (`default_runfiles` is empty).
+        #   * For rules with a _subset_ of files in data runfiles. For example,
+        #     a certain Google rule used for packaging arbitrary binaries will
+        #     generate multiple versions of a binary (e.g. different archs,
+        #     stripped vs un-stripped, etc) in its default outputs, but only
+        #     one of them in the runfiles; this helps avoid large, unused
+        #     binaries contributing to remote executor input limits.
+        #
+        # Unfortunately, the above behavior also results in surprising behavior
+        # in some cases. For example, simple custom rules that only return their
+        # files in their default outputs won't have their files included. Such
+        # cases must either return their files in runfiles, or use `filegroup()`
+        # which will do so for them.
+        #
+        # For each target in "srcs" and "deps"...
+        #   Add the default runfiles of the target to the runfiles. While this
+        #   is desirable behavior, it also ends up letting a `py_library`
+        #   be put in `srcs` and still mostly work.
+        # TODO(b/224640180): Reject py_library et al rules in srcs.
+        collect_default = True,
+    )
+
+def create_py_info(ctx, *, direct_sources, 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.
+        imports: depset of strings; the import path values to propagate.
+
+    Returns:
+        A tuple of the PyInfo instance and a depset of the
+        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
+    for target in ctx.attr.deps:
+        # PyInfo may not be present for e.g. cc_library rules.
+        if PyInfo in target:
+            info = target[PyInfo]
+            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
+        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,
+    )
+
+    # 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:
+        # 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:
+                info = target[PyInfo]
+                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:
+                        break
+            if uses_shared_libraries:
+                break
+
+    # 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(
+        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,
+    )
+    return py_info, deps_transitive_sources
+
+def create_instrumented_files_info(ctx):
+    return _coverage_common.instrumented_files_info(
+        ctx,
+        source_attributes = ["srcs"],
+        dependency_attributes = ["deps", "data"],
+        extensions = _PYTHON_SOURCE_EXTENSIONS,
+    )
+
+def create_output_group_info(transitive_sources, extra_groups):
+    return OutputGroupInfo(
+        compilation_prerequisites_INTERNAL_ = transitive_sources,
+        compilation_outputs = transitive_sources,
+        **extra_groups
+    )
+
+def maybe_add_test_execution_info(providers, ctx):
+    """Adds ExecutionInfo, if necessary for proper test execution.
+
+    Args:
+        providers: Mutable list of providers; may have ExecutionInfo
+            provider appended.
+        ctx: Rule ctx.
+    """
+
+    # When built for Apple platforms, require the execution to be on a Mac.
+    # TODO(b/176993122): Remove when bazel automatically knows to run on darwin.
+    if target_platform_has_any_constraint(ctx, ctx.attr._apple_constraints):
+        providers.append(_testing.ExecutionInfo({"requires-darwin": ""}))
+
+_BOOL_TYPE = type(True)
+
+def is_bool(v):
+    return type(v) == _BOOL_TYPE
+
+def target_platform_has_any_constraint(ctx, constraints):
+    """Check if target platform has any of a list of constraints.
+
+    Args:
+      ctx: rule context.
+      constraints: label_list of constraints.
+
+    Returns:
+      True if target platform has at least one of the constraints.
+    """
+    for constraint in constraints:
+        constraint_value = constraint[_platform_common.ConstraintValueInfo]
+        if ctx.target_platform_has_constraint(constraint_value):
+            return True
+    return False
+
+def check_native_allowed(ctx):
+    """Check if the usage of the native rule is allowed.
+
+    Args:
+        ctx: rule context to check
+    """
+    if not ctx.fragments.py.disallow_native_rules:
+        return
+
+    if _MIGRATION_TAG in ctx.attr.tags:
+        return
+
+    # NOTE: The main repo name is empty in *labels*, but not in
+    # ctx.workspace_name
+    is_main_repo = not bool(ctx.label.workspace_name)
+    if is_main_repo:
+        check_label = ctx.label
+    else:
+        # package_group doesn't allow @repo syntax, so we work around that
+        # by prefixing external repos with a fake package path. This also
+        # makes it easy to enable or disable all external repos.
+        check_label = Label("@//__EXTERNAL_REPOS__/{workspace}/{package}".format(
+            workspace = ctx.label.workspace_name,
+            package = ctx.label.package,
+        ))
+    allowlist = ctx.attr._native_rules_allowlist
+    if allowlist:
+        allowed = ctx.attr._native_rules_allowlist[PackageSpecificationInfo].contains(check_label)
+        allowlist_help = str(allowlist.label).replace("@//", "//")
+    else:
+        allowed = False
+        allowlist_help = ("no allowlist specified; all disallowed; specify one " +
+                          "with --python_native_rules_allowlist")
+    if not allowed:
+        if ctx.attr.generator_function:
+            generator = "{generator_function}(name={generator_name}) in {generator_location}".format(
+                generator_function = ctx.attr.generator_function,
+                generator_name = ctx.attr.generator_name,
+                generator_location = ctx.attr.generator_location,
+            )
+        else:
+            generator = "No generator (called directly in BUILD file)"
+
+        msg = (
+            "{target} not allowed to use native.{rule}\n" +
+            "Generated by: {generator}\n" +
+            "Allowlist: {allowlist}\n" +
+            "Migrate to using @rules_python, see {help_url}\n" +
+            "FIXCMD: {fix_cmd} --target={target} --rule={rule} " +
+            "--generator_name={generator_name} --location={generator_location}"
+        )
+        fail(msg.format(
+            target = str(ctx.label).replace("@//", "//"),
+            rule = _py_builtins.get_rule_name(ctx),
+            generator = generator,
+            allowlist = allowlist_help,
+            generator_name = ctx.attr.generator_name,
+            generator_location = ctx.attr.generator_location,
+            help_url = NATIVE_RULES_MIGRATION_HELP_URL,
+            fix_cmd = NATIVE_RULES_MIGRATION_FIX_CMD,
+        ))
diff --git a/python/private/common/common_bazel.bzl b/python/private/common/common_bazel.bzl
new file mode 100644
index 0000000..51b06fb
--- /dev/null
+++ b/python/private/common/common_bazel.bzl
@@ -0,0 +1,104 @@
+# 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.
+"""Common functions that are specific to Bazel rule implementation"""
+
+load(":common/cc/cc_common.bzl", _cc_common = "cc_common")
+load(":common/cc/cc_info.bzl", _CcInfo = "CcInfo")
+load(":common/paths.bzl", "paths")
+load(":common/python/common.bzl", "is_bool")
+load(":common/python/providers.bzl", "PyCcLinkParamsProvider")
+
+_py_builtins = _builtins.internal.py_builtins
+
+def collect_cc_info(ctx, extra_deps = []):
+    """Collect C++ information from dependencies for Bazel.
+
+    Args:
+        ctx: Rule ctx; must have `deps` attribute.
+        extra_deps: list of Target to also collect C+ information from.
+
+    Returns:
+        CcInfo provider of merged information.
+    """
+    deps = ctx.attr.deps
+    if extra_deps:
+        deps = list(deps)
+        deps.extend(extra_deps)
+    cc_infos = []
+    for dep in deps:
+        if _CcInfo in dep:
+            cc_infos.append(dep[_CcInfo])
+
+        if PyCcLinkParamsProvider in dep:
+            cc_infos.append(dep[PyCcLinkParamsProvider].cc_info)
+
+    return _cc_common.merge_cc_infos(cc_infos = cc_infos)
+
+def maybe_precompile(ctx, srcs):
+    """Computes all the outputs (maybe precompiled) from the input srcs.
+
+    See create_binary_semantics_struct for details about this function.
+
+    Args:
+        ctx: Rule ctx.
+        srcs: List of Files; the inputs to maybe precompile.
+
+    Returns:
+        List of Files; the desired output files derived from the input sources.
+    """
+    _ = ctx  # @unused
+
+    # Precompilation isn't implemented yet, so just return srcs as-is
+    return srcs
+
+def get_imports(ctx):
+    """Gets the imports from a rule's `imports` attribute.
+
+    See create_binary_semantics_struct for details about this function.
+
+    Args:
+        ctx: Rule ctx.
+
+    Returns:
+        List of strings.
+    """
+    prefix = "{}/{}".format(
+        ctx.workspace_name,
+        _py_builtins.get_label_repo_runfiles_path(ctx.label),
+    )
+    result = []
+    for import_str in ctx.attr.imports:
+        import_str = ctx.expand_make_variables("imports", import_str, {})
+        if import_str.startswith("/"):
+            continue
+
+        # To prevent "escaping" out of the runfiles tree, we normalize
+        # the path and ensure it doesn't have up-level references.
+        import_path = paths.normalize("{}/{}".format(prefix, import_str))
+        if import_path.startswith("../") or import_path == "..":
+            fail("Path '{}' references a path above the execution root".format(
+                import_str,
+            ))
+        result.append(import_path)
+    return result
+
+def convert_legacy_create_init_to_int(kwargs):
+    """Convert "legacy_create_init" key to int, in-place.
+
+    Args:
+        kwargs: The kwargs to modify. The key "legacy_create_init", if present
+            and bool, will be converted to its integer value, in place.
+    """
+    if is_bool(kwargs.get("legacy_create_init")):
+        kwargs["legacy_create_init"] = 1 if kwargs["legacy_create_init"] else 0
diff --git a/python/private/common/providers.bzl b/python/private/common/providers.bzl
new file mode 100644
index 0000000..a9df61b
--- /dev/null
+++ b/python/private/common/providers.bzl
@@ -0,0 +1,212 @@
+# 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.
+"""Providers for Python rules."""
+
+load(":common/python/semantics.bzl", "TOOLS_REPO")
+
+_CcInfo = _builtins.toplevel.CcInfo
+
+# NOTE: This is copied to PyRuntimeInfo.java
+DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3"
+
+# NOTE: This is copied to PyRuntimeInfo.java
+DEFAULT_BOOTSTRAP_TEMPLATE = "@" + TOOLS_REPO + "//tools/python:python_bootstrap_template.txt"
+_PYTHON_VERSION_VALUES = ["PY2", "PY3"]
+
+def _PyRuntimeInfo_init(
+        *,
+        interpreter_path = None,
+        interpreter = None,
+        files = None,
+        coverage_tool = None,
+        coverage_files = None,
+        python_version,
+        stub_shebang = None,
+        bootstrap_template = None):
+    if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
+        fail("exactly one of interpreter or interpreter_path must be specified")
+
+    if interpreter_path and files != None:
+        fail("cannot specify 'files' if 'interpreter_path' is given")
+
+    if (coverage_tool and not coverage_files) or (not coverage_tool and coverage_files):
+        fail(
+            "coverage_tool and coverage_files must both be set or neither must be set, " +
+            "got coverage_tool={}, coverage_files={}".format(
+                coverage_tool,
+                coverage_files,
+            ),
+        )
+
+    if python_version not in _PYTHON_VERSION_VALUES:
+        fail("invalid python_version: '{}'; must be one of {}".format(
+            python_version,
+            _PYTHON_VERSION_VALUES,
+        ))
+
+    if files != None and type(files) != type(depset()):
+        fail("invalid files: got value of type {}, want depset".format(type(files)))
+
+    if interpreter:
+        if files == None:
+            files = depset()
+    else:
+        files = None
+
+    if coverage_files == None:
+        coverage_files = depset()
+
+    if not stub_shebang:
+        stub_shebang = DEFAULT_STUB_SHEBANG
+
+    return {
+        "bootstrap_template": bootstrap_template,
+        "coverage_files": coverage_files,
+        "coverage_tool": coverage_tool,
+        "files": files,
+        "interpreter": interpreter,
+        "interpreter_path": interpreter_path,
+        "python_version": python_version,
+        "stub_shebang": stub_shebang,
+    }
+
+# 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 = provider(
+    doc = """Contains information about a Python runtime, as returned by the `py_runtime`
+rule.
+
+A Python runtime describes 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 a `File` that acts as the interpreter. In
+both cases, an "interpreter" is really 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.
+""",
+    init = _PyRuntimeInfo_init,
+    fields = {
+        "bootstrap_template": (
+            "See py_runtime_rule.bzl%py_runtime.bootstrap_template for docs."
+        ),
+        "coverage_files": (
+            "The files required at runtime for using `coverage_tool`. " +
+            "Will be `None` if no `coverage_tool` was provided."
+        ),
+        "coverage_tool": (
+            "If set, this field is a `File` representing tool used for collecting code coverage information from python tests. Otherwise, this is `None`."
+        ),
+        "files": (
+            "If this is an in-build runtime, this field is a `depset` of `File`s" +
+            "that need to be added to the runfiles of an executable target that " +
+            "uses this runtime (in particular, files needed by `interpreter`). " +
+            "The value of `interpreter` need not be included in this field. If " +
+            "this is a platform runtime then this field is `None`."
+        ),
+        "interpreter": (
+            "If this is an in-build runtime, this field is a `File` representing " +
+            "the interpreter. Otherwise, this is `None`. Note that an in-build " +
+            "runtime can use either a prebuilt, checked-in interpreter or an " +
+            "interpreter built from source."
+        ),
+        "interpreter_path": (
+            "If this is a platform runtime, this field is the absolute " +
+            "filesystem path to the interpreter on the target platform. " +
+            "Otherwise, this is `None`."
+        ),
+        "python_version": (
+            "Indicates whether this runtime uses Python major version 2 or 3. " +
+            "Valid values are (only) `\"PY2\"` and " +
+            "`\"PY3\"`."
+        ),
+        "stub_shebang": (
+            "\"Shebang\" expression prepended to the bootstrapping Python stub " +
+            "script used when executing `py_binary` targets.  Does not " +
+            "apply to Windows."
+        ),
+    },
+)
+
+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):
+    _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)
+    return {
+        "has_py2_only_sources": has_py2_only_sources,
+        "has_py3_only_sources": has_py2_only_sources,
+        "imports": imports,
+        "transitive_sources": transitive_sources,
+        "uses_shared_libraries": uses_shared_libraries,
+    }
+
+PyInfo, _unused_raw_py_info_ctor = provider(
+    "Encapsulates information provided by the Python rules.",
+    init = _PyInfo_init,
+    fields = {
+        "has_py2_only_sources": "Whether any of this target's transitive sources requires a Python 2 runtime.",
+        "has_py3_only_sources": "Whether any of this target's transitive sources requires a Python 3 runtime.",
+        "imports": """\
+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_sources": """\
+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": """
+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 = provider(
+    doc = ("Python-wrapper to forward 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": "A CcInfo instance; it has only linking_context set",
+    },
+)
diff --git a/python/private/common/py_binary_bazel.bzl b/python/private/common/py_binary_bazel.bzl
new file mode 100644
index 0000000..3a5df73
--- /dev/null
+++ b/python/private/common/py_binary_bazel.bzl
@@ -0,0 +1,48 @@
+# 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(":common/python/attributes.bzl", "AGNOSTIC_BINARY_ATTRS")
+load(
+    ":common/python/py_executable_bazel.bzl",
+    "create_executable_rule",
+    "py_executable_bazel_impl",
+)
+load(":common/python/semantics.bzl", "TOOLS_REPO")
+
+_PY_TEST_ATTRS = {
+    "_collect_cc_coverage": attr.label(
+        default = "@" + TOOLS_REPO + "//tools/test:collect_cc_coverage",
+        executable = True,
+        cfg = "exec",
+    ),
+    "_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 = AGNOSTIC_BINARY_ATTRS | _PY_TEST_ATTRS,
+    executable = True,
+)
diff --git a/python/private/common/py_binary_macro.bzl b/python/private/common/py_binary_macro.bzl
new file mode 100644
index 0000000..24e5c6d
--- /dev/null
+++ b/python/private/common/py_binary_macro.bzl
@@ -0,0 +1,21 @@
+# 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 macro-half of py_binary rule."""
+
+load(":common/python/common_bazel.bzl", "convert_legacy_create_init_to_int")
+load(":common/python/py_binary_bazel.bzl", py_binary_rule = "py_binary")
+
+def py_binary(**kwargs):
+    convert_legacy_create_init_to_int(kwargs)
+    py_binary_rule(**kwargs)
diff --git a/python/private/common/py_executable.bzl b/python/private/common/py_executable.bzl
new file mode 100644
index 0000000..9db92b1
--- /dev/null
+++ b/python/private/common/py_executable.bzl
@@ -0,0 +1,845 @@
+# 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.
+"""Common functionality between test/binary executables."""
+
+load(":common/cc/cc_common.bzl", _cc_common = "cc_common")
+load(":common/cc/cc_helper.bzl", "cc_helper")
+load(
+    ":common/python/attributes.bzl",
+    "AGNOSTIC_EXECUTABLE_ATTRS",
+    "COMMON_ATTRS",
+    "PY_SRCS_ATTRS",
+    "SRCS_VERSION_ALL_VALUES",
+    "create_srcs_attr",
+    "create_srcs_version_attr",
+)
+load(
+    ":common/python/common.bzl",
+    "TOOLCHAIN_TYPE",
+    "check_native_allowed",
+    "collect_imports",
+    "collect_runfiles",
+    "create_instrumented_files_info",
+    "create_output_group_info",
+    "create_py_info",
+    "csv",
+    "filter_to_py_srcs",
+    "union_attrs",
+)
+load(
+    ":common/python/providers.bzl",
+    "PyCcLinkParamsProvider",
+    "PyRuntimeInfo",
+)
+load(
+    ":common/python/semantics.bzl",
+    "ALLOWED_MAIN_EXTENSIONS",
+    "BUILD_DATA_SYMLINK_PATH",
+    "IS_BAZEL",
+    "PY_RUNTIME_ATTR_NAME",
+)
+
+_py_builtins = _builtins.internal.py_builtins
+
+# Non-Google-specific attributes for executables
+# These attributes are for rules that accept Python sources.
+EXECUTABLE_ATTRS = union_attrs(
+    COMMON_ATTRS,
+    AGNOSTIC_EXECUTABLE_ATTRS,
+    PY_SRCS_ATTRS,
+    {
+        # TODO(b/203567235): In the Java impl, any file is allowed. While marked
+        # label, it is more treated as a string, and doesn't have to refer to
+        # anything that exists because it gets treated as suffix-search string
+        # over `srcs`.
+        "main": attr.label(
+            allow_single_file = True,
+            doc = """\
+Optional; the name of the source file that is the main entry point of the
+application. This file must also be listed in `srcs`. If left unspecified,
+`name`, with `.py` appended, is used instead. If `name` does not match any
+filename in `srcs`, `main` must be specified.
+""",
+        ),
+        # TODO(b/203567235): In Google, this attribute is deprecated, and can
+        # only effectively be PY3. Externally, with Bazel, this attribute has
+        # a separate story.
+        "python_version": attr.string(
+            # TODO(b/203567235): In the Java impl, the default comes from
+            # --python_version. Not clear what the Starlark equivalent is.
+            default = "PY3",
+            # NOTE: Some tests care about the order of these values.
+            values = ["PY2", "PY3"],
+        ),
+    },
+    create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES),
+    create_srcs_attr(mandatory = True),
+    allow_none = True,
+)
+
+def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = []):
+    """Base rule implementation for a Python executable.
+
+    Google and Bazel call this common base and apply customizations using the
+    semantics object.
+
+    Args:
+        ctx: The rule ctx
+        semantics: BinarySemantics struct; see create_binary_semantics_struct()
+        is_test: bool, True if the rule is a test rule (has `test=True`),
+            False if not (has `executable=True`)
+        inherited_environment: List of str; additional environment variable
+            names that should be inherited from the runtime environment when the
+            executable is run.
+    Returns:
+        DefaultInfo provider for the executable
+    """
+    _validate_executable(ctx)
+
+    main_py = determine_main(ctx)
+    direct_sources = filter_to_py_srcs(ctx.files.srcs)
+    output_sources = semantics.maybe_precompile(ctx, direct_sources)
+    imports = collect_imports(ctx, semantics)
+    executable, files_to_build = _compute_outputs(ctx, output_sources)
+
+    runtime_details = _get_runtime_details(ctx, semantics)
+    if ctx.configuration.coverage_enabled:
+        extra_deps = semantics.get_coverage_deps(ctx, runtime_details)
+    else:
+        extra_deps = []
+
+    # The debugger dependency should be prevented by select() config elsewhere,
+    # but just to be safe, also guard against adding it to the output here.
+    if not _is_tool_config(ctx):
+        extra_deps.extend(semantics.get_debugger_deps(ctx, runtime_details))
+
+    cc_details = semantics.get_cc_details_for_binary(ctx, extra_deps = extra_deps)
+    native_deps_details = _get_native_deps_details(
+        ctx,
+        semantics = semantics,
+        cc_details = cc_details,
+        is_test = is_test,
+    )
+    runfiles_details = _get_base_runfiles_for_binary(
+        ctx,
+        executable = executable,
+        extra_deps = extra_deps,
+        files_to_build = files_to_build,
+        extra_common_runfiles = [
+            runtime_details.runfiles,
+            cc_details.extra_runfiles,
+            native_deps_details.runfiles,
+            semantics.get_extra_common_runfiles_for_binary(ctx),
+        ],
+        semantics = semantics,
+    )
+    exec_result = semantics.create_executable(
+        ctx,
+        executable = executable,
+        main_py = main_py,
+        imports = imports,
+        is_test = is_test,
+        runtime_details = runtime_details,
+        cc_details = cc_details,
+        native_deps_details = native_deps_details,
+        runfiles_details = runfiles_details,
+    )
+    files_to_build = depset(transitive = [
+        exec_result.extra_files_to_build,
+        files_to_build,
+    ])
+    extra_exec_runfiles = ctx.runfiles(transitive_files = files_to_build)
+    runfiles_details = struct(
+        default_runfiles = runfiles_details.default_runfiles.merge(extra_exec_runfiles),
+        data_runfiles = runfiles_details.data_runfiles.merge(extra_exec_runfiles),
+    )
+
+    legacy_providers, modern_providers = _create_providers(
+        ctx = ctx,
+        executable = executable,
+        runfiles_details = runfiles_details,
+        main_py = main_py,
+        imports = imports,
+        direct_sources = direct_sources,
+        files_to_build = files_to_build,
+        runtime_details = runtime_details,
+        cc_info = cc_details.cc_info_for_propagating,
+        inherited_environment = inherited_environment,
+        semantics = semantics,
+        output_groups = exec_result.output_groups,
+    )
+    return struct(
+        legacy_providers = legacy_providers,
+        providers = modern_providers,
+    )
+
+def _validate_executable(ctx):
+    if ctx.attr.python_version != "PY3":
+        fail("It is not allowed to use Python 2")
+    check_native_allowed(ctx)
+
+def _compute_outputs(ctx, output_sources):
+    # TODO: This should use the configuration instead of the Bazel OS.
+    if _py_builtins.get_current_os_name() == "windows":
+        executable = ctx.actions.declare_file(ctx.label.name + ".exe")
+    else:
+        executable = ctx.actions.declare_file(ctx.label.name)
+
+    # TODO(b/208657718): Remove output_sources from the default outputs
+    # once the depot is cleaned up.
+    return executable, depset([executable] + output_sources)
+
+def _get_runtime_details(ctx, semantics):
+    """Gets various information about the Python runtime to use.
+
+    While most information comes from the toolchain, various legacy and
+    compatibility behaviors require computing some other information.
+
+    Args:
+        ctx: Rule ctx
+        semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`
+
+    Returns:
+        A struct; see inline-field comments of the return value for details.
+    """
+
+    # Bazel has --python_path. This flag has a computed default of "python" when
+    # its actual default is null (see
+    # BazelPythonConfiguration.java#getPythonPath). This flag is only used if
+    # toolchains are not enabled and `--python_top` isn't set. Note that Google
+    # used to have a variant of this named --python_binary, but it has since
+    # been removed.
+    #
+    # TOOD(bazelbuild/bazel#7901): Remove this once --python_path flag is removed.
+
+    if IS_BAZEL:
+        flag_interpreter_path = ctx.fragments.bazel_py.python_path
+        toolchain_runtime, effective_runtime = _maybe_get_runtime_from_ctx(ctx)
+        if not effective_runtime:
+            # Clear these just in case
+            toolchain_runtime = None
+            effective_runtime = None
+
+    else:  # Google code path
+        flag_interpreter_path = None
+        toolchain_runtime, effective_runtime = _maybe_get_runtime_from_ctx(ctx)
+        if not effective_runtime:
+            fail("Unable to find Python runtime")
+
+    if effective_runtime:
+        direct = []  # List of files
+        transitive = []  # List of depsets
+        if effective_runtime.interpreter:
+            direct.append(effective_runtime.interpreter)
+            transitive.append(effective_runtime.files)
+
+        if ctx.configuration.coverage_enabled:
+            if effective_runtime.coverage_tool:
+                direct.append(effective_runtime.coverage_tool)
+            if effective_runtime.coverage_files:
+                transitive.append(effective_runtime.coverage_files)
+        runtime_files = depset(direct = direct, transitive = transitive)
+    else:
+        runtime_files = depset()
+
+    executable_interpreter_path = semantics.get_interpreter_path(
+        ctx,
+        runtime = effective_runtime,
+        flag_interpreter_path = flag_interpreter_path,
+    )
+
+    return struct(
+        # Optional PyRuntimeInfo: The runtime found from toolchain resolution.
+        # This may be None because, within Google, toolchain resolution isn't
+        # yet enabled.
+        toolchain_runtime = toolchain_runtime,
+        # Optional PyRuntimeInfo: The runtime that should be used. When
+        # toolchain resolution is enabled, this is the same as
+        # `toolchain_resolution`. Otherwise, this probably came from the
+        # `_python_top` attribute that the Google implementation still uses.
+        # This is separate from `toolchain_runtime` because toolchain_runtime
+        # is propagated as a provider, while non-toolchain runtimes are not.
+        effective_runtime = effective_runtime,
+        # str; Path to the Python interpreter to use for running the executable
+        # itself (not the bootstrap script). Either an absolute path (which
+        # means it is platform-specific), or a runfiles-relative path (which
+        # means the interpreter should be within `runtime_files`)
+        executable_interpreter_path = executable_interpreter_path,
+        # runfiles: Additional runfiles specific to the runtime that should
+        # be included. For in-build runtimes, this shold include the interpreter
+        # and any supporting files.
+        runfiles = ctx.runfiles(transitive_files = runtime_files),
+    )
+
+def _maybe_get_runtime_from_ctx(ctx):
+    """Finds the PyRuntimeInfo from the toolchain or attribute, if available.
+
+    Returns:
+        2-tuple of toolchain_runtime, effective_runtime
+    """
+    if ctx.fragments.py.use_toolchains:
+        toolchain = ctx.toolchains[TOOLCHAIN_TYPE]
+
+        if not hasattr(toolchain, "py3_runtime"):
+            fail("Python toolchain field 'py3_runtime' is missing")
+        if not toolchain.py3_runtime:
+            fail("Python toolchain missing py3_runtime")
+        py3_runtime = toolchain.py3_runtime
+
+        # Hack around the fact that the autodetecting Python toolchain, which is
+        # automatically registered, does not yet support Windows. In this case,
+        # we want to return null so that _get_interpreter_path falls back on
+        # --python_path. See tools/python/toolchain.bzl.
+        # TODO(#7844): Remove this hack when the autodetecting toolchain has a
+        # Windows implementation.
+        if py3_runtime.interpreter_path == "/_magic_pyruntime_sentinel_do_not_use":
+            return None, None
+
+        if py3_runtime.python_version != "PY3":
+            fail("Python toolchain py3_runtime must be python_version=PY3, got {}".format(
+                py3_runtime.python_version,
+            ))
+        toolchain_runtime = toolchain.py3_runtime
+        effective_runtime = toolchain_runtime
+    else:
+        toolchain_runtime = None
+        attr_target = getattr(ctx.attr, PY_RUNTIME_ATTR_NAME)
+
+        # In Bazel, --python_top is null by default.
+        if attr_target and PyRuntimeInfo in attr_target:
+            effective_runtime = attr_target[PyRuntimeInfo]
+        else:
+            return None, None
+
+    return toolchain_runtime, effective_runtime
+
+def _get_base_runfiles_for_binary(
+        ctx,
+        *,
+        executable,
+        extra_deps,
+        files_to_build,
+        extra_common_runfiles,
+        semantics):
+    """Returns the set of runfiles necessary prior to executable creation.
+
+    NOTE: The term "common runfiles" refers to the runfiles that both the
+    default and data runfiles have in common.
+
+    Args:
+        ctx: The rule ctx.
+        executable: The main executable output.
+        extra_deps: List of Targets; additional targets whose runfiles
+            will be added to the common runfiles.
+        files_to_build: depset of File of the default outputs to add into runfiles.
+        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`.
+
+    Returns:
+        struct with attributes:
+        * default_runfiles: The default runfiles
+        * data_runfiles: The data runfiles
+    """
+    common_runfiles = collect_runfiles(ctx, depset(
+        direct = [executable],
+        transitive = [files_to_build],
+    ))
+    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)
+
+    if semantics.should_create_init_files(ctx):
+        common_runfiles = _py_builtins.merge_runfiles_with_generated_inits_empty_files_supplier(
+            ctx = ctx,
+            runfiles = common_runfiles,
+        )
+
+    # Don't include build_data.txt in data runfiles. This allows binaries to
+    # contain other binaries while still using the same fixed location symlink
+    # for the build_data.txt file. Really, the fixed location symlink should be
+    # removed and another way found to locate the underlying build data file.
+    data_runfiles = common_runfiles
+
+    if is_stamping_enabled(ctx, semantics) and semantics.should_include_build_data(ctx):
+        default_runfiles = common_runfiles.merge(_create_runfiles_with_build_data(
+            ctx,
+            semantics.get_central_uncachable_version_file(ctx),
+            semantics.get_extra_write_build_data_env(ctx),
+        ))
+    else:
+        default_runfiles = common_runfiles
+
+    return struct(
+        default_runfiles = default_runfiles,
+        data_runfiles = data_runfiles,
+    )
+
+def _create_runfiles_with_build_data(
+        ctx,
+        central_uncachable_version_file,
+        extra_write_build_data_env):
+    return ctx.runfiles(
+        symlinks = {
+            BUILD_DATA_SYMLINK_PATH: _write_build_data(
+                ctx,
+                central_uncachable_version_file,
+                extra_write_build_data_env,
+            ),
+        },
+    )
+
+def _write_build_data(ctx, central_uncachable_version_file, extra_write_build_data_env):
+    # TODO: Remove this logic when a central file is always available
+    if not central_uncachable_version_file:
+        version_file = ctx.actions.declare_file(ctx.label.name + "-uncachable_version_file.txt")
+        _py_builtins.copy_without_caching(
+            ctx = ctx,
+            read_from = ctx.version_file,
+            write_to = version_file,
+        )
+    else:
+        version_file = central_uncachable_version_file
+
+    direct_inputs = [ctx.info_file, version_file]
+
+    # A "constant metadata" file is basically a special file that doesn't
+    # support change detection logic and reports that it is unchanged. i.e., it
+    # behaves like ctx.version_file and is ignored when computing "what inputs
+    # changed" (see https://bazel.build/docs/user-manual#workspace-status).
+    #
+    # We do this so that consumers of the final build data file don't have
+    # to transitively rebuild everything -- the `uncachable_version_file` file
+    # isn't cachable, which causes the build data action to always re-run.
+    #
+    # While this technically means a binary could have stale build info,
+    # it ends up not mattering in practice because the volatile information
+    # doesn't meaningfully effect other outputs.
+    #
+    # This is also done for performance and Make It work reasons:
+    #   * Passing the transitive dependencies into the action requires passing
+    #     the runfiles, but actions don't directly accept runfiles. While
+    #     flattening the depsets can be deferred, accessing the
+    #     `runfiles.empty_filenames` attribute will will invoke the empty
+    #     file supplier a second time, which is too much of a memory and CPU
+    #     performance hit.
+    #   * Some targets specify a directory in `data`, which is unsound, but
+    #     mostly works. Google's RBE, unfortunately, rejects it.
+    #   * A binary's transitive closure may be so large that it exceeds
+    #     Google RBE limits for action inputs.
+    build_data = _py_builtins.declare_constant_metadata_file(
+        ctx = ctx,
+        name = ctx.label.name + ".build_data.txt",
+        root = ctx.bin_dir,
+    )
+
+    ctx.actions.run(
+        executable = ctx.executable._build_data_gen,
+        env = {
+            # NOTE: ctx.info_file is undocumented; see
+            # https://github.com/bazelbuild/bazel/issues/9363
+            "INFO_FILE": ctx.info_file.path,
+            "OUTPUT": build_data.path,
+            "PLATFORM": cc_helper.find_cpp_toolchain(ctx).toolchain_id,
+            "TARGET": str(ctx.label),
+            "VERSION_FILE": version_file.path,
+        } | extra_write_build_data_env,
+        inputs = depset(
+            direct = direct_inputs,
+        ),
+        outputs = [build_data],
+        mnemonic = "PyWriteBuildData",
+        progress_message = "Generating %{label} build_data.txt",
+    )
+    return build_data
+
+def _get_native_deps_details(ctx, *, semantics, cc_details, is_test):
+    if not semantics.should_build_native_deps_dso(ctx):
+        return struct(dso = None, runfiles = ctx.runfiles())
+
+    cc_info = cc_details.cc_info_for_self_link
+
+    if not cc_info.linking_context.linker_inputs:
+        return struct(dso = None, runfiles = ctx.runfiles())
+
+    dso = ctx.actions.declare_file(semantics.get_native_deps_dso_name(ctx))
+    share_native_deps = ctx.fragments.cpp.share_native_deps()
+    cc_feature_config = cc_configure_features(
+        ctx,
+        cc_toolchain = cc_details.cc_toolchain,
+        # See b/171276569#comment18: this feature string is just to allow
+        # Google's RBE to know the link action is for the Python case so it can
+        # take special actions (though as of Jun 2022, no special action is
+        # taken).
+        extra_features = ["native_deps_link"],
+    )
+    if share_native_deps:
+        linked_lib = _create_shared_native_deps_dso(
+            ctx,
+            cc_info = cc_info,
+            is_test = is_test,
+            requested_features = cc_feature_config.requested_features,
+            feature_configuration = cc_feature_config.feature_configuration,
+        )
+        ctx.actions.symlink(
+            output = dso,
+            target_file = linked_lib,
+            progress_message = "Symlinking shared native deps for %{label}",
+        )
+    else:
+        linked_lib = dso
+    _cc_common.link(
+        name = ctx.label.name,
+        actions = ctx.actions,
+        linking_contexts = [cc_info.linking_context],
+        output_type = "dynamic_library",
+        never_link = True,
+        native_deps = True,
+        feature_configuration = cc_feature_config.feature_configuration,
+        cc_toolchain = cc_details.cc_toolchain,
+        test_only_target = is_test,
+        stamp = 1 if is_stamping_enabled(ctx, semantics) else 0,
+        main_output = linked_lib,
+        use_shareable_artifact_factory = True,
+        # NOTE: Only flags not captured by cc_info.linking_context need to
+        # be manually passed
+        user_link_flags = semantics.get_native_deps_user_link_flags(ctx),
+    )
+    return struct(
+        dso = dso,
+        runfiles = ctx.runfiles(files = [dso]),
+    )
+
+def _create_shared_native_deps_dso(
+        ctx,
+        *,
+        cc_info,
+        is_test,
+        feature_configuration,
+        requested_features):
+    linkstamps = cc_info.linking_context.linkstamps()
+
+    partially_disabled_thin_lto = (
+        _cc_common.is_enabled(
+            feature_name = "thin_lto_linkstatic_tests_use_shared_nonlto_backends",
+            feature_configuration = feature_configuration,
+        ) and not _cc_common.is_enabled(
+            feature_name = "thin_lto_all_linkstatic_use_shared_nonlto_backends",
+            feature_configuration = feature_configuration,
+        )
+    )
+    dso_hash = _get_shared_native_deps_hash(
+        linker_inputs = cc_helper.get_static_mode_params_for_dynamic_library_libraries(
+            depset([
+                lib
+                for linker_input in cc_info.linking_context.linker_inputs.to_list()
+                for lib in linker_input.libraries
+            ]),
+        ),
+        link_opts = [
+            flag
+            for input in cc_info.linking_context.linker_inputs.to_list()
+            for flag in input.user_link_flags
+        ],
+        linkstamps = [linkstamp.file() for linkstamp in linkstamps.to_list()],
+        build_info_artifacts = _cc_common.get_build_info(ctx) if linkstamps else [],
+        features = requested_features,
+        is_test_target_partially_disabled_thin_lto = is_test and partially_disabled_thin_lto,
+    )
+    return ctx.actions.declare_shareable_artifact("_nativedeps/%x.so" % dso_hash)
+
+# This is a minimal version of NativeDepsHelper.getSharedNativeDepsPath, see
+# com.google.devtools.build.lib.rules.nativedeps.NativeDepsHelper#getSharedNativeDepsPath
+# The basic idea is to take all the inputs that affect linking and encode (via
+# hashing) them into the filename.
+# TODO(b/234232820): The settings that affect linking must be kept in sync with the actual
+# C++ link action. For more information, see the large descriptive comment on
+# NativeDepsHelper#getSharedNativeDepsPath.
+def _get_shared_native_deps_hash(
+        *,
+        linker_inputs,
+        link_opts,
+        linkstamps,
+        build_info_artifacts,
+        features,
+        is_test_target_partially_disabled_thin_lto):
+    # NOTE: We use short_path because the build configuration root in which
+    # files are always created already captures the configuration-specific
+    # parts, so no need to include them manually.
+    parts = []
+    for artifact in linker_inputs:
+        parts.append(artifact.short_path)
+    parts.append(str(len(link_opts)))
+    parts.extend(link_opts)
+    for artifact in linkstamps:
+        parts.append(artifact.short_path)
+    for artifact in build_info_artifacts:
+        parts.append(artifact.short_path)
+    parts.extend(sorted(features))
+
+    # Sharing of native dependencies may cause an {@link
+    # ActionConflictException} when ThinLTO is disabled for test and test-only
+    # targets that are statically linked, but enabled for other statically
+    # linked targets. This happens in case the artifacts for the shared native
+    # dependency are output by {@link Action}s owned by the non-test and test
+    # targets both. To fix this, we allow creation of multiple artifacts for the
+    # shared native library - one shared among the test and test-only targets
+    # where ThinLTO is disabled, and the other shared among other targets where
+    # ThinLTO is enabled. See b/138118275
+    parts.append("1" if is_test_target_partially_disabled_thin_lto else "0")
+
+    return hash("".join(parts))
+
+def determine_main(ctx):
+    """Determine the main entry point .py source file.
+
+    Args:
+        ctx: The rule ctx.
+
+    Returns:
+        Artifact; the main file. If one can't be found, an error is raised.
+    """
+    if ctx.attr.main:
+        proposed_main = ctx.attr.main.label.name
+        if not proposed_main.endswith(tuple(ALLOWED_MAIN_EXTENSIONS)):
+            fail("main must end in '.py'")
+    else:
+        if ctx.label.name.endswith(".py"):
+            fail("name must not end in '.py'")
+        proposed_main = ctx.label.name + ".py"
+
+    main_files = [src for src in ctx.files.srcs if _path_endswith(src.short_path, proposed_main)]
+    if not main_files:
+        if ctx.attr.main:
+            fail("could not find '{}' as specified by 'main' attribute".format(proposed_main))
+        else:
+            fail(("corresponding default '{}' does not appear in srcs. Add " +
+                  "it or override default file name with a 'main' attribute").format(
+                proposed_main,
+            ))
+
+    elif len(main_files) > 1:
+        if ctx.attr.main:
+            fail(("file name '{}' specified by 'main' attributes matches multiple files. " +
+                  "Matches: {}").format(
+                proposed_main,
+                csv([f.short_path for f in main_files]),
+            ))
+        else:
+            fail(("default main file '{}' matches multiple files in srcs. Perhaps specify " +
+                  "an explicit file with 'main' attribute? Matches were: {}").format(
+                proposed_main,
+                csv([f.short_path for f in main_files]),
+            ))
+    return main_files[0]
+
+def _path_endswith(path, endswith):
+    # Use slash to anchor each path to prevent e.g.
+    # "ab/c.py".endswith("b/c.py") from incorrectly matching.
+    return ("/" + path).endswith("/" + endswith)
+
+def is_stamping_enabled(ctx, semantics):
+    """Tells if stamping is enabled or not.
+
+    Args:
+        ctx: The rule ctx
+        semantics: a semantics struct (see create_semantics_struct).
+    Returns:
+        bool; True if stamping is enabled, False if not.
+    """
+    if _is_tool_config(ctx):
+        return False
+
+    stamp = ctx.attr.stamp
+    if stamp == 1:
+        return True
+    elif stamp == 0:
+        return False
+    elif stamp == -1:
+        return semantics.get_stamp_flag(ctx)
+    else:
+        fail("Unsupported `stamp` value: {}".format(stamp))
+
+def _is_tool_config(ctx):
+    # NOTE: The is_tool_configuration() function is only usable by builtins.
+    # See https://github.com/bazelbuild/bazel/issues/14444 for the FR for
+    # a more public API. Outside of builtins, ctx.bin_dir.path can be
+    # checked for `/host/` or `-exec-`.
+    return ctx.configuration.is_tool_configuration()
+
+def _create_providers(
+        *,
+        ctx,
+        executable,
+        main_py,
+        direct_sources,
+        files_to_build,
+        runfiles_details,
+        imports,
+        cc_info,
+        inherited_environment,
+        runtime_details,
+        output_groups,
+        semantics):
+    """Creates the providers an executable should return.
+
+    Args:
+        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.
+        files_to_build: 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
+            is propagated, not the whole CcInfo.
+        inherited_environment: list of strings; Environment variable names
+            that should be inherited from the environment the executuble
+            is run within.
+        runtime_details: struct of runtime information; see _get_runtime_details()
+        output_groups: dict[str, depset[File]]; used to create OutputGroupInfo
+        semantics: BinarySemantics struct; see create_binary_semantics()
+
+    Returns:
+        A two-tuple of:
+        1. A dict of legacy providers.
+        2. A list of modern providers.
+    """
+    providers = [
+        DefaultInfo(
+            executable = executable,
+            files = files_to_build,
+            default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles(
+                ctx,
+                runfiles_details.default_runfiles,
+            ),
+            data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles(
+                ctx,
+                runfiles_details.data_runfiles,
+            ),
+        ),
+        create_instrumented_files_info(ctx),
+        _create_run_environment_info(ctx, inherited_environment),
+    ]
+
+    # TODO(b/265840007): Make this non-conditional once Google enables
+    # --incompatible_use_python_toolchains.
+    if runtime_details.toolchain_runtime:
+        providers.append(runtime_details.toolchain_runtime)
+
+    # TODO(b/163083591): Remove the PyCcLinkParamsProvider once binaries-in-deps
+    # are cleaned up.
+    if cc_info:
+        providers.append(
+            PyCcLinkParamsProvider(cc_info = cc_info),
+        )
+
+    py_info, deps_transitive_sources = create_py_info(
+        ctx,
+        direct_sources = depset(direct_sources),
+        imports = imports,
+    )
+
+    # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455
+    listeners_enabled = _py_builtins.are_action_listeners_enabled(ctx)
+    if listeners_enabled:
+        _py_builtins.add_py_extra_pseudo_action(
+            ctx = ctx,
+            dependency_transitive_python_sources = deps_transitive_sources,
+        )
+
+    providers.append(py_info)
+    providers.append(create_output_group_info(py_info.transitive_sources, output_groups))
+
+    extra_legacy_providers, extra_providers = semantics.get_extra_providers(
+        ctx,
+        main_py = main_py,
+        runtime_details = runtime_details,
+    )
+    providers.extend(extra_providers)
+    return extra_legacy_providers, providers
+
+def _create_run_environment_info(ctx, inherited_environment):
+    expanded_env = {}
+    for key, value in ctx.attr.env.items():
+        expanded_env[key] = _py_builtins.expand_location_and_make_variables(
+            ctx = ctx,
+            attribute_name = "env[{}]".format(key),
+            expression = value,
+            targets = ctx.attr.data,
+        )
+    return RunEnvironmentInfo(
+        environment = expanded_env,
+        inherited_environment = inherited_environment,
+    )
+
+def create_base_executable_rule(*, attrs, fragments = [], **kwargs):
+    """Create a function for defining for Python binary/test targets.
+
+    Args:
+        attrs: Rule attributes
+        fragments: List of str; extra config fragments that are required.
+        **kwargs: Additional args to pass onto `rule()`
+
+    Returns:
+        A rule function
+    """
+    if "py" not in fragments:
+        # The list might be frozen, so use concatentation
+        fragments = fragments + ["py"]
+    return rule(
+        # TODO: add ability to remove attrs, i.e. for imports attr
+        attrs = EXECUTABLE_ATTRS | attrs,
+        toolchains = [TOOLCHAIN_TYPE] + cc_helper.use_cpp_toolchain(),
+        fragments = fragments,
+        **kwargs
+    )
+
+def cc_configure_features(ctx, *, cc_toolchain, extra_features):
+    """Configure C++ features for Python purposes.
+
+    Args:
+        ctx: Rule ctx
+        cc_toolchain: The CcToolchain the target is using.
+        extra_features: list of strings; additional features to request be
+            enabled.
+
+    Returns:
+        struct of the feature configuration and all requested features.
+    """
+    requested_features = ["static_linking_mode"]
+    requested_features.extend(extra_features)
+    requested_features.extend(ctx.features)
+    if "legacy_whole_archive" not in ctx.disabled_features:
+        requested_features.append("legacy_whole_archive")
+    feature_configuration = _cc_common.configure_features(
+        ctx = ctx,
+        cc_toolchain = cc_toolchain,
+        requested_features = requested_features,
+        unsupported_features = ctx.disabled_features,
+    )
+    return struct(
+        feature_configuration = feature_configuration,
+        requested_features = requested_features,
+    )
+
+only_exposed_for_google_internal_reason = struct(
+    create_runfiles_with_build_data = _create_runfiles_with_build_data,
+)
diff --git a/python/private/common/py_executable_bazel.bzl b/python/private/common/py_executable_bazel.bzl
new file mode 100644
index 0000000..7c7ecb0
--- /dev/null
+++ b/python/private/common/py_executable_bazel.bzl
@@ -0,0 +1,480 @@
+# 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 for Bazel Python executable."""
+
+load(":common/paths.bzl", "paths")
+load(":common/python/attributes_bazel.bzl", "IMPORTS_ATTRS")
+load(
+    ":common/python/common.bzl",
+    "create_binary_semantics_struct",
+    "create_cc_details_struct",
+    "create_executable_result_struct",
+    "union_attrs",
+)
+load(":common/python/common_bazel.bzl", "collect_cc_info", "get_imports", "maybe_precompile")
+load(":common/python/providers.bzl", "DEFAULT_STUB_SHEBANG")
+load(
+    ":common/python/py_executable.bzl",
+    "create_base_executable_rule",
+    "py_executable_base_impl",
+)
+load(":common/python/semantics.bzl", "TOOLS_REPO")
+
+_py_builtins = _builtins.internal.py_builtins
+_EXTERNAL_PATH_PREFIX = "external"
+_ZIP_RUNFILES_DIRECTORY_NAME = "runfiles"
+
+BAZEL_EXECUTABLE_ATTRS = union_attrs(
+    IMPORTS_ATTRS,
+    {
+        "legacy_create_init": attr.int(
+            default = -1,
+            values = [-1, 0, 1],
+            doc = """\
+Whether to implicitly create empty `__init__.py` files in the runfiles tree.
+These are created in every directory containing Python source code or shared
+libraries, and every parent directory of those directories, excluding the repo
+root directory. The default, `-1` (auto), means true unless
+`--incompatible_default_to_explicit_init_py` is used. If false, the user is
+responsible for creating (possibly empty) `__init__.py` files and adding them to
+the `srcs` of Python targets as required.
+                                       """,
+        ),
+        "_bootstrap_template": attr.label(
+            allow_single_file = True,
+            default = "@" + TOOLS_REPO + "//tools/python:python_bootstrap_template.txt",
+        ),
+        "_launcher": attr.label(
+            cfg = "target",
+            default = "@" + TOOLS_REPO + "//tools/launcher:launcher",
+            executable = True,
+        ),
+        "_py_interpreter": attr.label(
+            default = configuration_field(
+                fragment = "bazel_py",
+                name = "python_top",
+            ),
+        ),
+        # TODO: This appears to be vestigial. It's only added because
+        # GraphlessQueryTest.testLabelsOperator relies on it to test for
+        # query behavior of implicit dependencies.
+        "_py_toolchain_type": attr.label(
+            default = "@" + TOOLS_REPO + "//tools/python:toolchain_type",
+        ),
+        "_windows_launcher_maker": attr.label(
+            default = "@" + TOOLS_REPO + "//tools/launcher:launcher_maker",
+            cfg = "exec",
+            executable = True,
+        ),
+        "_zipper": attr.label(
+            cfg = "exec",
+            executable = True,
+            default = "@" + TOOLS_REPO + "//tools/zip:zipper",
+        ),
+    },
+)
+
+def create_executable_rule(*, attrs, **kwargs):
+    return create_base_executable_rule(
+        attrs = BAZEL_EXECUTABLE_ATTRS | attrs,
+        fragments = ["py", "bazel_py"],
+        **kwargs
+    )
+
+def py_executable_bazel_impl(ctx, *, is_test, inherited_environment):
+    """Common code for executables for Baze."""
+    result = py_executable_base_impl(
+        ctx = ctx,
+        semantics = create_binary_semantics_bazel(),
+        is_test = is_test,
+        inherited_environment = inherited_environment,
+    )
+    return struct(
+        providers = result.providers,
+        **result.legacy_providers
+    )
+
+def create_binary_semantics_bazel():
+    return create_binary_semantics_struct(
+        # keep-sorted start
+        create_executable = _create_executable,
+        get_cc_details_for_binary = _get_cc_details_for_binary,
+        get_central_uncachable_version_file = lambda ctx: None,
+        get_coverage_deps = _get_coverage_deps,
+        get_debugger_deps = _get_debugger_deps,
+        get_extra_common_runfiles_for_binary = lambda ctx: ctx.runfiles(),
+        get_extra_providers = _get_extra_providers,
+        get_extra_write_build_data_env = lambda ctx: {},
+        get_imports = get_imports,
+        get_interpreter_path = _get_interpreter_path,
+        get_native_deps_dso_name = _get_native_deps_dso_name,
+        get_native_deps_user_link_flags = _get_native_deps_user_link_flags,
+        get_stamp_flag = _get_stamp_flag,
+        maybe_precompile = maybe_precompile,
+        should_build_native_deps_dso = lambda ctx: False,
+        should_create_init_files = _should_create_init_files,
+        should_include_build_data = lambda ctx: False,
+        # keep-sorted end
+    )
+
+def _get_coverage_deps(ctx, runtime_details):
+    _ = ctx, runtime_details  # @unused
+    return []
+
+def _get_debugger_deps(ctx, runtime_details):
+    _ = ctx, runtime_details  # @unused
+    return []
+
+def _get_extra_providers(ctx, main_py, runtime_details):
+    _ = ctx, main_py, runtime_details  # @unused
+    return {}, []
+
+def _get_stamp_flag(ctx):
+    # NOTE: Undocumented API; private to builtins
+    return ctx.configuration.stamp_binaries
+
+def _should_create_init_files(ctx):
+    if ctx.attr.legacy_create_init == -1:
+        return not ctx.fragments.py.default_to_explicit_init_py
+    else:
+        return bool(ctx.attr.legacy_create_init)
+
+def _create_executable(
+        ctx,
+        *,
+        executable,
+        main_py,
+        imports,
+        is_test,
+        runtime_details,
+        cc_details,
+        native_deps_details,
+        runfiles_details):
+    _ = is_test, cc_details, native_deps_details  # @unused
+
+    common_bootstrap_template_kwargs = dict(
+        main_py = main_py,
+        imports = imports,
+        runtime_details = runtime_details,
+    )
+
+    # TODO: This should use the configuration instead of the Bazel OS.
+    # This is just legacy behavior.
+    is_windows = _py_builtins.get_current_os_name() == "windows"
+
+    if is_windows:
+        if not executable.extension == "exe":
+            fail("Should not happen: somehow we are generating a non-.exe file on windows")
+        base_executable_name = executable.basename[0:-4]
+    else:
+        base_executable_name = executable.basename
+
+    zip_bootstrap = ctx.actions.declare_file(base_executable_name + ".temp", sibling = executable)
+    zip_file = ctx.actions.declare_file(base_executable_name + ".zip", sibling = executable)
+
+    _expand_bootstrap_template(
+        ctx,
+        output = zip_bootstrap,
+        is_for_zip = True,
+        **common_bootstrap_template_kwargs
+    )
+    _create_zip_file(
+        ctx,
+        output = zip_file,
+        original_nonzip_executable = executable,
+        executable_for_zip_file = zip_bootstrap,
+        runfiles = runfiles_details.default_runfiles,
+    )
+
+    extra_files_to_build = []
+
+    # NOTE: --build_python_zip defauls to true on Windows
+    build_zip_enabled = ctx.fragments.py.build_python_zip
+
+    # When --build_python_zip is enabled, then the zip file becomes
+    # one of the default outputs.
+    if build_zip_enabled:
+        extra_files_to_build.append(zip_file)
+
+    # The logic here is a bit convoluted. Essentially, there are 3 types of
+    # executables produced:
+    # 1. (non-Windows) A bootstrap template based program.
+    # 2. (non-Windows) A self-executable zip file of a bootstrap template based program.
+    # 3. (Windows) A native Windows executable that finds and launches
+    #    the actual underlying Bazel program (one of the above). Note that
+    #    it implicitly assumes one of the above is located next to it, and
+    #    that --build_python_zip defaults to true for Windows.
+
+    should_create_executable_zip = False
+    bootstrap_output = None
+    if not is_windows:
+        if build_zip_enabled:
+            should_create_executable_zip = True
+        else:
+            bootstrap_output = executable
+    else:
+        _create_windows_exe_launcher(
+            ctx,
+            output = executable,
+            use_zip_file = build_zip_enabled,
+            python_binary_path = runtime_details.executable_interpreter_path,
+        )
+        if not build_zip_enabled:
+            # On Windows, the main executable has an "exe" extension, so
+            # here we re-use the un-extensioned name for the bootstrap output.
+            bootstrap_output = ctx.actions.declare_file(base_executable_name)
+
+            # The launcher looks for the non-zip executable next to
+            # itself, so add it to the default outputs.
+            extra_files_to_build.append(bootstrap_output)
+
+    if should_create_executable_zip:
+        if bootstrap_output != None:
+            fail("Should not occur: bootstrap_output should not be used " +
+                 "when creating an executable zip")
+        _create_executable_zip_file(ctx, output = executable, zip_file = zip_file)
+    elif bootstrap_output:
+        _expand_bootstrap_template(
+            ctx,
+            output = bootstrap_output,
+            is_for_zip = build_zip_enabled,
+            **common_bootstrap_template_kwargs
+        )
+    else:
+        # Otherwise, this should be the Windows case of launcher + zip.
+        # Double check this just to make sure.
+        if not is_windows or not build_zip_enabled:
+            fail(("Should not occur: The non-executable-zip and " +
+                  "non-boostrap-template case should have windows and zip " +
+                  "both true, but got " +
+                  "is_windows={is_windows} " +
+                  "build_zip_enabled={build_zip_enabled}").format(
+                is_windows = is_windows,
+                build_zip_enabled = build_zip_enabled,
+            ))
+
+    return create_executable_result_struct(
+        extra_files_to_build = depset(extra_files_to_build),
+        output_groups = {"python_zip_file": depset([zip_file])},
+    )
+
+def _expand_bootstrap_template(
+        ctx,
+        *,
+        output,
+        main_py,
+        imports,
+        is_for_zip,
+        runtime_details):
+    runtime = runtime_details.effective_runtime
+    if (ctx.configuration.coverage_enabled and
+        runtime and
+        runtime.coverage_tool):
+        coverage_tool_runfiles_path = "{}/{}".format(
+            ctx.workspace_name,
+            runtime.coverage_tool.short_path,
+        )
+    else:
+        coverage_tool_runfiles_path = ""
+
+    if runtime:
+        shebang = runtime.stub_shebang
+        template = runtime.bootstrap_template
+    else:
+        shebang = DEFAULT_STUB_SHEBANG
+        template = ctx.file._bootstrap_template
+
+    ctx.actions.expand_template(
+        template = template,
+        output = output,
+        substitutions = {
+            "%coverage_tool%": coverage_tool_runfiles_path,
+            "%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False",
+            "%imports%": ":".join(imports.to_list()),
+            "%is_zipfile%": "True" if is_for_zip else "False",
+            "%main%": "{}/{}".format(
+                ctx.workspace_name,
+                main_py.short_path,
+            ),
+            "%python_binary%": runtime_details.executable_interpreter_path,
+            "%shebang%": shebang,
+            "%target%": str(ctx.label),
+            "%workspace_name%": ctx.workspace_name,
+        },
+        is_executable = True,
+    )
+
+def _create_windows_exe_launcher(
+        ctx,
+        *,
+        output,
+        python_binary_path,
+        use_zip_file):
+    launch_info = ctx.actions.args()
+    launch_info.use_param_file("%s", use_always = True)
+    launch_info.set_param_file_format("multiline")
+    launch_info.add("binary_type=Python")
+    launch_info.add(ctx.workspace_name, format = "workspace_name=%s")
+    launch_info.add(
+        "1" if ctx.configuration.runfiles_enabled() else "0",
+        format = "symlink_runfiles_enabled=%s",
+    )
+    launch_info.add(python_binary_path, format = "python_bin_path=%s")
+    launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s")
+
+    ctx.actions.run(
+        executable = ctx.executable._windows_launcher_maker,
+        arguments = [ctx.executable._launcher.path, launch_info, output.path],
+        inputs = [ctx.executable._launcher],
+        outputs = [output],
+        mnemonic = "PyBuildLauncher",
+        progress_message = "Creating launcher for %{label}",
+        # Needed to inherit PATH when using non-MSVC compilers like MinGW
+        use_default_shell_env = True,
+    )
+
+def _create_zip_file(ctx, *, output, original_nonzip_executable, executable_for_zip_file, runfiles):
+    workspace_name = ctx.workspace_name
+    legacy_external_runfiles = _py_builtins.get_legacy_external_runfiles(ctx)
+
+    manifest = ctx.actions.args()
+    manifest.use_param_file("@%s", use_always = True)
+    manifest.set_param_file_format("multiline")
+
+    manifest.add("__main__.py={}".format(executable_for_zip_file.path))
+    manifest.add("__init__.py=")
+    manifest.add(
+        "{}=".format(
+            _get_zip_runfiles_path("__init__.py", workspace_name, legacy_external_runfiles),
+        ),
+    )
+    for path in runfiles.empty_filenames.to_list():
+        manifest.add("{}=".format(_get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles)))
+
+    def map_zip_runfiles(file):
+        if file != original_nonzip_executable and file != output:
+            return "{}={}".format(
+                _get_zip_runfiles_path(file.short_path, workspace_name, legacy_external_runfiles),
+                file.path,
+            )
+        else:
+            return None
+
+    manifest.add_all(runfiles.files, map_each = map_zip_runfiles, allow_closure = True)
+
+    inputs = [executable_for_zip_file]
+    if _py_builtins.is_bzlmod_enabled(ctx):
+        zip_repo_mapping_manifest = ctx.actions.declare_file(
+            output.basename + ".repo_mapping",
+            sibling = output,
+        )
+        _py_builtins.create_repo_mapping_manifest(
+            ctx = ctx,
+            runfiles = runfiles,
+            output = zip_repo_mapping_manifest,
+        )
+        manifest.add("{}/_repo_mapping={}".format(
+            _ZIP_RUNFILES_DIRECTORY_NAME,
+            zip_repo_mapping_manifest.path,
+        ))
+        inputs.append(zip_repo_mapping_manifest)
+
+    for artifact in runfiles.files.to_list():
+        # Don't include the original executable because it isn't used by the
+        # zip file, so no need to build it for the action.
+        # Don't include the zipfile itself because it's an output.
+        if artifact != original_nonzip_executable and artifact != output:
+            inputs.append(artifact)
+
+    zip_cli_args = ctx.actions.args()
+    zip_cli_args.add("cC")
+    zip_cli_args.add(output)
+
+    ctx.actions.run(
+        executable = ctx.executable._zipper,
+        arguments = [zip_cli_args, manifest],
+        inputs = depset(inputs),
+        outputs = [output],
+        use_default_shell_env = True,
+        mnemonic = "PythonZipper",
+        progress_message = "Building Python zip: %{label}",
+    )
+
+def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles):
+    if legacy_external_runfiles and path.startswith(_EXTERNAL_PATH_PREFIX):
+        zip_runfiles_path = paths.relativize(path, _EXTERNAL_PATH_PREFIX)
+    else:
+        # NOTE: External runfiles (artifacts in other repos) will have a leading
+        # path component of "../" so that they refer outside the main workspace
+        # directory and into the runfiles root. By normalizing, we simplify e.g.
+        # "workspace/../foo/bar" to simply "foo/bar".
+        zip_runfiles_path = paths.normalize("{}/{}".format(workspace_name, path))
+    return "{}/{}".format(_ZIP_RUNFILES_DIRECTORY_NAME, zip_runfiles_path)
+
+def _create_executable_zip_file(ctx, *, output, zip_file):
+    ctx.actions.run_shell(
+        command = "echo '{shebang}' | cat - {zip} > {output}".format(
+            shebang = "#!/usr/bin/env python3",
+            zip = zip_file.path,
+            output = output.path,
+        ),
+        inputs = [zip_file],
+        outputs = [output],
+        use_default_shell_env = True,
+        mnemonic = "BuildBinary",
+        progress_message = "Build Python zip executable: %{label}",
+    )
+
+def _get_cc_details_for_binary(ctx, extra_deps):
+    cc_info = collect_cc_info(ctx, extra_deps = extra_deps)
+    return create_cc_details_struct(
+        cc_info_for_propagating = cc_info,
+        cc_info_for_self_link = cc_info,
+        cc_info_with_extra_link_time_libraries = None,
+        extra_runfiles = ctx.runfiles(),
+        # Though the rules require the CcToolchain, it isn't actually used.
+        cc_toolchain = None,
+    )
+
+def _get_interpreter_path(ctx, *, runtime, flag_interpreter_path):
+    if runtime:
+        if runtime.interpreter_path:
+            interpreter_path = runtime.interpreter_path
+        else:
+            interpreter_path = "{}/{}".format(
+                ctx.workspace_name,
+                runtime.interpreter.short_path,
+            )
+
+            # NOTE: External runfiles (artifacts in other repos) will have a
+            # leading path component of "../" so that they refer outside the
+            # main workspace directory and into the runfiles root. By
+            # normalizing, we simplify e.g. "workspace/../foo/bar" to simply
+            # "foo/bar"
+            interpreter_path = paths.normalize(interpreter_path)
+
+    elif flag_interpreter_path:
+        interpreter_path = flag_interpreter_path
+    else:
+        fail("Unable to determine interpreter path")
+
+    return interpreter_path
+
+def _get_native_deps_dso_name(ctx):
+    _ = ctx  # @unused
+    fail("Building native deps DSO not supported.")
+
+def _get_native_deps_user_link_flags(ctx):
+    _ = ctx  # @unused
+    fail("Building native deps DSO not supported.")
diff --git a/python/private/common/py_library.bzl b/python/private/common/py_library.bzl
new file mode 100644
index 0000000..62f974f
--- /dev/null
+++ b/python/private/common/py_library.bzl
@@ -0,0 +1,99 @@
+# 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 rule."""
+
+load(
+    ":common/python/attributes.bzl",
+    "COMMON_ATTRS",
+    "PY_SRCS_ATTRS",
+    "SRCS_VERSION_ALL_VALUES",
+    "create_srcs_attr",
+    "create_srcs_version_attr",
+)
+load(
+    ":common/python/common.bzl",
+    "check_native_allowed",
+    "collect_imports",
+    "collect_runfiles",
+    "create_instrumented_files_info",
+    "create_output_group_info",
+    "create_py_info",
+    "filter_to_py_srcs",
+    "union_attrs",
+)
+load(":common/python/providers.bzl", "PyCcLinkParamsProvider")
+
+_py_builtins = _builtins.internal.py_builtins
+
+LIBRARY_ATTRS = union_attrs(
+    COMMON_ATTRS,
+    PY_SRCS_ATTRS,
+    create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES),
+    create_srcs_attr(mandatory = False),
+)
+
+def py_library_impl(ctx, *, semantics):
+    """Abstract implementation of py_library rule.
+
+    Args:
+        ctx: The rule ctx
+        semantics: A `LibrarySemantics` struct; see `create_library_semantics_struct`
+
+    Returns:
+        A list of modern providers to propagate.
+    """
+    check_native_allowed(ctx)
+    direct_sources = filter_to_py_srcs(ctx.files.srcs)
+    output_sources = depset(semantics.maybe_precompile(ctx, direct_sources))
+    runfiles = collect_runfiles(ctx = ctx, files = output_sources)
+
+    cc_info = semantics.get_cc_info_for_library(ctx)
+    py_info, deps_transitive_sources = create_py_info(
+        ctx,
+        direct_sources = depset(direct_sources),
+        imports = collect_imports(ctx, semantics),
+    )
+
+    # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455
+    listeners_enabled = _py_builtins.are_action_listeners_enabled(ctx)
+    if listeners_enabled:
+        _py_builtins.add_py_extra_pseudo_action(
+            ctx = ctx,
+            dependency_transitive_python_sources = deps_transitive_sources,
+        )
+
+    return [
+        DefaultInfo(files = output_sources, runfiles = runfiles),
+        py_info,
+        create_instrumented_files_info(ctx),
+        PyCcLinkParamsProvider(cc_info = cc_info),
+        create_output_group_info(py_info.transitive_sources, extra_groups = {}),
+    ]
+
+def create_py_library_rule(*, attrs = {}, **kwargs):
+    """Creates a py_library rule.
+
+    Args:
+        attrs: dict of rule attributes.
+        **kwargs: Additional kwargs to pass onto the rule() call.
+    Returns:
+        A rule object
+    """
+    return rule(
+        attrs = LIBRARY_ATTRS | attrs,
+        # TODO(b/253818097): fragments=py is only necessary so that
+        # RequiredConfigFragmentsTest passes
+        fragments = ["py"],
+        **kwargs
+    )
diff --git a/python/private/common/py_library_bazel.bzl b/python/private/common/py_library_bazel.bzl
new file mode 100644
index 0000000..b844b97
--- /dev/null
+++ b/python/private/common/py_library_bazel.bzl
@@ -0,0 +1,59 @@
+# 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(
+    ":common/python/attributes_bazel.bzl",
+    "IMPORTS_ATTRS",
+)
+load(
+    ":common/python/common.bzl",
+    "create_library_semantics_struct",
+    "union_attrs",
+)
+load(
+    ":common/python/common_bazel.bzl",
+    "collect_cc_info",
+    "get_imports",
+    "maybe_precompile",
+)
+load(
+    ":common/python/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/common/py_library_macro.bzl b/python/private/common/py_library_macro.bzl
new file mode 100644
index 0000000..729c426
--- /dev/null
+++ b/python/private/common/py_library_macro.bzl
@@ -0,0 +1,19 @@
+# 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 macro-half of py_library rule."""
+
+load(":common/python/py_library_bazel.bzl", py_library_rule = "py_library")
+
+def py_library(**kwargs):
+    py_library_rule(**kwargs)
diff --git a/python/private/common/py_runtime_macro.bzl b/python/private/common/py_runtime_macro.bzl
new file mode 100644
index 0000000..6b27bcc
--- /dev/null
+++ b/python/private/common/py_runtime_macro.bzl
@@ -0,0 +1,22 @@
+# 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.
+"""Macro to wrap the py_runtime rule."""
+
+load(":common/python/py_runtime_rule.bzl", py_runtime_rule = "py_runtime")
+
+# NOTE: The function name is purposefully selected to match the underlying
+# rule name so that e.g. 'generator_function' shows as the same name so
+# that it is less confusing to users.
+def py_runtime(**kwargs):
+    py_runtime_rule(**kwargs)
diff --git a/python/private/common/py_runtime_rule.bzl b/python/private/common/py_runtime_rule.bzl
new file mode 100644
index 0000000..22efaa6
--- /dev/null
+++ b/python/private/common/py_runtime_rule.bzl
@@ -0,0 +1,214 @@
+# 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(":common/paths.bzl", "paths")
+load(":common/python/attributes.bzl", "NATIVE_RULES_ALLOWLIST_ATTRS")
+load(":common/python/common.bzl", "check_native_allowed")
+load(":common/python/providers.bzl", "DEFAULT_BOOTSTRAP_TEMPLATE", "DEFAULT_STUB_SHEBANG", _PyRuntimeInfo = "PyRuntimeInfo")
+
+_py_builtins = _builtins.internal.py_builtins
+
+def _py_runtime_impl(ctx):
+    check_native_allowed(ctx)
+    interpreter_path = ctx.attr.interpreter_path or None  # Convert empty string to None
+    interpreter = ctx.file.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
+    ])
+
+    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")
+
+    if ctx.attr.coverage_tool:
+        coverage_di = ctx.attr.coverage_tool[DefaultInfo]
+
+        if _py_builtins.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
+    if python_version == "_INTERNAL_SENTINEL":
+        if ctx.fragments.py.use_toolchains:
+            fail(
+                "When using Python toolchains, this attribute must be set explicitly to either 'PY2' " +
+                "or 'PY3'. See https://github.com/bazelbuild/bazel/issues/7899 for more " +
+                "information. You can temporarily avoid this error by reverting to the legacy " +
+                "Python runtime mechanism (`--incompatible_use_python_toolchains=false`).",
+            )
+        else:
+            python_version = ctx.fragments.py.default_python_version
+
+    # 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")
+
+    return [
+        _PyRuntimeInfo(
+            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,
+        ),
+        DefaultInfo(
+            files = runtime_files,
+            runfiles = ctx.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
+
+```
+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 = 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 `py_binary`
+and `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.
+""",
+        ),
+        "interpreter": attr.label(
+            allow_single_file = True,
+            doc = """
+For an in-build runtime, this is the target to invoke as the interpreter. For a
+platform runtime 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.
+"""),
+        "python_version": attr.string(
+            default = "_INTERNAL_SENTINEL",
+            values = ["PY2", "PY3", "_INTERNAL_SENTINEL"],
+            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.
+            """,
+        ),
+        "stub_shebang": attr.string(
+            default = DEFAULT_STUB_SHEBANG,
+            doc = """
+"Shebang" expression prepended to the bootstrapping Python stub script
+used when executing `py_binary` targets.
+
+See https://github.com/bazelbuild/bazel/issues/8685 for
+motivation.
+
+Does not apply to Windows.
+""",
+        ),
+    },
+)
diff --git a/python/private/common/py_test_bazel.bzl b/python/private/common/py_test_bazel.bzl
new file mode 100644
index 0000000..fde3a5a
--- /dev/null
+++ b/python/private/common/py_test_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(":common/python/attributes.bzl", "AGNOSTIC_TEST_ATTRS")
+load(":common/python/common.bzl", "maybe_add_test_execution_info")
+load(
+    ":common/python/py_executable_bazel.bzl",
+    "create_executable_rule",
+    "py_executable_bazel_impl",
+)
+load(":common/python/semantics.bzl", "TOOLS_REPO")
+
+_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 = "@" + TOOLS_REPO + "//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.providers, ctx)
+    return providers
+
+py_test = create_executable_rule(
+    implementation = _py_test_impl,
+    attrs = AGNOSTIC_TEST_ATTRS | _BAZEL_PY_TEST_ATTRS,
+    test = True,
+)
diff --git a/python/private/common/py_test_macro.bzl b/python/private/common/py_test_macro.bzl
new file mode 100644
index 0000000..4faede6
--- /dev/null
+++ b/python/private/common/py_test_macro.bzl
@@ -0,0 +1,21 @@
+# 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 macro-half of py_test rule."""
+
+load(":common/python/common_bazel.bzl", "convert_legacy_create_init_to_int")
+load(":common/python/py_test_bazel.bzl", py_test_rule = "py_test")
+
+def py_test(**kwargs):
+    convert_legacy_create_init_to_int(kwargs)
+    py_test_rule(**kwargs)
diff --git a/python/private/common/semantics.bzl b/python/private/common/semantics.bzl
new file mode 100644
index 0000000..487ff30
--- /dev/null
+++ b/python/private/common/semantics.bzl
@@ -0,0 +1,34 @@
+# 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.
+"""Contains constants that vary between Bazel and Google-internal"""
+
+IMPORTS_ATTR_SUPPORTED = True
+
+TOOLS_REPO = "bazel_tools"
+PLATFORMS_LOCATION = "@platforms/"
+
+SRCS_ATTR_ALLOW_FILES = [".py", ".py3"]
+
+DEPS_ATTR_ALLOW_RULES = None
+
+PY_RUNTIME_ATTR_NAME = "_py_interpreter"
+
+BUILD_DATA_SYMLINK_PATH = None
+
+IS_BAZEL = True
+
+NATIVE_RULES_MIGRATION_HELP_URL = "https://github.com/bazelbuild/bazel/issues/17773"
+NATIVE_RULES_MIGRATION_FIX_CMD = "add_python_loads"
+
+ALLOWED_MAIN_EXTENSIONS = [".py"]