fix(precompiling)!: make binary-level precompile opt-in/opt-opt work (#2243)

This makes binary-level opt-in/opt-out of precompiling work as intended.
Previously,
when `pyc_collection=include_pyc` was set on a binary, only transitive
libraries that
had explicitly enabled precompiling were being included (which was moot
anyways --
libraries put their files in runfiles, so no matter what, their files
were included).

The intent was that, when a binary set `pyc_collection=include_pyc`,
then precompiled
files would be used for all its transitive dependencies (unless they
had, at the
target-level, disabled precompiling). Conversely, if
`pyc_collection=disabled` was set,
the precompiled files would not be used (unless a target had, at the
target level,
enabled precompiling).

To make it work as desired, the basic fix is to make it so that
libraries have a place to
put the implicit pyc files (the ones automatically generated), and have
the binaries
include those when requested. The net effect is a library has 4 sets of
files it produces:
* required py files: py source files that should always go into the
binary's runfiles
* required pyc files: precompiled pyc files that should always go into
the binary's
  runfiles (e.g., when a library sets `precompile=enabled` directly).
* implicit pyc files: precompiled pyc files for a library that are
always generated, but
  it's up to the binary if they go into the runfiles
* implicit pyc source files: the source py file for an implicit pyc
file. When a binary
*doesn't* include the implicit pyc file, it must include the source py
file (otherwise
  none of the library's code ends up included).

Similarly, in order to allow a binary to decide what files are used,
libraries must
stop putting the py/pyc files into runfiles themselves. While this is
potentially
a breaking change, I found that, within Google, there was no reliance on
this behavior,
so should be safe enough. That said, I added `--add_srcs_to_runfiles` to
restore
the previous behavior to aid in transitioning.

**BREAKING CHANGES**
1. `py_library` no longer puts its srcs into runfiles directly.
2. Removed `--precompile_add_to_runfiles`
3. Removed `--pyc_collection`
4. `precompile=if_generated_source` removed
5. `precompile_source_retention=omit_if_generated_source` removed

Though 2 through 5 are technically breaking changes, I don't think
precompiling
was very usable anyways, so usages of those flags/values is rare.

Fixes https://github.com/bazelbuild/rules_python/issues/2212
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5473dc5..abaa2bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,14 @@
 [x.x.x]: https://github.com/bazelbuild/rules_python/releases/tag/x.x.x
 
 ### Changed
+* **BREAKING** `py_library` no longer puts its source files or generated pyc
+  files in runfiles; it's the responsibility of consumers (e.g. binaries) to
+  populate runfiles with the necessary files. Adding source files to runfiles
+  can be temporarily restored by setting {obj}`--add_srcs_to_runfiles=enabled`,
+  but this flag will be removed in a subsequent releases.
+* {obj}`PyInfo.transitive_sources` is now added to runfiles. These files are
+  `.py` files that are required to be added to runfiles by downstream binaries
+  (or equivalent).
 * (toolchains) `py_runtime.implementation_name` now defaults to `cpython`
   (previously it defaulted to None).
 
@@ -50,6 +58,8 @@
 * (bzlmod) In hybrid bzlmod with WORKSPACE builds,
   `python_register_toolchains(register_toolchains=True)` is respected
   ([#1675](https://github.com/bazelbuild/rules_python/issues/1675)).
+* (precompiling) The {obj}`pyc_collection` attribute now correctly
+  enables (or disables) using pyc files from targets transitively
 
 ### Added
 * (py_wheel) Now supports `compress = (True|False)` to allow disabling
@@ -66,14 +76,20 @@
     * `3.10 -> 3.10.15`
     * `3.11 -> 3.11.10`
     * `3.12 -> 3.12.7`
-[20241008]: https://github.com/indygreg/python-build-standalone/releases/tag/20241008
 * (coverage) Add support for python 3.13 and bump `coverage.py` to 7.6.1.
 * (bzlmod) Add support for `download_only` flag to disable usage of `sdists`
   when {bzl:attr}`pip.parse.experimental_index_url` is set.
+* (api) PyInfo fields: {obj}`PyInfo.transitive_implicit_pyc_files`,
+  {obj}`PyInfo.transitive_implicit_pyc_source_files`.
 
+[20241008]: https://github.com/indygreg/python-build-standalone/releases/tag/20241008
 
 ### Removed
-* Nothing yet
+* (precompiling) {obj}`--precompile_add_to_runfiles` has been removed.
+* (precompiling) {obj}`--pyc_collection` has been removed. The `pyc_collection`
+  attribute now bases its default on {obj}`--precompile`.
+* (precompiling) The {obj}`precompile=if_generated_source` value has been removed.
+* (precompiling) The {obj}`precompile_source_retention=omit_if_generated_source` value has been removed.
 
 ## [0.36.0] - 2024-09-24
 
diff --git a/docs/api/rules_python/python/config_settings/index.md b/docs/api/rules_python/python/config_settings/index.md
index 645e4e2..511a218 100644
--- a/docs/api/rules_python/python/config_settings/index.md
+++ b/docs/api/rules_python/python/config_settings/index.md
@@ -5,6 +5,25 @@
 
 # //python/config_settings
 
+:::{bzl:flag} add_srcs_to_runfiles
+Determines if the `srcs` of targets are added to their runfiles.
+
+More specifically, the sources added to runfiles are the `.py` files in `srcs`.
+If precompiling is performed, it is the `.py` files that are kept according
+to {obj}`precompile_source_retention`.
+
+Values:
+* `auto`: (default) Automatically decide the effective value; the current
+  behavior is `disabled`.
+* `disabled`: Don't add `srcs` to a target's runfiles.
+* `enabled`:  Add `srcs` to a target's runfiles.
+::::{versionadded} 0.37.0
+::::
+::::{deprecated} 0.37.0
+This is a transition flag and will be removed in a subsequent release.
+::::
+:::
+
 :::{bzl:flag} python_version
 Determines the default hermetic Python toolchain version. This can be set to
 one of the values that `rules_python` maintains.
@@ -42,12 +61,8 @@
 
 * `auto`: (default) Automatically decide the effective value based on environment,
   target platform, etc.
-* `enabled`: Compile Python source files at build time. Note that
-  {bzl:obj}`--precompile_add_to_runfiles` affects how the compiled files are included into
-  a downstream binary.
+* `enabled`: Compile Python source files at build time.
 * `disabled`: Don't compile Python source files at build time.
-* `if_generated_source`: Compile Python source files, but only if they're a
-  generated file.
 * `force_enabled`: Like `enabled`, except overrides target-level setting. This
   is mostly useful for development, testing enabling precompilation more
   broadly, or as an escape hatch if build-time compiling is not available.
@@ -56,6 +71,9 @@
   broadly, or as an escape hatch if build-time compiling is not available.
 :::{versionadded} 0.33.0
 :::
+:::{versionchanged} 0.37.0
+The `if_generated_source` value was removed
+:::
 ::::
 
 ::::{bzl:flag} precompile_source_retention
@@ -73,45 +91,14 @@
   target platform, etc.
 * `keep_source`: Include the original Python source.
 * `omit_source`: Don't include the orignal py source.
-* `omit_if_generated_source`: Keep the original source if it's a regular source
-  file, but omit it if it's a generated file.
 
 :::{versionadded} 0.33.0
 :::
 :::{versionadded} 0.36.0
 The `auto` value
 :::
-::::
-
-::::{bzl:flag} precompile_add_to_runfiles
-Determines if a target adds its compiled files to its runfiles.
-
-When a target compiles its files, but doesn't add them to its own runfiles, it
-relies on a downstream target to retrieve them from
-{bzl:obj}`PyInfo.transitive_pyc_files`
-
-Values:
-* `always`: Always include the compiled files in the target's runfiles.
-* `decided_elsewhere`: Don't include the compiled files in the target's
-  runfiles; they are still added to {bzl:obj}`PyInfo.transitive_pyc_files`. See
-  also: {bzl:obj}`py_binary.pyc_collection` attribute. This is useful for allowing
-  incrementally enabling precompilation on a per-binary basis.
-:::{versionadded} 0.33.0
-:::
-::::
-
-::::{bzl:flag} pyc_collection
-Determine if `py_binary` collects transitive pyc files.
-
-:::{note}
-This flag is overridden by the target level `pyc_collection` attribute.
-:::
-
-Values:
-* `include_pyc`: Include `PyInfo.transitive_pyc_files` as part of the binary.
-* `disabled`: Don't include `PyInfo.transitive_pyc_files` as part of the binary.
-:::{versionadded} 0.33.0
-:::
+:::{versionchanged} 0.37.0
+The `omit_if_generated_source` value was removed
 ::::
 
 ::::{bzl:flag} py_linux_libc
diff --git a/docs/precompiling.md b/docs/precompiling.md
index 52678e6..6eadc40 100644
--- a/docs/precompiling.md
+++ b/docs/precompiling.md
@@ -20,24 +20,24 @@
 
 ## Binary-level opt-in
 
-Because of the costs of precompiling, it may not be feasible to globally enable it
-for your repo for everything. For example, some binaries may be
-particularly large, and doubling the number of runfiles isn't doable.
+Binary-level opt-in allows enabling precompiling on a per-target basic. This is
+useful for situations such as:
 
-If this is the case, there's an alternative way to more selectively and
-incrementally control precompiling on a per-binry basis.
+* Globally enabling precompiling in your `.bazelrc` isn't feasible. This may
+  be because some targets don't work with precompiling, e.g. because they're too
+  big.
+* Enabling precompiling for build tools (exec config targets) separately from
+  target-config programs.
 
-To use this approach, the two basic steps are:
-1. Disable pyc files from being automatically added to runfiles:
-   {bzl:obj}`--@rules_python//python/config_settings:precompile_add_to_runfiles=decided_elsewhere`,
-2. Set the `pyc_collection` attribute on the binaries/tests that should or should
-   not use precompiling.
+To use this approach, set the {bzl:attr}`pyc_collection` attribute on the
+binaries/tests that should or should not use precompiling. Then change the
+{bzl:flag}`--precompile` default.
 
-The default for the `pyc_collection` attribute is controlled by the flag
-{bzl:obj}`--@rules_python//python/config_settings:pyc_collection`, so you
+The default for the {bzl:attr}`pyc_collection` attribute is controlled by the flag
+{bzl:obj}`--@rules_python//python/config_settings:precompile`, so you
 can use an opt-in or opt-out approach by setting its value:
-* targets must opt-out: `--@rules_python//python/config_settings:pyc_collection=include_pyc`
-* targets must opt-in: `--@rules_python//python/config_settings:pyc_collection=disabled`
+* targets must opt-out: `--@rules_python//python/config_settings:precompile=enabled`
+* targets must opt-in: `--@rules_python//python/config_settings:precompile=disabled`
 
 ## Advanced precompiler customization
 
@@ -48,7 +48,7 @@
 mechanisms are available:
 
 * The exec tools toolchain allows customizing the precompiler binary used with
-  the `precompiler` attribute. Arbitrary binaries are supported.
+  the {bzl:attr}`precompiler` attribute. Arbitrary binaries are supported.
 * The execution requirements can be customized using
   `--@rules_python//tools/precompiler:execution_requirements`. This is a list
   flag that can be repeated. Each entry is a key=value that is added to the
@@ -92,3 +92,9 @@
   `foo.cpython-39.opt-2.pyc`). This works fine (it's all byte code), but also
   means the interpreter `-O` argument can't be used -- doing so will cause the
   interpreter to look for the non-existent `opt-N` named files.
+* Targets with the same source files and different exec properites will result
+  in action conflicts. This most commonly occurs when a `py_binary` and
+  `py_library` have the same source files. To fix, modify both targets so
+  they have the same exec properties. If this is difficult because unsupported
+  exec groups end up being passed to the Python rules, please file an issue
+  to have those exec groups added to the Python rules.
diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel
index 9fb3957..b55213b 100644
--- a/python/config_settings/BUILD.bazel
+++ b/python/config_settings/BUILD.bazel
@@ -2,12 +2,11 @@
 load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING", "PYTHON_VERSIONS")
 load(
     "//python/private:flags.bzl",
+    "AddSrcsToRunfilesFlag",
     "BootstrapImplFlag",
     "ExecToolsToolchainFlag",
-    "PrecompileAddToRunfilesFlag",
     "PrecompileFlag",
     "PrecompileSourceRetentionFlag",
-    "PycCollectionFlag",
 )
 load(
     "//python/private/pypi:flags.bzl",
@@ -34,6 +33,14 @@
 )
 
 string_flag(
+    name = "add_srcs_to_runfiles",
+    build_setting_default = AddSrcsToRunfilesFlag.AUTO,
+    values = AddSrcsToRunfilesFlag.flag_values(),
+    # NOTE: Only public because it is dependency of public rules.
+    visibility = ["//visibility:public"],
+)
+
+string_flag(
     name = "exec_tools_toolchain",
     build_setting_default = ExecToolsToolchainFlag.DISABLED,
     values = sorted(ExecToolsToolchainFlag.__members__.values()),
@@ -69,22 +76,6 @@
 )
 
 string_flag(
-    name = "precompile_add_to_runfiles",
-    build_setting_default = PrecompileAddToRunfilesFlag.ALWAYS,
-    values = sorted(PrecompileAddToRunfilesFlag.__members__.values()),
-    # NOTE: Only public because it's an implicit dependency
-    visibility = ["//visibility:public"],
-)
-
-string_flag(
-    name = "pyc_collection",
-    build_setting_default = PycCollectionFlag.DISABLED,
-    values = sorted(PycCollectionFlag.__members__.values()),
-    # NOTE: Only public because it's an implicit dependency
-    visibility = ["//visibility:public"],
-)
-
-string_flag(
     name = "bootstrap_impl",
     build_setting_default = BootstrapImplFlag.SYSTEM_PYTHON,
     values = sorted(BootstrapImplFlag.__members__.values()),
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 6fb4a1c..cee77c5 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -297,6 +297,10 @@
     name = "py_package_bzl",
     srcs = ["py_package.bzl"],
     visibility = ["//:__subpackages__"],
+    deps = [
+        ":builders_bzl",
+        ":py_info_bzl",
+    ],
 )
 
 bzl_library(
diff --git a/python/private/common/attributes.bzl b/python/private/common/attributes.bzl
index 0299e85..56e8a66 100644
--- a/python/private/common/attributes.bzl
+++ b/python/private/common/attributes.bzl
@@ -29,6 +29,26 @@
 
 _PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None)
 
+# Due to how the common exec_properties attribute works, rules must add exec
+# groups even if they don't actually use them. This is due to two interactions:
+# 1. Rules give an error if users pass an unsupported exec group.
+# 2. exec_properties is configurable, so macro-code can't always filter out
+#    exec group names that aren't supported by the rule.
+# The net effect is, if a user passes exec_properties to a macro, and the macro
+# invokes two rules, the macro can't always ensure each rule is only passed
+# valid exec groups, and is thus liable to cause an error.
+#
+# NOTE: These are no-op/empty exec groups. If a rule *does* support an exec
+# group and needs custom settings, it should merge this dict with one that
+# overrides the supported key.
+REQUIRED_EXEC_GROUPS = {
+    # py_binary may invoke C++ linking, or py rules may be used in combination
+    # with cc rules (e.g. within the same macro), so support that exec group.
+    # This exec group is defined by rules_cc for the cc rules.
+    "cpp_link": exec_group(),
+    "py_precompile": exec_group(),
+}
+
 _STAMP_VALUES = [-1, 0, 1]
 
 def _precompile_attr_get_effective_value(ctx):
@@ -50,7 +70,6 @@
     if precompile not in (
         PrecompileAttr.ENABLED,
         PrecompileAttr.DISABLED,
-        PrecompileAttr.IF_GENERATED_SOURCE,
     ):
         fail("Unexpected final precompile value: {}".format(repr(precompile)))
 
@@ -60,14 +79,10 @@
 PrecompileAttr = enum(
     # Determine the effective value from --precompile
     INHERIT = "inherit",
-    # Compile Python source files at build time. Note that
-    # --precompile_add_to_runfiles affects how the compiled files are included
-    # into a downstream binary.
+    # Compile Python source files at build time.
     ENABLED = "enabled",
     # Don't compile Python source files at build time.
     DISABLED = "disabled",
-    # Compile Python source files, but only if they're a generated file.
-    IF_GENERATED_SOURCE = "if_generated_source",
     get_effective_value = _precompile_attr_get_effective_value,
 )
 
@@ -90,7 +105,6 @@
     if attr_value not in (
         PrecompileSourceRetentionAttr.KEEP_SOURCE,
         PrecompileSourceRetentionAttr.OMIT_SOURCE,
-        PrecompileSourceRetentionAttr.OMIT_IF_GENERATED_SOURCE,
     ):
         fail("Unexpected final precompile_source_retention value: {}".format(repr(attr_value)))
     return attr_value
@@ -100,14 +114,17 @@
     INHERIT = "inherit",
     KEEP_SOURCE = "keep_source",
     OMIT_SOURCE = "omit_source",
-    OMIT_IF_GENERATED_SOURCE = "omit_if_generated_source",
     get_effective_value = _precompile_source_retention_get_effective_value,
 )
 
 def _pyc_collection_attr_is_pyc_collection_enabled(ctx):
     pyc_collection = ctx.attr.pyc_collection
     if pyc_collection == PycCollectionAttr.INHERIT:
-        pyc_collection = ctx.attr._pyc_collection_flag[BuildSettingInfo].value
+        precompile_flag = PrecompileFlag.get_effective_value(ctx)
+        if precompile_flag in (PrecompileFlag.ENABLED, PrecompileFlag.FORCE_ENABLED):
+            pyc_collection = PycCollectionAttr.INCLUDE_PYC
+        else:
+            pyc_collection = PycCollectionAttr.DISABLED
 
     if pyc_collection not in (PycCollectionAttr.INCLUDE_PYC, PycCollectionAttr.DISABLED):
         fail("Unexpected final pyc_collection value: {}".format(repr(pyc_collection)))
@@ -283,13 +300,9 @@
 
 Values:
 
-* `inherit`: Determine the value from the {flag}`--precompile` flag.
-* `enabled`: Compile Python source files at build time. Note that
-  --precompile_add_to_runfiles affects how the compiled files are included into
-  a downstream binary.
+* `inherit`: Allow the downstream binary decide if precompiled files are used.
+* `enabled`: Compile Python source files at build time.
 * `disabled`: Don't compile Python source files at build time.
-* `if_generated_source`: Compile Python source files, but only if they're a
-  generated file.
 
 :::{seealso}
 
@@ -344,8 +357,6 @@
 * `inherit`: Inherit the value from the {flag}`--precompile_source_retention` flag.
 * `keep_source`: Include the original Python source.
 * `omit_source`: Don't include the original py source.
-* `omit_if_generated_source`: Keep the original source if it's a regular source
-  file, but omit it if it's a generated file.
 """,
         ),
         # Required attribute, but details vary by rule.
@@ -357,10 +368,6 @@
         # Required attribute, but the details vary by rule.
         # Use create_srcs_version_attr to create one.
         "srcs_version": None,
-        "_precompile_add_to_runfiles_flag": attr.label(
-            default = "//python/config_settings:precompile_add_to_runfiles",
-            providers = [BuildSettingInfo],
-        ),
         "_precompile_flag": attr.label(
             default = "//python/config_settings:precompile",
             providers = [BuildSettingInfo],
diff --git a/python/private/common/common.bzl b/python/private/common/common.bzl
index 99a6324..ec46ea8 100644
--- a/python/private/common/common.bzl
+++ b/python/private/common/common.bzl
@@ -348,15 +348,29 @@
         collect_default = True,
     )
 
-def create_py_info(ctx, *, direct_sources, direct_pyc_files, imports):
+def create_py_info(
+        ctx,
+        *,
+        required_py_files,
+        required_pyc_files,
+        implicit_pyc_files,
+        implicit_pyc_source_files,
+        imports):
     """Create PyInfo provider.
 
     Args:
         ctx: rule ctx.
-        direct_sources: depset of Files; the direct, raw `.py` sources for the
-            target. This should only be Python source files. It should not
-            include pyc files.
-        direct_pyc_files: depset of Files; the direct `.pyc` sources for the target.
+        required_py_files: `depset[File]`; the direct, `.py` sources for the
+            target that **must** be included by downstream targets. This should
+            only be Python source files. It should not include pyc files.
+        required_pyc_files: `depset[File]`; the direct `.pyc` files this target
+            produces.
+        implicit_pyc_files: `depset[File]` pyc files that are only used if pyc
+            collection is enabled.
+        implicit_pyc_source_files: `depset[File]` source files for implicit pyc
+            files that are used when the implicit pyc files are not.
+        implicit_pyc_files: {type}`depset[File]` Implicitly generated pyc files
+            that a binary can choose to include.
         imports: depset of strings; the import path values to propagate.
 
     Returns:
@@ -366,8 +380,10 @@
     """
 
     py_info = PyInfoBuilder()
-    py_info.direct_pyc_files.add(direct_pyc_files)
-    py_info.transitive_pyc_files.add(direct_pyc_files)
+    py_info.direct_pyc_files.add(required_pyc_files)
+    py_info.transitive_pyc_files.add(required_pyc_files)
+    py_info.transitive_implicit_pyc_files.add(implicit_pyc_files)
+    py_info.transitive_implicit_pyc_source_files.add(implicit_pyc_source_files)
     py_info.imports.add(imports)
     py_info.merge_has_py2_only_sources(ctx.attr.srcs_version in ("PY2", "PY2ONLY"))
     py_info.merge_has_py3_only_sources(ctx.attr.srcs_version in ("PY3", "PY3ONLY"))
@@ -386,7 +402,7 @@
                 py_info.merge_uses_shared_libraries(cc_helper.is_valid_shared_library_artifact(f))
 
     deps_transitive_sources = py_info.transitive_sources.build()
-    py_info.transitive_sources.add(direct_sources)
+    py_info.transitive_sources.add(required_py_files)
 
     # We only look at data to calculate uses_shared_libraries, if it's already
     # true, then we don't need to waste time looping over it.
diff --git a/python/private/common/common_bazel.bzl b/python/private/common/common_bazel.bzl
index c86abd2..6148fc2 100644
--- a/python/private/common/common_bazel.bzl
+++ b/python/private/common/common_bazel.bzl
@@ -14,7 +14,9 @@
 """Common functions that are specific to Bazel rule implementation"""
 
 load("@bazel_skylib//lib:paths.bzl", "paths")
+load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("@rules_cc//cc:defs.bzl", "CcInfo", "cc_common")
+load("//python/private:flags.bzl", "PrecompileFlag")
 load("//python/private:py_interpreter_program.bzl", "PyInterpreterProgramInfo")
 load("//python/private:toolchain_types.bzl", "EXEC_TOOLS_TOOLCHAIN_TYPE", "TARGET_TOOLCHAIN_TYPE")
 load(":attributes.bzl", "PrecompileAttr", "PrecompileInvalidationModeAttr", "PrecompileSourceRetentionAttr")
@@ -60,7 +62,7 @@
     Returns:
         Struct of precompiling results with fields:
         * `keep_srcs`: list of File; the input sources that should be included
-          as default outputs and runfiles.
+          as default outputs.
         * `pyc_files`: list of File; the precompiled files.
         * `py_to_pyc_map`: dict of src File input to pyc File output. If a source
           file wasn't precompiled, it won't be in the dict.
@@ -72,9 +74,27 @@
     if exec_tools_toolchain == None or exec_tools_toolchain.exec_tools.precompiler == None:
         precompile = PrecompileAttr.DISABLED
     else:
-        precompile = PrecompileAttr.get_effective_value(ctx)
+        precompile_flag = ctx.attr._precompile_flag[BuildSettingInfo].value
+
+        if precompile_flag == PrecompileFlag.FORCE_ENABLED:
+            precompile = PrecompileAttr.ENABLED
+        elif precompile_flag == PrecompileFlag.FORCE_DISABLED:
+            precompile = PrecompileAttr.DISABLED
+        else:
+            precompile = ctx.attr.precompile
+
+    # Unless explicitly disabled, we always generate a pyc. This allows
+    # binaries to decide whether to include them or not later.
+    if precompile != PrecompileAttr.DISABLED:
+        should_precompile = True
+    else:
+        should_precompile = False
 
     source_retention = PrecompileSourceRetentionAttr.get_effective_value(ctx)
+    keep_source = (
+        not should_precompile or
+        source_retention == PrecompileSourceRetentionAttr.KEEP_SOURCE
+    )
 
     result = struct(
         keep_srcs = [],
@@ -82,26 +102,17 @@
         py_to_pyc_map = {},
     )
     for src in srcs:
-        # The logic below is a bit convoluted. The gist is:
-        # * If precompiling isn't done, add the py source to default outputs.
-        #   Otherwise, the source retention flag decides.
-        # * In order to determine `use_pycache`, we have to know if the source
-        #   is being added to the default outputs.
-        is_generated_source = not src.is_source
-        should_precompile = (
-            precompile == PrecompileAttr.ENABLED or
-            (precompile == PrecompileAttr.IF_GENERATED_SOURCE and is_generated_source)
-        )
-        keep_source = (
-            not should_precompile or
-            source_retention == PrecompileSourceRetentionAttr.KEEP_SOURCE or
-            (source_retention == PrecompileSourceRetentionAttr.OMIT_IF_GENERATED_SOURCE and not is_generated_source)
-        )
         if should_precompile:
+            # NOTE: _precompile() may return None
             pyc = _precompile(ctx, src, use_pycache = keep_source)
+        else:
+            pyc = None
+
+        if pyc:
             result.pyc_files.append(pyc)
             result.py_to_pyc_map[src] = pyc
-        if keep_source:
+
+        if keep_source or not pyc:
             result.keep_srcs.append(src)
 
     return result
@@ -119,6 +130,12 @@
     Returns:
         File of the generated pyc file.
     """
+
+    # Generating a file in another package is an error, so we have to skip
+    # such cases.
+    if ctx.label.package != src.owner.package:
+        return None
+
     exec_tools_info = ctx.toolchains[EXEC_TOOLS_TOOLCHAIN_TYPE].exec_tools
     target_toolchain = ctx.toolchains[TARGET_TOOLCHAIN_TYPE].py3_runtime
 
@@ -149,7 +166,11 @@
     stem = src.basename[:-(len(src.extension) + 1)]
     if use_pycache:
         if not target_toolchain.pyc_tag:
-            fail("Unable to create __pycache__ pyc: pyc_tag is empty")
+            # This is most likely because of a "runtime toolchain", i.e. the
+            # autodetecting toolchain, or some equivalent toolchain that can't
+            # assume to know the runtime Python version at build time.
+            # Instead of failing, just don't generate any pyc.
+            return None
         pyc_path = "__pycache__/{stem}.{tag}.pyc".format(
             stem = stem,
             tag = target_toolchain.pyc_tag,
diff --git a/python/private/common/py_executable.bzl b/python/private/common/py_executable.bzl
index cfd9961..6c238a2 100644
--- a/python/private/common/py_executable.bzl
+++ b/python/private/common/py_executable.bzl
@@ -18,10 +18,9 @@
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("@rules_cc//cc:defs.bzl", "cc_common")
 load("//python/private:builders.bzl", "builders")
-load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag")
 load("//python/private:py_executable_info.bzl", "PyExecutableInfo")
 load("//python/private:py_info.bzl", "PyInfo")
-load("//python/private:reexports.bzl", "BuiltinPyRuntimeInfo")
+load("//python/private:reexports.bzl", "BuiltinPyInfo", "BuiltinPyRuntimeInfo")
 load(
     "//python/private:toolchain_types.bzl",
     "EXEC_TOOLS_TOOLCHAIN_TYPE",
@@ -32,7 +31,9 @@
     "AGNOSTIC_EXECUTABLE_ATTRS",
     "COMMON_ATTRS",
     "PY_SRCS_ATTRS",
+    "PrecompileAttr",
     "PycCollectionAttr",
+    "REQUIRED_EXEC_GROUPS",
     "SRCS_VERSION_ALL_VALUES",
     "create_srcs_attr",
     "create_srcs_version_attr",
@@ -99,16 +100,13 @@
             doc = """
 Determines whether pyc files from dependencies should be manually included.
 
-NOTE: This setting is only useful with {flag}`--precompile_add_to_runfiles=decided_elsewhere`.
-
 Valid values are:
-* `inherit`: Inherit the value from {flag}`--pyc_collection`.
-* `include_pyc`: Add pyc files from dependencies in the binary (from
-  {obj}`PyInfo.transitive_pyc_files`.
-* `disabled`: Don't explicitly add pyc files from dependencies. Note that
-  pyc files may still come from dependencies if a target includes them as
-  part of their runfiles (such as when {obj}`--precompile_add_to_runfiles=always`
-  is used).
+* `inherit`: Inherit the value from {flag}`--precompile`.
+* `include_pyc`: Add implicitly generated pyc files from dependencies. i.e.
+  pyc files for targets that specify {attr}`precompile="inherit"`.
+* `disabled`: Don't add implicitly generated pyc files. Note that
+  pyc files may still come from dependencies that enable precompiling at the
+  target level.
 """,
         ),
         # TODO(b/203567235): In Google, this attribute is deprecated, and can
@@ -126,10 +124,6 @@
             default = "//python/config_settings:bootstrap_impl",
             providers = [BuildSettingInfo],
         ),
-        "_pyc_collection_flag": attr.label(
-            default = "//python/config_settings:pyc_collection",
-            providers = [BuildSettingInfo],
-        ),
         "_windows_constraints": attr.label_list(
             default = [
                 "@platforms//os:windows",
@@ -164,11 +158,21 @@
     direct_sources = filter_to_py_srcs(ctx.files.srcs)
     precompile_result = semantics.maybe_precompile(ctx, direct_sources)
 
+    required_py_files = precompile_result.keep_srcs
+    required_pyc_files = []
+    implicit_pyc_files = []
+    implicit_pyc_source_files = direct_sources
+
+    if ctx.attr.precompile == PrecompileAttr.ENABLED:
+        required_pyc_files.extend(precompile_result.pyc_files)
+    else:
+        implicit_pyc_files.extend(precompile_result.pyc_files)
+
     # Sourceless precompiled builds omit the main py file from outputs, so
     # main has to be pointed to the precompiled main instead.
-    if main_py not in precompile_result.keep_srcs:
+    if (main_py not in precompile_result.keep_srcs and
+        PycCollectionAttr.is_pyc_collection_enabled(ctx)):
         main_py = precompile_result.py_to_pyc_map[main_py]
-    direct_pyc_files = depset(precompile_result.pyc_files)
 
     executable = _declare_executable_file(ctx)
     default_outputs = builders.DepsetBuilder()
@@ -200,8 +204,10 @@
         ctx,
         executable = executable,
         extra_deps = extra_deps,
-        main_py_files = depset([main_py] + precompile_result.keep_srcs),
-        direct_pyc_files = direct_pyc_files,
+        required_py_files = required_py_files,
+        required_pyc_files = required_pyc_files,
+        implicit_pyc_files = implicit_pyc_files,
+        implicit_pyc_source_files = implicit_pyc_source_files,
         extra_common_runfiles = [
             runtime_details.runfiles,
             cc_details.extra_runfiles,
@@ -241,8 +247,10 @@
         runfiles_details = runfiles_details,
         main_py = main_py,
         imports = imports,
-        direct_sources = direct_sources,
-        direct_pyc_files = direct_pyc_files,
+        required_py_files = required_py_files,
+        required_pyc_files = required_pyc_files,
+        implicit_pyc_files = implicit_pyc_files,
+        implicit_pyc_source_files = implicit_pyc_source_files,
         default_outputs = default_outputs.build(),
         runtime_details = runtime_details,
         cc_info = cc_details.cc_info_for_propagating,
@@ -403,8 +411,10 @@
         *,
         executable,
         extra_deps,
-        main_py_files,
-        direct_pyc_files,
+        required_py_files,
+        required_pyc_files,
+        implicit_pyc_files,
+        implicit_pyc_source_files,
         extra_common_runfiles,
         semantics):
     """Returns the set of runfiles necessary prior to executable creation.
@@ -417,8 +427,15 @@
         executable: The main executable output.
         extra_deps: List of Targets; additional targets whose runfiles
             will be added to the common runfiles.
-        main_py_files: depset of File of the default outputs to add into runfiles.
-        direct_pyc_files: depset of File of pyc files directly from this target.
+        required_py_files: `depset[File]` the direct, `.py` sources for the
+            target that **must** be included by downstream targets. This should
+            only be Python source files. It should not include pyc files.
+        required_pyc_files: `depset[File]` the direct `.pyc` files this target
+            produces.
+        implicit_pyc_files: `depset[File]` pyc files that are only used if pyc
+            collection is enabled.
+        implicit_pyc_source_files: `depset[File]` source files for implicit pyc
+            files that are used when the implicit pyc files are not.
         extra_common_runfiles: List of runfiles; additional runfiles that
             will be added to the common runfiles.
         semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`.
@@ -433,19 +450,31 @@
           None.
     """
     common_runfiles = builders.RunfilesBuilder()
-    common_runfiles.add(main_py_files)
+    common_runfiles.files.add(required_py_files)
+    common_runfiles.files.add(required_pyc_files)
+    pyc_collection_enabled = PycCollectionAttr.is_pyc_collection_enabled(ctx)
+    if pyc_collection_enabled:
+        common_runfiles.files.add(implicit_pyc_files)
+    else:
+        common_runfiles.files.add(implicit_pyc_source_files)
 
-    if ctx.attr._precompile_add_to_runfiles_flag[BuildSettingInfo].value == PrecompileAddToRunfilesFlag.ALWAYS:
-        common_runfiles.add(direct_pyc_files)
-    elif PycCollectionAttr.is_pyc_collection_enabled(ctx):
-        common_runfiles.add(direct_pyc_files)
-        for dep in (ctx.attr.deps + extra_deps):
-            if PyInfo not in dep:
-                continue
-            common_runfiles.add(dep[PyInfo].transitive_pyc_files)
+    for dep in (ctx.attr.deps + extra_deps):
+        if not (PyInfo in dep or BuiltinPyInfo in dep):
+            continue
+        info = dep[PyInfo] if PyInfo in dep else dep[BuiltinPyInfo]
+        common_runfiles.files.add(info.transitive_sources)
 
-    common_runfiles.add(collect_runfiles(ctx))
-    common_runfiles.add(collect_runfiles(ctx))
+        # Everything past this won't work with BuiltinPyInfo
+        if not hasattr(info, "transitive_pyc_files"):
+            continue
+
+        common_runfiles.files.add(info.transitive_pyc_files)
+        if pyc_collection_enabled:
+            common_runfiles.files.add(info.transitive_implicit_pyc_files)
+        else:
+            common_runfiles.files.add(info.transitive_implicit_pyc_source_files)
+
+    common_runfiles.runfiles.append(collect_runfiles(ctx))
     if extra_deps:
         common_runfiles.add_runfiles(targets = extra_deps)
     common_runfiles.add(extra_common_runfiles)
@@ -782,8 +811,10 @@
         ctx,
         executable,
         main_py,
-        direct_sources,
-        direct_pyc_files,
+        required_py_files,
+        required_pyc_files,
+        implicit_pyc_files,
+        implicit_pyc_source_files,
         default_outputs,
         runfiles_details,
         imports,
@@ -798,10 +829,15 @@
         ctx: The rule ctx.
         executable: File; the target's executable file.
         main_py: File; the main .py entry point.
-        direct_sources: list of Files; the direct, raw `.py` sources for the target.
-            This should only be Python source files. It should not include pyc
-            files.
-        direct_pyc_files: depset of File; the direct pyc files for the target.
+        required_py_files: `depset[File]` the direct, `.py` sources for the
+            target that **must** be included by downstream targets. This should
+            only be Python source files. It should not include pyc files.
+        required_pyc_files: `depset[File]` the direct `.pyc` files this target
+            produces.
+        implicit_pyc_files: `depset[File]` pyc files that are only used if pyc
+            collection is enabled.
+        implicit_pyc_source_files: `depset[File]` source files for implicit pyc
+            files that are used when the implicit pyc files are not.
         default_outputs: depset of Files; the files for DefaultInfo.files
         runfiles_details: runfiles that will become the default  and data runfiles.
         imports: depset of strings; the import paths to propagate
@@ -876,8 +912,10 @@
 
     py_info, deps_transitive_sources, builtin_py_info = create_py_info(
         ctx,
-        direct_sources = depset(direct_sources),
-        direct_pyc_files = direct_pyc_files,
+        required_py_files = required_py_files,
+        required_pyc_files = required_pyc_files,
+        implicit_pyc_files = implicit_pyc_files,
+        implicit_pyc_source_files = implicit_pyc_source_files,
         imports = imports,
     )
 
@@ -931,6 +969,7 @@
         # The list might be frozen, so use concatentation
         fragments = fragments + ["py"]
     kwargs.setdefault("provides", []).append(PyExecutableInfo)
+    kwargs["exec_groups"] = REQUIRED_EXEC_GROUPS | (kwargs.get("exec_groups") or {})
     return rule(
         # TODO: add ability to remove attrs, i.e. for imports attr
         attrs = dicts.add(EXECUTABLE_ATTRS, attrs),
diff --git a/python/private/common/py_library.bzl b/python/private/common/py_library.bzl
index 078626e..bce18c3 100644
--- a/python/private/common/py_library.bzl
+++ b/python/private/common/py_library.bzl
@@ -16,7 +16,7 @@
 load("@bazel_skylib//lib:dicts.bzl", "dicts")
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("//python/private:builders.bzl", "builders")
-load("//python/private:flags.bzl", "PrecompileAddToRunfilesFlag")
+load("//python/private:flags.bzl", "AddSrcsToRunfilesFlag", "PrecompileFlag")
 load(
     "//python/private:toolchain_types.bzl",
     "EXEC_TOOLS_TOOLCHAIN_TYPE",
@@ -26,6 +26,8 @@
     ":attributes.bzl",
     "COMMON_ATTRS",
     "PY_SRCS_ATTRS",
+    "PrecompileAttr",
+    "REQUIRED_EXEC_GROUPS",
     "SRCS_VERSION_ALL_VALUES",
     "create_srcs_attr",
     "create_srcs_version_attr",
@@ -51,6 +53,11 @@
     PY_SRCS_ATTRS,
     create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES),
     create_srcs_attr(mandatory = False),
+    {
+        "_add_srcs_to_runfiles_flag": attr.label(
+            default = "//python/config_settings:add_srcs_to_runfiles",
+        ),
+    },
 )
 
 def py_library_impl(ctx, *, semantics):
@@ -67,27 +74,39 @@
     direct_sources = filter_to_py_srcs(ctx.files.srcs)
 
     precompile_result = semantics.maybe_precompile(ctx, direct_sources)
-    direct_pyc_files = depset(precompile_result.pyc_files)
+
+    required_py_files = precompile_result.keep_srcs
+    required_pyc_files = []
+    implicit_pyc_files = []
+    implicit_pyc_source_files = direct_sources
+
+    precompile_attr = ctx.attr.precompile
+    precompile_flag = ctx.attr._precompile_flag[BuildSettingInfo].value
+    if (precompile_attr == PrecompileAttr.ENABLED or
+        precompile_flag == PrecompileFlag.FORCE_ENABLED):
+        required_pyc_files.extend(precompile_result.pyc_files)
+    else:
+        implicit_pyc_files.extend(precompile_result.pyc_files)
+
     default_outputs = builders.DepsetBuilder()
     default_outputs.add(precompile_result.keep_srcs)
-    default_outputs.add(direct_pyc_files)
+    default_outputs.add(required_pyc_files)
     default_outputs = default_outputs.build()
 
     runfiles = builders.RunfilesBuilder()
-    runfiles.add(precompile_result.keep_srcs)
-
-    if ctx.attr._precompile_add_to_runfiles_flag[BuildSettingInfo].value == PrecompileAddToRunfilesFlag.ALWAYS:
-        runfiles.add(direct_pyc_files)
-
+    if AddSrcsToRunfilesFlag.is_enabled(ctx):
+        runfiles.add(required_py_files)
     runfiles.add(collect_runfiles(ctx))
     runfiles = runfiles.build(ctx)
 
     cc_info = semantics.get_cc_info_for_library(ctx)
     py_info, deps_transitive_sources, builtins_py_info = create_py_info(
         ctx,
-        direct_sources = depset(direct_sources),
+        required_py_files = required_py_files,
+        required_pyc_files = required_pyc_files,
+        implicit_pyc_files = implicit_pyc_files,
+        implicit_pyc_source_files = implicit_pyc_source_files,
         imports = collect_imports(ctx, semantics),
-        direct_pyc_files = direct_pyc_files,
     )
 
     # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455
@@ -119,6 +138,10 @@
 NOTE: Precompilation affects which of the default outputs are included in the
 resulting runfiles. See the precompile-related attributes and flags for
 more information.
+
+:::{versionchanged} 0.37.0
+Source files are no longer added to the runfiles directly.
+:::
 """
 
 def create_py_library_rule(*, attrs = {}, **kwargs):
@@ -137,6 +160,7 @@
     # TODO: b/253818097 - fragments=py is only necessary so that
     # RequiredConfigFragmentsTest passes
     fragments = kwargs.pop("fragments", None) or []
+    kwargs["exec_groups"] = REQUIRED_EXEC_GROUPS | (kwargs.get("exec_groups") or {})
     return rule(
         attrs = dicts.add(LIBRARY_ATTRS, attrs),
         toolchains = [
diff --git a/python/private/enum.bzl b/python/private/enum.bzl
index 011d9fb..d71442e 100644
--- a/python/private/enum.bzl
+++ b/python/private/enum.bzl
@@ -17,10 +17,13 @@
 This is a separate file to minimize transitive loads.
 """
 
-def enum(**kwargs):
+def enum(methods = {}, **kwargs):
     """Creates a struct whose primary purpose is to be like an enum.
 
     Args:
+        methods: {type}`dict[str, callable]` functions that will be
+            added to the created enum object, but will have the enum object
+            itself passed as the first positional arg when calling them.
         **kwargs: The fields of the returned struct. All uppercase names will
             be treated as enum values and added to `__members__`.
 
@@ -33,4 +36,10 @@
         for key, value in kwargs.items()
         if key.upper() == key
     }
-    return struct(__members__ = members, **kwargs)
+
+    for name, unbound_method in methods.items():
+        # buildifier: disable=uninitialized
+        kwargs[name] = lambda *a, **k: unbound_method(self, *a, **k)
+
+    self = struct(__members__ = members, **kwargs)
+    return self
diff --git a/python/private/flags.bzl b/python/private/flags.bzl
index 652e117..e7643fc 100644
--- a/python/private/flags.bzl
+++ b/python/private/flags.bzl
@@ -21,6 +21,40 @@
 load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
 load("//python/private:enum.bzl", "enum")
 
+def _FlagEnum_flag_values(self):
+    return sorted(self.__members__.values())
+
+def FlagEnum(**kwargs):
+    """Define an enum specialized for flags.
+
+    Args:
+        **kwargs: members of the enum.
+
+    Returns:
+        {type}`FlagEnum` struct. This is an enum with the following extras:
+        * `flag_values`: A function that returns a sorted list of the
+          flag values (enum `__members__`). Useful for passing to the
+          `values` attribute for string flags.
+    """
+    return enum(
+        methods = dict(flag_values = _FlagEnum_flag_values),
+        **kwargs
+    )
+
+def _AddSrcsToRunfilesFlag_is_enabled(ctx):
+    value = ctx.attr._add_srcs_to_runfiles_flag[BuildSettingInfo].value
+    if value == AddSrcsToRunfilesFlag.AUTO:
+        value = AddSrcsToRunfilesFlag.ENABLED
+    return value == AddSrcsToRunfilesFlag.ENABLED
+
+# buildifier: disable=name-conventions
+AddSrcsToRunfilesFlag = FlagEnum(
+    AUTO = "auto",
+    ENABLED = "enabled",
+    DISABLED = "disabled",
+    is_enabled = _AddSrcsToRunfilesFlag_is_enabled,
+)
+
 def _bootstrap_impl_flag_get_value(ctx):
     return ctx.attr._bootstrap_impl_flag[BuildSettingInfo].value
 
@@ -55,17 +89,13 @@
     # Automatically decide the effective value based on environment,
     # target platform, etc.
     AUTO = "auto",
-    # Compile Python source files at build time. Note that
-    # --precompile_add_to_runfiles affects how the compiled files are included
-    # into a downstream binary.
+    # Compile Python source files at build time.
     ENABLED = "enabled",
     # Don't compile Python source files at build time.
     DISABLED = "disabled",
-    # Compile Python source files, but only if they're a generated file.
-    IF_GENERATED_SOURCE = "if_generated_source",
     # Like `enabled`, except overrides target-level setting. This is mostly
     # useful for development, testing enabling precompilation more broadly, or
-    # as an escape hatch if build-time compiling is not available.
+    # as an escape hatch to force all transitive deps to precompile.
     FORCE_ENABLED = "force_enabled",
     # Like `disabled`, except overrides target-level setting. This is useful
     # useful for development, testing enabling precompilation more broadly, or
@@ -90,32 +120,5 @@
     KEEP_SOURCE = "keep_source",
     # Don't include the original py source.
     OMIT_SOURCE = "omit_source",
-    # Keep the original py source if it's a regular source file, but omit it
-    # if it's a generated file.
-    OMIT_IF_GENERATED_SOURCE = "omit_if_generated_source",
     get_effective_value = _precompile_source_retention_flag_get_effective_value,
 )
-
-# Determines if a target adds its compiled files to its runfiles. When a target
-# compiles its files, but doesn't add them to its own runfiles, it relies on
-# a downstream target to retrieve them from `PyInfo.transitive_pyc_files`
-# buildifier: disable=name-conventions
-PrecompileAddToRunfilesFlag = enum(
-    # Always include the compiled files in the target's runfiles.
-    ALWAYS = "always",
-    # Don't include the compiled files in the target's runfiles; they are
-    # still added to `PyInfo.transitive_pyc_files`. See also:
-    # `py_binary.pyc_collection` attribute. This is useful for allowing
-    # incrementally enabling precompilation on a per-binary basis.
-    DECIDED_ELSEWHERE = "decided_elsewhere",
-)
-
-# Determine if `py_binary` collects transitive pyc files.
-# NOTE: This flag is only respect if `py_binary.pyc_collection` is `inherit`.
-# buildifier: disable=name-conventions
-PycCollectionFlag = enum(
-    # Include `PyInfo.transitive_pyc_files` as part of the binary.
-    INCLUDE_PYC = "include_pyc",
-    # Don't include `PyInfo.transitive_pyc_files` as part of the binary.
-    DISABLED = "disabled",
-)
diff --git a/python/private/proto/py_proto_library.bzl b/python/private/proto/py_proto_library.bzl
index e123ff8..ecb0938 100644
--- a/python/private/proto/py_proto_library.bzl
+++ b/python/private/proto/py_proto_library.bzl
@@ -16,6 +16,7 @@
 
 load("@rules_proto//proto:defs.bzl", "ProtoInfo", "proto_common")
 load("//python:defs.bzl", "PyInfo")
+load("//python/api:api.bzl", _py_common = "py_common")
 
 PY_PROTO_TOOLCHAIN = "@rules_python//python/proto:toolchain_type"
 
@@ -25,6 +26,7 @@
         "imports": """
             (depset[str]) The field forwarding PyInfo.imports coming from
             the proto language runtime dependency.""",
+        "py_info": "PyInfo from proto runtime (or other deps) to propagate.",
         "runfiles_from_proto_deps": """
             (depset[File]) Files from the transitive closure implicit proto
             dependencies""",
@@ -71,6 +73,11 @@
     else:
         proto_lang_toolchain_info = getattr(ctx.attr, "_aspect_proto_toolchain")[proto_common.ProtoLangToolchainInfo]
 
+    py_common = _py_common.get(ctx)
+    py_info = py_common.PyInfoBuilder().merge_target(
+        proto_lang_toolchain_info.runtime,
+    ).build()
+
     api_deps = [proto_lang_toolchain_info.runtime]
 
     generated_sources = []
@@ -127,16 +134,19 @@
             ),
             runfiles_from_proto_deps = runfiles_from_proto_deps,
             transitive_sources = transitive_sources,
+            py_info = py_info,
         ),
     ]
 
 _py_proto_aspect = aspect(
     implementation = _py_proto_aspect_impl,
-    attrs = {} if _incompatible_toolchains_enabled() else {
-        "_aspect_proto_toolchain": attr.label(
-            default = ":python_toolchain",
-        ),
-    },
+    attrs = _py_common.API_ATTRS | (
+        {} if _incompatible_toolchains_enabled() else {
+            "_aspect_proto_toolchain": attr.label(
+                default = ":python_toolchain",
+            ),
+        }
+    ),
     attr_aspects = ["deps"],
     required_providers = [ProtoInfo],
     provides = [_PyProtoInfo],
@@ -159,6 +169,17 @@
         transitive = [info.transitive_sources for info in pyproto_infos],
     )
 
+    py_common = _py_common.get(ctx)
+
+    py_info = py_common.PyInfoBuilder()
+    py_info.set_has_py2_only_sources(False)
+    py_info.set_has_py3_only_sources(False)
+    py_info.transitive_sources.add(default_outputs)
+    py_info.imports.add([info.imports for info in pyproto_infos])
+    py_info.merge_all([
+        pyproto_info.py_info
+        for pyproto_info in pyproto_infos
+    ])
     return [
         DefaultInfo(
             files = default_outputs,
@@ -171,13 +192,7 @@
         OutputGroupInfo(
             default = depset(),
         ),
-        PyInfo(
-            transitive_sources = default_outputs,
-            imports = depset(transitive = [info.imports for info in pyproto_infos]),
-            # Proto always produces 2- and 3- compatible source files
-            has_py2_only_sources = False,
-            has_py3_only_sources = False,
-        ),
+        py_info.build(),
     ]
 
 py_proto_library = rule(
@@ -218,6 +233,6 @@
             providers = [ProtoInfo],
             aspects = [_py_proto_aspect],
         ),
-    },
+    } | _py_common.API_ATTRS,
     provides = [PyInfo],
 )
diff --git a/python/private/py_info.bzl b/python/private/py_info.bzl
index ce56e23..6c2c3c6 100644
--- a/python/private/py_info.bzl
+++ b/python/private/py_info.bzl
@@ -36,7 +36,9 @@
         has_py2_only_sources = False,
         has_py3_only_sources = False,
         direct_pyc_files = depset(),
-        transitive_pyc_files = depset()):
+        transitive_pyc_files = depset(),
+        transitive_implicit_pyc_files = depset(),
+        transitive_implicit_pyc_source_files = depset()):
     _check_arg_type("transitive_sources", "depset", transitive_sources)
 
     # Verify it's postorder compatible, but retain is original ordering.
@@ -49,11 +51,15 @@
     _check_arg_type("direct_pyc_files", "depset", direct_pyc_files)
     _check_arg_type("transitive_pyc_files", "depset", transitive_pyc_files)
 
+    _check_arg_type("transitive_implicit_pyc_files", "depset", transitive_pyc_files)
+    _check_arg_type("transitive_implicit_pyc_source_files", "depset", transitive_pyc_files)
     return {
         "direct_pyc_files": direct_pyc_files,
         "has_py2_only_sources": has_py2_only_sources,
         "has_py3_only_sources": has_py2_only_sources,
         "imports": imports,
+        "transitive_implicit_pyc_files": transitive_implicit_pyc_files,
+        "transitive_implicit_pyc_source_files": transitive_implicit_pyc_source_files,
         "transitive_pyc_files": transitive_pyc_files,
         "transitive_sources": transitive_sources,
         "uses_shared_libraries": uses_shared_libraries,
@@ -91,6 +97,26 @@
 The order of the depset is not guaranteed and may be changed in the future. It
 is recommended to use `default` order (the default).
 """,
+        "transitive_implicit_pyc_files": """
+:type: depset[File]
+
+Automatically generated pyc files that downstream binaries (or equivalent)
+can choose to include in their output. If not included, then
+{obj}`transitive_implicit_pyc_source_files` should be included instead.
+
+::::{versionadded} 0.37.0
+::::
+""",
+        "transitive_implicit_pyc_source_files": """
+:type: depset[File]
+
+Source `.py` files for {obj}`transitive_implicit_pyc_files` that downstream
+binaries (or equivalent) can choose to include in their output. If not included,
+then {obj}`transitive_implicit_pyc_files` should be included instead.
+
+::::{versionadded} 0.37.0
+::::
+""",
         "transitive_pyc_files": """
 :type: depset[File]
 
@@ -105,6 +131,14 @@
 
 A (`postorder`-compatible) depset of `.py` files appearing in the target's
 `srcs` and the `srcs` of the target's transitive `deps`.
+
+These are `.py` source files that are considered required and downstream
+binaries (or equivalent) must include in their outputs.
+
+::::{versionchanged} 0.37.0
+The files are considered necessary for downstream binaries to function;
+previously they were considerd informational and largely unused.
+::::
 """,
         "uses_shared_libraries": """
 :type: bool
@@ -143,6 +177,8 @@
         set_has_py2_only_sources = lambda *a, **k: _PyInfoBuilder_set_has_py2_only_sources(self, *a, **k),
         set_has_py3_only_sources = lambda *a, **k: _PyInfoBuilder_set_has_py3_only_sources(self, *a, **k),
         set_uses_shared_libraries = lambda *a, **k: _PyInfoBuilder_set_uses_shared_libraries(self, *a, **k),
+        transitive_implicit_pyc_files = builders.DepsetBuilder(),
+        transitive_implicit_pyc_source_files = builders.DepsetBuilder(),
         transitive_pyc_files = builders.DepsetBuilder(),
         transitive_sources = builders.DepsetBuilder(),
     )
@@ -199,6 +235,8 @@
 
         # BuiltinPyInfo doesn't have these fields
         if hasattr(info, "transitive_pyc_files"):
+            self.transitive_implicit_pyc_files.add(info.transitive_implicit_pyc_files)
+            self.transitive_implicit_pyc_source_files.add(info.transitive_implicit_pyc_source_files)
             self.transitive_pyc_files.add(info.transitive_pyc_files)
 
     return self
@@ -220,6 +258,8 @@
         kwargs = dict(
             direct_pyc_files = self.direct_pyc_files.build(),
             transitive_pyc_files = self.transitive_pyc_files.build(),
+            transitive_implicit_pyc_files = self.transitive_implicit_pyc_files.build(),
+            transitive_implicit_pyc_source_files = self.transitive_implicit_pyc_source_files.build(),
         )
     else:
         kwargs = {}
diff --git a/python/private/py_package.bzl b/python/private/py_package.bzl
index 08f4b0b..fd8bc27 100644
--- a/python/private/py_package.bzl
+++ b/python/private/py_package.bzl
@@ -11,9 +11,11 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
 "Implementation of py_package rule"
 
+load(":builders.bzl", "builders")
+load(":py_info.bzl", "PyInfoBuilder")
+
 def _path_inside_wheel(input_file):
     # input_file.short_path is sometimes relative ("../${repository_root}/foobar")
     # which is not a valid path within a zip file. Fix that.
@@ -31,10 +33,20 @@
     return short_path
 
 def _py_package_impl(ctx):
-    inputs = depset(
-        transitive = [dep[DefaultInfo].data_runfiles.files for dep in ctx.attr.deps] +
-                     [dep[DefaultInfo].default_runfiles.files for dep in ctx.attr.deps],
-    )
+    inputs = builders.DepsetBuilder()
+    py_info = PyInfoBuilder()
+    for dep in ctx.attr.deps:
+        inputs.add(dep[DefaultInfo].data_runfiles.files)
+        inputs.add(dep[DefaultInfo].default_runfiles.files)
+        py_info.merge_target(dep)
+    py_info = py_info.build()
+    inputs.add(py_info.transitive_sources)
+
+    # Remove conditional once Bazel 6 support dropped.
+    if hasattr(py_info, "transitive_pyc_files"):
+        inputs.add(py_info.transitive_pyc_files)
+
+    inputs = inputs.build()
 
     # TODO: '/' is wrong on windows, but the path separator is not available in starlark.
     # Fix this once ctx.configuration has directory separator information.
diff --git a/tests/base_rules/precompile/precompile_tests.bzl b/tests/base_rules/precompile/precompile_tests.bzl
index 4c0f936..9d6ac5f 100644
--- a/tests/base_rules/precompile/precompile_tests.bzl
+++ b/tests/base_rules/precompile/precompile_tests.bzl
@@ -26,11 +26,10 @@
 load("//tests/support:py_info_subject.bzl", "py_info_subject")
 load(
     "//tests/support:support.bzl",
+    "ADD_SRCS_TO_RUNFILES",
     "CC_TOOLCHAIN",
     "EXEC_TOOLS_TOOLCHAIN",
     "PRECOMPILE",
-    "PRECOMPILE_ADD_TO_RUNFILES",
-    "PRECOMPILE_SOURCE_RETENTION",
     "PY_TOOLCHAINS",
 )
 
@@ -44,7 +43,7 @@
 
 _tests = []
 
-def _test_precompile_enabled_setup(name, py_rule, **kwargs):
+def _test_executable_precompile_attr_enabled_setup(name, py_rule, **kwargs):
     if not rp_config.enable_pystar:
         rt_util.skip_test(name = name)
         return
@@ -53,31 +52,43 @@
         name = name + "_subject",
         precompile = "enabled",
         srcs = ["main.py"],
-        deps = [name + "_lib"],
+        deps = [name + "_lib1"],
         **kwargs
     )
     rt_util.helper_target(
         py_library,
-        name = name + "_lib",
-        srcs = ["lib.py"],
+        name = name + "_lib1",
+        srcs = ["lib1.py"],
+        precompile = "enabled",
+        deps = [name + "_lib2"],
+    )
+
+    # 2nd order target to verify propagation
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib2",
+        srcs = ["lib2.py"],
         precompile = "enabled",
     )
     analysis_test(
         name = name,
-        impl = _test_precompile_enabled_impl,
+        impl = _test_executable_precompile_attr_enabled_impl,
         target = name + "_subject",
         config_settings = _COMMON_CONFIG_SETTINGS,
     )
 
-def _test_precompile_enabled_impl(env, target):
+def _test_executable_precompile_attr_enabled_impl(env, target):
     target = env.expect.that_target(target)
     runfiles = target.runfiles()
-    runfiles.contains_predicate(
+    runfiles_contains_at_least_predicates(runfiles, [
         matching.str_matches("__pycache__/main.fakepy-45.pyc"),
-    )
-    runfiles.contains_predicate(
+        matching.str_matches("__pycache__/lib1.fakepy-45.pyc"),
+        matching.str_matches("__pycache__/lib2.fakepy-45.pyc"),
         matching.str_matches("/main.py"),
-    )
+        matching.str_matches("/lib1.py"),
+        matching.str_matches("/lib2.py"),
+    ])
+
     target.default_outputs().contains_at_least_predicates([
         matching.file_path_matches("__pycache__/main.fakepy-45.pyc"),
         matching.file_path_matches("/main.py"),
@@ -88,23 +99,85 @@
     ])
     py_info.transitive_pyc_files().contains_exactly([
         "{package}/__pycache__/main.fakepy-45.pyc",
-        "{package}/__pycache__/lib.fakepy-45.pyc",
+        "{package}/__pycache__/lib1.fakepy-45.pyc",
+        "{package}/__pycache__/lib2.fakepy-45.pyc",
     ])
 
 def _test_precompile_enabled_py_binary(name):
-    _test_precompile_enabled_setup(name = name, py_rule = py_binary, main = "main.py")
+    _test_executable_precompile_attr_enabled_setup(name = name, py_rule = py_binary, main = "main.py")
 
 _tests.append(_test_precompile_enabled_py_binary)
 
 def _test_precompile_enabled_py_test(name):
-    _test_precompile_enabled_setup(name = name, py_rule = py_test, main = "main.py")
+    _test_executable_precompile_attr_enabled_setup(name = name, py_rule = py_test, main = "main.py")
 
 _tests.append(_test_precompile_enabled_py_test)
 
-def _test_precompile_enabled_py_library(name):
-    _test_precompile_enabled_setup(name = name, py_rule = py_library)
+def _test_precompile_enabled_py_library_setup(name, impl, config_settings):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    rt_util.helper_target(
+        py_library,
+        name = name + "_subject",
+        srcs = ["lib.py"],
+        precompile = "enabled",
+    )
+    analysis_test(
+        name = name,
+        impl = impl,  #_test_precompile_enabled_py_library_impl,
+        target = name + "_subject",
+        config_settings = _COMMON_CONFIG_SETTINGS | config_settings,
+    )
 
-_tests.append(_test_precompile_enabled_py_library)
+def _test_precompile_enabled_py_library_common_impl(env, target):
+    target = env.expect.that_target(target)
+
+    target.default_outputs().contains_at_least_predicates([
+        matching.file_path_matches("__pycache__/lib.fakepy-45.pyc"),
+        matching.file_path_matches("/lib.py"),
+    ])
+    py_info = target.provider(PyInfo, factory = py_info_subject)
+    py_info.direct_pyc_files().contains_exactly([
+        "{package}/__pycache__/lib.fakepy-45.pyc",
+    ])
+    py_info.transitive_pyc_files().contains_exactly([
+        "{package}/__pycache__/lib.fakepy-45.pyc",
+    ])
+
+def _test_precompile_enabled_py_library_add_to_runfiles_disabled(name):
+    _test_precompile_enabled_py_library_setup(
+        name = name,
+        impl = _test_precompile_enabled_py_library_add_to_runfiles_disabled_impl,
+        config_settings = {
+            ADD_SRCS_TO_RUNFILES: "disabled",
+        },
+    )
+
+def _test_precompile_enabled_py_library_add_to_runfiles_disabled_impl(env, target):
+    _test_precompile_enabled_py_library_common_impl(env, target)
+    runfiles = env.expect.that_target(target).runfiles()
+    runfiles.contains_exactly([])
+
+_tests.append(_test_precompile_enabled_py_library_add_to_runfiles_disabled)
+
+def _test_precompile_enabled_py_library_add_to_runfiles_enabled(name):
+    _test_precompile_enabled_py_library_setup(
+        name = name,
+        impl = _test_precompile_enabled_py_library_add_to_runfiles_enabled_impl,
+        config_settings = {
+            ADD_SRCS_TO_RUNFILES: "enabled",
+        },
+    )
+
+def _test_precompile_enabled_py_library_add_to_runfiles_enabled_impl(env, target):
+    _test_precompile_enabled_py_library_common_impl(env, target)
+    runfiles = env.expect.that_target(target).runfiles()
+    runfiles.contains_exactly([
+        "{workspace}/{package}/lib.py",
+    ])
+
+_tests.append(_test_precompile_enabled_py_library_add_to_runfiles_enabled)
 
 def _test_pyc_only(name):
     if not rp_config.enable_pystar:
@@ -117,12 +190,19 @@
         srcs = ["main.py"],
         main = "main.py",
         precompile_source_retention = "omit_source",
+        pyc_collection = "include_pyc",
+        deps = [name + "_lib"],
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib",
+        srcs = ["lib.py"],
+        precompile_source_retention = "omit_source",
     )
     analysis_test(
         name = name,
         impl = _test_pyc_only_impl,
         config_settings = _COMMON_CONFIG_SETTINGS | {
-            ##PRECOMPILE_SOURCE_RETENTION: "omit_source",
             PRECOMPILE: "enabled",
         },
         target = name + "_subject",
@@ -136,9 +216,15 @@
     runfiles.contains_predicate(
         matching.str_matches("/main.pyc"),
     )
+    runfiles.contains_predicate(
+        matching.str_matches("/lib.pyc"),
+    )
     runfiles.not_contains_predicate(
         matching.str_endswith("/main.py"),
     )
+    runfiles.not_contains_predicate(
+        matching.str_endswith("/lib.py"),
+    )
     target.default_outputs().contains_at_least_predicates([
         matching.file_path_matches("/main.pyc"),
     ])
@@ -146,130 +232,6 @@
         matching.file_basename_equals("main.py"),
     )
 
-def _test_precompile_if_generated(name):
-    if not rp_config.enable_pystar:
-        rt_util.skip_test(name = name)
-        return
-    rt_util.helper_target(
-        py_binary,
-        name = name + "_subject",
-        srcs = [
-            "main.py",
-            rt_util.empty_file("generated1.py"),
-        ],
-        main = "main.py",
-        precompile = "if_generated_source",
-    )
-    analysis_test(
-        name = name,
-        impl = _test_precompile_if_generated_impl,
-        target = name + "_subject",
-        config_settings = _COMMON_CONFIG_SETTINGS,
-    )
-
-_tests.append(_test_precompile_if_generated)
-
-def _test_precompile_if_generated_impl(env, target):
-    target = env.expect.that_target(target)
-    runfiles = target.runfiles()
-    runfiles.contains_predicate(
-        matching.str_matches("/__pycache__/generated1.fakepy-45.pyc"),
-    )
-    runfiles.not_contains_predicate(
-        matching.str_matches("main.*pyc"),
-    )
-    target.default_outputs().contains_at_least_predicates([
-        matching.file_path_matches("/__pycache__/generated1.fakepy-45.pyc"),
-    ])
-    target.default_outputs().not_contains_predicate(
-        matching.file_path_matches("main.*pyc"),
-    )
-
-def _test_omit_source_if_generated_source(name):
-    if not rp_config.enable_pystar:
-        rt_util.skip_test(name = name)
-        return
-    rt_util.helper_target(
-        py_binary,
-        name = name + "_subject",
-        srcs = [
-            "main.py",
-            rt_util.empty_file("generated2.py"),
-        ],
-        main = "main.py",
-        precompile = "enabled",
-    )
-    analysis_test(
-        name = name,
-        impl = _test_omit_source_if_generated_source_impl,
-        target = name + "_subject",
-        config_settings = _COMMON_CONFIG_SETTINGS | {
-            PRECOMPILE_SOURCE_RETENTION: "omit_if_generated_source",
-        },
-    )
-
-_tests.append(_test_omit_source_if_generated_source)
-
-def _test_omit_source_if_generated_source_impl(env, target):
-    target = env.expect.that_target(target)
-    runfiles = target.runfiles()
-    runfiles.contains_predicate(
-        matching.str_matches("/generated2.pyc"),
-    )
-    runfiles.contains_predicate(
-        matching.str_matches("__pycache__/main.fakepy-45.pyc"),
-    )
-    target.default_outputs().contains_at_least_predicates([
-        matching.file_path_matches("generated2.pyc"),
-    ])
-    target.default_outputs().contains_predicate(
-        matching.file_path_matches("__pycache__/main.fakepy-45.pyc"),
-    )
-
-def _test_precompile_add_to_runfiles_decided_elsewhere(name):
-    if not rp_config.enable_pystar:
-        rt_util.skip_test(name = name)
-        return
-    rt_util.helper_target(
-        py_binary,
-        name = name + "_binary",
-        srcs = ["bin.py"],
-        main = "bin.py",
-        deps = [name + "_lib"],
-        pyc_collection = "include_pyc",
-    )
-    rt_util.helper_target(
-        py_library,
-        name = name + "_lib",
-        srcs = ["lib.py"],
-    )
-    analysis_test(
-        name = name,
-        impl = _test_precompile_add_to_runfiles_decided_elsewhere_impl,
-        targets = {
-            "binary": name + "_binary",
-            "library": name + "_lib",
-        },
-        config_settings = _COMMON_CONFIG_SETTINGS | {
-            PRECOMPILE_ADD_TO_RUNFILES: "decided_elsewhere",
-            PRECOMPILE: "enabled",
-        },
-    )
-
-_tests.append(_test_precompile_add_to_runfiles_decided_elsewhere)
-
-def _test_precompile_add_to_runfiles_decided_elsewhere_impl(env, targets):
-    env.expect.that_target(targets.binary).runfiles().contains_at_least([
-        "{workspace}/{package}/__pycache__/bin.fakepy-45.pyc",
-        "{workspace}/{package}/__pycache__/lib.fakepy-45.pyc",
-        "{workspace}/{package}/bin.py",
-        "{workspace}/{package}/lib.py",
-    ])
-
-    env.expect.that_target(targets.library).runfiles().contains_exactly([
-        "{workspace}/{package}/lib.py",
-    ])
-
 def _test_precompiler_action(name):
     if not rp_config.enable_pystar:
         rt_util.skip_test(name = name)
@@ -306,6 +268,256 @@
         "PYTHONSAFEPATH": "1",
     })
 
+def _setup_precompile_flag_pyc_collection_attr_interaction(
+        *,
+        name,
+        pyc_collection_attr,
+        precompile_flag,
+        test_impl):
+    rt_util.helper_target(
+        py_binary,
+        name = name + "_bin",
+        srcs = ["bin.py"],
+        main = "bin.py",
+        precompile = "disabled",
+        pyc_collection = pyc_collection_attr,
+        deps = [
+            name + "_lib_inherit",
+            name + "_lib_enabled",
+            name + "_lib_disabled",
+        ],
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib_inherit",
+        srcs = ["lib_inherit.py"],
+        precompile = "inherit",
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib_enabled",
+        srcs = ["lib_enabled.py"],
+        precompile = "enabled",
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib_disabled",
+        srcs = ["lib_disabled.py"],
+        precompile = "disabled",
+    )
+    analysis_test(
+        name = name,
+        impl = test_impl,
+        target = name + "_bin",
+        config_settings = _COMMON_CONFIG_SETTINGS | {
+            PRECOMPILE: precompile_flag,
+        },
+    )
+
+def _verify_runfiles(contains_patterns, not_contains_patterns):
+    def _verify_runfiles_impl(env, target):
+        runfiles = env.expect.that_target(target).runfiles()
+        for pattern in contains_patterns:
+            runfiles.contains_predicate(matching.str_matches(pattern))
+        for pattern in not_contains_patterns:
+            runfiles.not_contains_predicate(
+                matching.str_matches(pattern),
+            )
+
+    return _verify_runfiles_impl
+
+def _test_precompile_flag_enabled_pyc_collection_attr_include_pyc(name):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    _setup_precompile_flag_pyc_collection_attr_interaction(
+        name = name,
+        precompile_flag = "enabled",
+        pyc_collection_attr = "include_pyc",
+        test_impl = _verify_runfiles(
+            contains_patterns = [
+                "__pycache__/lib_enabled.*.pyc",
+                "__pycache__/lib_inherit.*.pyc",
+            ],
+            not_contains_patterns = [
+                "/bin*.pyc",
+                "/lib_disabled*.pyc",
+            ],
+        ),
+    )
+
+_tests.append(_test_precompile_flag_enabled_pyc_collection_attr_include_pyc)
+
+# buildifier: disable=function-docstring-header
+def _test_precompile_flag_enabled_pyc_collection_attr_disabled(name):
+    """Verify that a binary can opt-out of using implicit pycs even when
+    precompiling is enabled by default.
+    """
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    _setup_precompile_flag_pyc_collection_attr_interaction(
+        name = name,
+        precompile_flag = "enabled",
+        pyc_collection_attr = "disabled",
+        test_impl = _verify_runfiles(
+            contains_patterns = [
+                "__pycache__/lib_enabled.*.pyc",
+            ],
+            not_contains_patterns = [
+                "/bin*.pyc",
+                "/lib_disabled*.pyc",
+                "/lib_inherit.*.pyc",
+            ],
+        ),
+    )
+
+_tests.append(_test_precompile_flag_enabled_pyc_collection_attr_disabled)
+
+# buildifier: disable=function-docstring-header
+def _test_precompile_flag_disabled_pyc_collection_attr_include_pyc(name):
+    """Verify that a binary can opt-in to using pycs even when precompiling is
+    disabled by default."""
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    _setup_precompile_flag_pyc_collection_attr_interaction(
+        name = name,
+        precompile_flag = "disabled",
+        pyc_collection_attr = "include_pyc",
+        test_impl = _verify_runfiles(
+            contains_patterns = [
+                "__pycache__/lib_enabled.*.pyc",
+                "__pycache__/lib_inherit.*.pyc",
+            ],
+            not_contains_patterns = [
+                "/bin*.pyc",
+                "/lib_disabled*.pyc",
+            ],
+        ),
+    )
+
+_tests.append(_test_precompile_flag_disabled_pyc_collection_attr_include_pyc)
+
+def _test_precompile_flag_disabled_pyc_collection_attr_disabled(name):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    _setup_precompile_flag_pyc_collection_attr_interaction(
+        name = name,
+        precompile_flag = "disabled",
+        pyc_collection_attr = "disabled",
+        test_impl = _verify_runfiles(
+            contains_patterns = [
+                "__pycache__/lib_enabled.*.pyc",
+            ],
+            not_contains_patterns = [
+                "/bin*.pyc",
+                "/lib_disabled*.pyc",
+                "/lib_inherit.*.pyc",
+            ],
+        ),
+    )
+
+_tests.append(_test_precompile_flag_disabled_pyc_collection_attr_disabled)
+
+# buildifier: disable=function-docstring-header
+def _test_pyc_collection_disabled_library_omit_source(name):
+    """Verify that, when a binary doesn't include implicit pyc files, libraries
+    that set omit_source still have the py source file included.
+    """
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    rt_util.helper_target(
+        py_binary,
+        name = name + "_subject",
+        srcs = ["bin.py"],
+        main = "bin.py",
+        deps = [name + "_lib"],
+        pyc_collection = "disabled",
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib",
+        srcs = ["lib.py"],
+        precompile = "inherit",
+        precompile_source_retention = "omit_source",
+    )
+    analysis_test(
+        name = name,
+        impl = _test_pyc_collection_disabled_library_omit_source_impl,
+        target = name + "_subject",
+        config_settings = _COMMON_CONFIG_SETTINGS,
+    )
+
+def _test_pyc_collection_disabled_library_omit_source_impl(env, target):
+    contains_patterns = [
+        "/lib.py",
+        "/bin.py",
+    ]
+    not_contains_patterns = [
+        "/lib.*pyc",
+        "/bin.*pyc",
+    ]
+    runfiles = env.expect.that_target(target).runfiles()
+    for pattern in contains_patterns:
+        runfiles.contains_predicate(matching.str_matches(pattern))
+    for pattern in not_contains_patterns:
+        runfiles.not_contains_predicate(
+            matching.str_matches(pattern),
+        )
+
+_tests.append(_test_pyc_collection_disabled_library_omit_source)
+
+def _test_pyc_collection_include_dep_omit_source(name):
+    if not rp_config.enable_pystar:
+        rt_util.skip_test(name = name)
+        return
+    rt_util.helper_target(
+        py_binary,
+        name = name + "_subject",
+        srcs = ["bin.py"],
+        main = "bin.py",
+        deps = [name + "_lib"],
+        precompile = "disabled",
+        pyc_collection = "include_pyc",
+    )
+    rt_util.helper_target(
+        py_library,
+        name = name + "_lib",
+        srcs = ["lib.py"],
+        precompile = "inherit",
+        precompile_source_retention = "omit_source",
+    )
+    analysis_test(
+        name = name,
+        impl = _test_pyc_collection_include_dep_omit_source_impl,
+        target = name + "_subject",
+        config_settings = _COMMON_CONFIG_SETTINGS,
+    )
+
+def _test_pyc_collection_include_dep_omit_source_impl(env, target):
+    contains_patterns = [
+        "/lib.pyc",
+    ]
+    not_contains_patterns = [
+        "/lib.py",
+    ]
+    runfiles = env.expect.that_target(target).runfiles()
+    for pattern in contains_patterns:
+        runfiles.contains_predicate(matching.str_endswith(pattern))
+    for pattern in not_contains_patterns:
+        runfiles.not_contains_predicate(
+            matching.str_endswith(pattern),
+        )
+
+_tests.append(_test_pyc_collection_include_dep_omit_source)
+
+def runfiles_contains_at_least_predicates(runfiles, predicates):
+    for predicate in predicates:
+        runfiles.contains_predicate(predicate)
+
 def precompile_test_suite(name):
     test_suite(
         name = name,
diff --git a/tests/support/support.bzl b/tests/support/support.bzl
index 150ca7f..7358a6b 100644
--- a/tests/support/support.bzl
+++ b/tests/support/support.bzl
@@ -32,9 +32,9 @@
 
 # str() around Label() is necessary because rules_testing's config_settings
 # doesn't accept yet Label objects.
+ADD_SRCS_TO_RUNFILES = str(Label("//python/config_settings:add_srcs_to_runfiles"))
 EXEC_TOOLS_TOOLCHAIN = str(Label("//python/config_settings:exec_tools_toolchain"))
 PRECOMPILE = str(Label("//python/config_settings:precompile"))
-PRECOMPILE_ADD_TO_RUNFILES = str(Label("//python/config_settings:precompile_add_to_runfiles"))
 PRECOMPILE_SOURCE_RETENTION = str(Label("//python/config_settings:precompile_source_retention"))
 PYC_COLLECTION = str(Label("//python/config_settings:pyc_collection"))
 PYTHON_VERSION = str(Label("//python/config_settings:python_version"))