refactor: introduce helper for defining distribution filegroups (#4152)

Previously, distribution filegroups required manually enumerating each
direct subpackage's distribution target in `srcs`. This created
repetitive boilerplate across BUILD files and risked omitting newly
added subpackages from release distributions.

Introduce the `distribution_filegroup` macro to automatically glob
package files and discover direct subpackages using
`native.subpackages()`, with support for an `exclude` list.

* Also updates BUILD files across the repository to use the new macro
diff --git a/command_line_option/BUILD.bazel b/command_line_option/BUILD.bazel
index 15c22f3..088a817 100644
--- a/command_line_option/BUILD.bazel
+++ b/command_line_option/BUILD.bazel
@@ -4,6 +4,8 @@
 # treated as aliases for `//command_line_option:XXX` psuedo-targets. They
 # are not actual flags or have any value.
 
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
+
 package(
     default_visibility = ["//visibility:public"],
 )
@@ -23,8 +25,6 @@
     actual = "//python:none",
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//:__subpackages__"],
 )
diff --git a/python/BUILD.bazel b/python/BUILD.bazel
index a4dd98c..f2fb827 100644
--- a/python/BUILD.bazel
+++ b/python/BUILD.bazel
@@ -29,31 +29,15 @@
 # gazelle:resolve starlark //python:current_py_toolchain.bzl //python:current_py_toolchain_bzl
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 load(":current_py_toolchain.bzl", "current_py_toolchain")
 
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]) + [
-        "//python/api:distribution",
-        "//python/bin:distribution",
-        "//python/cc:distribution",
-        "//python/config_settings:distribution",
-        "//python/constraints:distribution",
-        "//python/entry_points:distribution",
-        "//python/extensions:distribution",
-        "//python/local_toolchains:distribution",
-        "//python/pip_install:distribution",
-        "//python/private:distribution",
-        "//python/runfiles:distribution",
-        "//python/runtime_env_toolchains:distribution",
-        "//python/uv:distribution",
-        "//python/zipapp:distribution",
-    ],
-    visibility = ["//:__pkg__"],
 )
 
 # ========= bzl_library targets end =========
diff --git a/python/api/BUILD.bazel b/python/api/BUILD.bazel
index 93c70f4..8ef51e5 100644
--- a/python/api/BUILD.bazel
+++ b/python/api/BUILD.bazel
@@ -13,14 +13,14 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
 package(
     default_visibility = ["//:__subpackages__"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
 )
 
 bzl_library(
diff --git a/python/bin/BUILD.bazel b/python/bin/BUILD.bazel
index 30af7d1..91ad9c2 100644
--- a/python/bin/BUILD.bazel
+++ b/python/bin/BUILD.bazel
@@ -1,10 +1,11 @@
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 load("//python/private:interpreter.bzl", _interpreter_binary = "interpreter_binary")
 load("//python/private:repl.bzl", "py_repl_binary")
 
-filegroup(
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//:__subpackages__"],
 )
 
 _interpreter_binary(
diff --git a/python/cc/BUILD.bazel b/python/cc/BUILD.bazel
index 2fa03dc..8df3d55 100644
--- a/python/cc/BUILD.bazel
+++ b/python/cc/BUILD.bazel
@@ -4,6 +4,7 @@
 load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
 load("//python/private:current_py_cc_headers.bzl", "current_py_cc_headers", "current_py_cc_headers_abi3")
 load("//python/private:current_py_cc_libs.bzl", "current_py_cc_libs")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
 package(
     default_visibility = ["//:__subpackages__"],
@@ -47,9 +48,8 @@
     visibility = ["//visibility:public"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
 )
 
 bzl_library(
diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel
index 5eb0eaf..a540d1d 100644
--- a/python/config_settings/BUILD.bazel
+++ b/python/config_settings/BUILD.bazel
@@ -1,6 +1,7 @@
 load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
 load("@pythons_hub//:versions.bzl", "DEFAULT_PYTHON_VERSION", "MINOR_MAPPING", "PYTHON_VERSIONS")
 load("@rules_python_internal//:rules_python_config.bzl", "config")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 load(
     "//python/private:flags.bzl",
     "AddSrcsToRunfilesFlag",
@@ -19,15 +20,15 @@
 load("//python/private/pypi:flags.bzl", "define_pypi_internal_flags")
 load(":config_settings.bzl", "construct_config_settings")
 
+package(default_visibility = ["//:__subpackages__"])
+
 # We don't generate bzl_library for these because they aren't public targets
 # and should be moved
 # gazelle:exclude config_settings.bzl
 # gazelle:exclude transition.bzl
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__pkg__"],
 )
 
 construct_config_settings(
diff --git a/python/constraints/BUILD.bazel b/python/constraints/BUILD.bazel
index 969c867..dba6c0c 100644
--- a/python/constraints/BUILD.bazel
+++ b/python/constraints/BUILD.bazel
@@ -12,14 +12,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
+
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__pkg__"],
 )
 
 # A constraint_setting to use for constraints related to the location of the
diff --git a/python/entry_points/BUILD.bazel b/python/entry_points/BUILD.bazel
index 7cfd0cd..509a967 100644
--- a/python/entry_points/BUILD.bazel
+++ b/python/entry_points/BUILD.bazel
@@ -13,6 +13,9 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
+
+package(default_visibility = ["//:__subpackages__"])
 
 exports_files(
     [
@@ -21,10 +24,8 @@
     visibility = ["//docs:__subpackages__"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__subpackages__"],
 )
 
 bzl_library(
diff --git a/python/extensions/BUILD.bazel b/python/extensions/BUILD.bazel
index af18fa1..95bd7c1 100644
--- a/python/extensions/BUILD.bazel
+++ b/python/extensions/BUILD.bazel
@@ -13,15 +13,14 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__pkg__"],
 )
 
 bzl_library(
diff --git a/python/local_toolchains/BUILD.bazel b/python/local_toolchains/BUILD.bazel
index 256dffb..f5870ba 100644
--- a/python/local_toolchains/BUILD.bazel
+++ b/python/local_toolchains/BUILD.bazel
@@ -1,10 +1,10 @@
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
 package(default_visibility = ["//:__subpackages__"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
 )
 
 bzl_library(
diff --git a/python/pip_install/BUILD.bazel b/python/pip_install/BUILD.bazel
index b3c1ab1..d259734 100644
--- a/python/pip_install/BUILD.bazel
+++ b/python/pip_install/BUILD.bazel
@@ -13,15 +13,14 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
 package(
     default_visibility = ["//:__subpackages__"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__pkg__"],
 )
 
 filegroup(
diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel
index 9e4313d..b4ff84f 100644
--- a/python/private/BUILD.bazel
+++ b/python/private/BUILD.bazel
@@ -17,6 +17,7 @@
 load("//python:py_binary.bzl", "py_binary")
 load("//python:py_library.bzl", "py_library")
 load(":bazel_config_mode.bzl", "bazel_config_mode")
+load(":distribution_filegroup.bzl", "distribution_filegroup")
 load(":py_exec_tools_toolchain.bzl", "current_interpreter_executable")
 load(":py_interpreter_program.bzl", "py_interpreter_program")
 load(":sentinel_impl.bzl", "sentinel")
@@ -38,17 +39,8 @@
     "runtimes_manifest_workspace.bzl",
 ])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]) + [
-        "//python/private/api:distribution",
-        "//python/private/cc:distribution",
-        "//python/private/pypi:distribution",
-        "//python/private/whl_filegroup:distribution",
-        "//python/private/zipapp:distribution",
-        "//tools/build_defs/python/private:distribution",
-    ],
-    visibility = ["//python:__pkg__"],
 )
 
 filegroup(
@@ -943,6 +935,11 @@
 )
 
 bzl_library(
+    name = "distribution_filegroup",
+    srcs = ["distribution_filegroup.bzl"],
+)
+
+bzl_library(
     name = "enum",
     srcs = ["enum.bzl"],
 )
diff --git a/python/private/api/BUILD.bazel b/python/private/api/BUILD.bazel
index cd7eda1..7c48e35 100644
--- a/python/private/api/BUILD.bazel
+++ b/python/private/api/BUILD.bazel
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC")
 load(":py_common_api.bzl", "py_common_api")
 
@@ -20,9 +21,8 @@
     default_visibility = ["//:__subpackages__"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
 )
 
 py_common_api(
diff --git a/python/private/cc/BUILD.bazel b/python/private/cc/BUILD.bazel
index 087d406..8aef130 100644
--- a/python/private/cc/BUILD.bazel
+++ b/python/private/cc/BUILD.bazel
@@ -1,5 +1,6 @@
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
 load("@rules_cc//cc:cc_library.bzl", "cc_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC")
 
 package(
@@ -8,9 +9,8 @@
 
 licenses(["notice"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
 )
 
 # An empty cc target for use when a cc target is needed to satisfy
diff --git a/python/private/distribution_filegroup.bzl b/python/private/distribution_filegroup.bzl
new file mode 100644
index 0000000..b02cee6
--- /dev/null
+++ b/python/private/distribution_filegroup.bzl
@@ -0,0 +1,26 @@
+"""Helper for defining distribution filegroups."""
+
+def distribution_filegroup(name, exclude = None):
+    """Defines a filegroup target for repository distribution.
+
+    Args:
+        name: The name of the filegroup target.
+        exclude: Optional list of subpackage patterns to exclude from automatic
+            subpackage discovery.
+    """
+    exclude = exclude or []
+    pkg = native.package_name()
+    prefix = ("//" + pkg + "/") if pkg else "//"
+
+    srcs = native.glob(["**"])
+    for subpkg in native.subpackages(
+        include = ["*"],
+        exclude = exclude,
+        allow_empty = True,
+    ):
+        srcs.append(prefix + subpkg + ":distribution")
+
+    native.filegroup(
+        name = name,
+        srcs = srcs,
+    )
diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel
index eb87c63..96b68e7 100644
--- a/python/private/pypi/BUILD.bazel
+++ b/python/private/pypi/BUILD.bazel
@@ -14,6 +14,7 @@
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
 load("//python:py_binary.bzl", "py_binary")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
 package(default_visibility = ["//:__subpackages__"])
 
@@ -70,16 +71,8 @@
     visibility = ["//:__subpackages__"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(
-        ["**"],
-        exclude = ["requirements.txt"],
-    ) + [
-        "//python/private/pypi/dependency_resolver:distribution",
-        "//python/private/pypi/whl_installer:distribution",
-    ],
-    visibility = ["//python/private:__pkg__"],
 )
 
 # Filegroup of bzl files that can be used by downstream rules for documentation generation
diff --git a/python/private/pypi/dependency_resolver/BUILD.bazel b/python/private/pypi/dependency_resolver/BUILD.bazel
index 9531b55..ade6472 100644
--- a/python/private/pypi/dependency_resolver/BUILD.bazel
+++ b/python/private/pypi/dependency_resolver/BUILD.bazel
@@ -1,7 +1,9 @@
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
+
+package(default_visibility = ["//:__subpackages__"])
+
 exports_files(["dependency_resolver.py"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python/private/pypi:__subpackages__"],
 )
diff --git a/python/private/pypi/requirements_parser/BUILD.bazel b/python/private/pypi/requirements_parser/BUILD.bazel
index e69de29..59d178b 100644
--- a/python/private/pypi/requirements_parser/BUILD.bazel
+++ b/python/private/pypi/requirements_parser/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
+
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
+    name = "distribution",
+)
diff --git a/python/private/pypi/whl_installer/BUILD.bazel b/python/private/pypi/whl_installer/BUILD.bazel
index 1912b34..ae7d957 100644
--- a/python/private/pypi/whl_installer/BUILD.bazel
+++ b/python/private/pypi/whl_installer/BUILD.bazel
@@ -1,5 +1,8 @@
 load("//python:py_binary.bzl", "py_binary")
 load("//python:py_library.bzl", "py_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
+
+package(default_visibility = ["//:__subpackages__"])
 
 py_library(
     name = "lib",
@@ -26,8 +29,6 @@
     deps = [":lib"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["*"]),
-    visibility = ["//:__subpackages__"],
 )
diff --git a/python/private/whl_filegroup/BUILD.bazel b/python/private/whl_filegroup/BUILD.bazel
index 4d0a096..13ad2ad 100644
--- a/python/private/whl_filegroup/BUILD.bazel
+++ b/python/private/whl_filegroup/BUILD.bazel
@@ -1,10 +1,11 @@
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
 load("//python:py_binary.bzl", "py_binary")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
-filegroup(
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python/private:__pkg__"],
 )
 
 py_binary(
@@ -16,5 +17,4 @@
 bzl_library(
     name = "whl_filegroup",
     srcs = ["whl_filegroup.bzl"],
-    visibility = ["//python:__subpackages__"],
 )
diff --git a/python/private/zipapp/BUILD.bazel b/python/private/zipapp/BUILD.bazel
index 66f0b4f..395e1d2 100644
--- a/python/private/zipapp/BUILD.bazel
+++ b/python/private/zipapp/BUILD.bazel
@@ -1,4 +1,5 @@
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
 package(
     default_visibility = ["//:__subpackages__"],
@@ -6,9 +7,8 @@
 
 licenses(["notice"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
 )
 
 filegroup(
diff --git a/python/proto/BUILD.bazel b/python/proto/BUILD.bazel
index 4d5a92a..71d7042 100644
--- a/python/proto/BUILD.bazel
+++ b/python/proto/BUILD.bazel
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
+
 package(default_visibility = ["//visibility:public"])
 
 # Deprecated; use @com_google_protobuf//bazel/private:python_toolchain_type instead.
@@ -22,3 +24,7 @@
     actual = "@com_google_protobuf//bazel/private:python_toolchain_type",
     deprecation = "Use @com_google_protobuf//bazel/private:python_toolchain_type instead",
 )
+
+distribution_filegroup(
+    name = "distribution",
+)
diff --git a/python/runfiles/BUILD.bazel b/python/runfiles/BUILD.bazel
index 7366347..0c9e74a 100644
--- a/python/runfiles/BUILD.bazel
+++ b/python/runfiles/BUILD.bazel
@@ -15,11 +15,12 @@
 load("//python:packaging.bzl", "py_wheel")
 load("//python:py_library.bzl", "py_library")
 load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
-filegroup(
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__pkg__"],
 )
 
 filegroup(
diff --git a/python/runtime_env_toolchains/BUILD.bazel b/python/runtime_env_toolchains/BUILD.bazel
index 5001d12..5086f0a 100644
--- a/python/runtime_env_toolchains/BUILD.bazel
+++ b/python/runtime_env_toolchains/BUILD.bazel
@@ -12,14 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 load("//python/private:runtime_env_toolchain.bzl", "define_runtime_env_toolchain")
 
 package(default_visibility = ["//:__subpackages__"])
 
 define_runtime_env_toolchain(name = "runtime_env_toolchain")
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__pkg__"],
 )
diff --git a/python/uv/BUILD.bazel b/python/uv/BUILD.bazel
index 93aefbf..054c243 100644
--- a/python/uv/BUILD.bazel
+++ b/python/uv/BUILD.bazel
@@ -15,16 +15,13 @@
 # EXPERIMENTAL: This is experimental and may be removed without notice
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 load("//python/uv/private:current_toolchain.bzl", "current_toolchain")
 
 package(default_visibility = ["//:__subpackages__"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]) + [
-        "//python/uv/private:distribution",
-    ],
-    visibility = ["//:__subpackages__"],
 )
 
 toolchain_type(
diff --git a/python/uv/private/BUILD.bazel b/python/uv/private/BUILD.bazel
index e1d5204..e107419 100644
--- a/python/uv/private/BUILD.bazel
+++ b/python/uv/private/BUILD.bazel
@@ -14,14 +14,15 @@
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
 load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED")  # buildifier: disable=bzl-visibility
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
+
+package(default_visibility = ["//:__subpackages__"])
 
 # public only because this is used from a macro to template
 _NOT_REALLY_PUBLIC = ["//visibility:public"]
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python/uv:__pkg__"],
 )
 
 filegroup(
@@ -130,11 +131,9 @@
 bzl_library(
     name = "toolchain_types",
     srcs = ["toolchain_types.bzl"],
-    visibility = ["//python/uv:__subpackages__"],
 )
 
 bzl_library(
     name = "uv_toolchain_info",
     srcs = ["uv_toolchain_info.bzl"],
-    visibility = ["//python/uv:__subpackages__"],
 )
diff --git a/python/zipapp/BUILD.bazel b/python/zipapp/BUILD.bazel
index 77a8752..d8f8eeb 100644
--- a/python/zipapp/BUILD.bazel
+++ b/python/zipapp/BUILD.bazel
@@ -1,13 +1,12 @@
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")
 
 package(default_visibility = ["//visibility:public"])
 
 licenses(["notice"])
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__pkg__"],
 )
 
 bzl_library(
diff --git a/tests/distribution_filegroup/BUILD.bazel b/tests/distribution_filegroup/BUILD.bazel
new file mode 100644
index 0000000..c2174ef
--- /dev/null
+++ b/tests/distribution_filegroup/BUILD.bazel
@@ -0,0 +1,7 @@
+load(":distribution_filegroup_tests.bzl", "distribution_filegroup_test_suite")
+
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup_test_suite(
+    name = "distribution_filegroup_tests",
+)
diff --git a/tests/distribution_filegroup/child.txt b/tests/distribution_filegroup/child.txt
new file mode 100644
index 0000000..a69c0fe
--- /dev/null
+++ b/tests/distribution_filegroup/child.txt
@@ -0,0 +1 @@
+child
diff --git a/tests/distribution_filegroup/distribution_filegroup_tests.bzl b/tests/distribution_filegroup/distribution_filegroup_tests.bzl
new file mode 100644
index 0000000..b00c0f0
--- /dev/null
+++ b/tests/distribution_filegroup/distribution_filegroup_tests.bzl
@@ -0,0 +1,61 @@
+"""Tests for distribution_filegroup helper."""
+
+load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
+load("@rules_testing//lib:test_suite.bzl", "test_suite")
+load(
+    "//python/private:distribution_filegroup.bzl",
+    "distribution_filegroup",
+)  # buildifier: disable=bzl-visibility
+
+_tests = []
+
+def _test_auto_subpackages_and_default_glob(name):
+    distribution_filegroup(
+        name = name + "_subject",
+    )
+    analysis_test(
+        name = name,
+        target = name + "_subject",
+        impl = _test_auto_subpackages_and_default_glob_impl,
+    )
+
+def _test_auto_subpackages_and_default_glob_impl(env, target):
+    env.expect.that_target(target).default_outputs().contains_at_least([
+        "{package}/f1.txt",
+        "{package}/f2.txt",
+        "{package}/subpkg/BUILD.bazel",
+        "{package}/subpkg/subfile.txt",
+    ])
+
+_tests.append(_test_auto_subpackages_and_default_glob)
+
+def _test_exclude_subpackage(name):
+    distribution_filegroup(
+        name = name + "_subject",
+        exclude = ["subpkg"],
+    )
+    analysis_test(
+        name = name,
+        target = name + "_subject",
+        impl = _test_exclude_subpackage_impl,
+    )
+
+def _test_exclude_subpackage_impl(env, target):
+    env.expect.that_target(target).default_outputs().contains_at_least([
+        "{package}/f1.txt",
+        "{package}/f2.txt",
+    ])
+    env.expect.that_target(target).default_outputs().not_contains(
+        "{package}/subpkg/BUILD.bazel",
+    )
+    env.expect.that_target(target).default_outputs().not_contains(
+        "{package}/subpkg/subfile.txt",
+    )
+
+_tests.append(_test_exclude_subpackage)
+
+def distribution_filegroup_test_suite(name):
+    test_suite(
+        name = name,
+        tests = _tests,
+    )
diff --git a/tests/distribution_filegroup/dummy.ignored b/tests/distribution_filegroup/dummy.ignored
new file mode 100644
index 0000000..ea10ec8
--- /dev/null
+++ b/tests/distribution_filegroup/dummy.ignored
@@ -0,0 +1 @@
+ignored
diff --git a/tests/distribution_filegroup/f1.txt b/tests/distribution_filegroup/f1.txt
new file mode 100644
index 0000000..8e1e71d
--- /dev/null
+++ b/tests/distribution_filegroup/f1.txt
@@ -0,0 +1 @@
+f1
diff --git a/tests/distribution_filegroup/f2.txt b/tests/distribution_filegroup/f2.txt
new file mode 100644
index 0000000..9de77c1
--- /dev/null
+++ b/tests/distribution_filegroup/f2.txt
@@ -0,0 +1 @@
+f2
diff --git a/tests/distribution_filegroup/subpkg/BUILD.bazel b/tests/distribution_filegroup/subpkg/BUILD.bazel
new file mode 100644
index 0000000..7ca68bf
--- /dev/null
+++ b/tests/distribution_filegroup/subpkg/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
+
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
+    name = "distribution",
+)
diff --git a/tests/distribution_filegroup/subpkg/subfile.txt b/tests/distribution_filegroup/subpkg/subfile.txt
new file mode 100644
index 0000000..d8fc28d
--- /dev/null
+++ b/tests/distribution_filegroup/subpkg/subfile.txt
@@ -0,0 +1 @@
+subfile
diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel
index aa75c66..90026d4 100644
--- a/tools/BUILD.bazel
+++ b/tools/BUILD.bazel
@@ -11,7 +11,9 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+
 load("//python:py_binary.bzl", "py_binary")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
 
 package(default_visibility = ["//visibility:public"])
 
@@ -24,16 +26,6 @@
     deps = ["@pypi__packaging//:lib"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = [
-        "BUILD.bazel",
-        "wheelmaker.py",
-        "//tools/launcher:distribution",
-        "//tools/precompiler:distribution",
-        "//tools/private:distribution",
-        "//tools/publish:distribution",
-        "//tools/zipapp:distribution",
-    ],
-    visibility = ["//:__pkg__"],
 )
diff --git a/tools/build_defs/BUILD.bazel b/tools/build_defs/BUILD.bazel
new file mode 100644
index 0000000..7ca68bf
--- /dev/null
+++ b/tools/build_defs/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
+
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
+    name = "distribution",
+)
diff --git a/tools/build_defs/python/BUILD.bazel b/tools/build_defs/python/BUILD.bazel
index aa21042..05d6e3c 100644
--- a/tools/build_defs/python/BUILD.bazel
+++ b/tools/build_defs/python/BUILD.bazel
@@ -11,3 +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.
+
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
+
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
+    name = "distribution",
+)
diff --git a/tools/build_defs/python/private/BUILD.bazel b/tools/build_defs/python/private/BUILD.bazel
index e189b18..bba2984 100644
--- a/tools/build_defs/python/private/BUILD.bazel
+++ b/tools/build_defs/python/private/BUILD.bazel
@@ -13,11 +13,12 @@
 # limitations under the License.
 
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
 
-filegroup(
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//python:__subpackages__"],
 )
 
 # keep
diff --git a/tools/launcher/BUILD.bazel b/tools/launcher/BUILD.bazel
index aa46106..f00fcfe 100644
--- a/tools/launcher/BUILD.bazel
+++ b/tools/launcher/BUILD.bazel
@@ -12,10 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-filegroup(
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
+
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//:__subpackages__"],
 )
 
 alias(
diff --git a/tools/precompiler/BUILD.bazel b/tools/precompiler/BUILD.bazel
index e055980..153d52a 100644
--- a/tools/precompiler/BUILD.bazel
+++ b/tools/precompiler/BUILD.bazel
@@ -13,13 +13,14 @@
 # limitations under the License.
 
 load("@bazel_skylib//rules:common_settings.bzl", "string_list_flag")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
 load("//python/private:py_interpreter_program.bzl", "py_interpreter_program")  # buildifier: disable=bzl-visibility
 load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC")  # buildifier: disable=bzl-visibility
 
-filegroup(
+package(default_visibility = ["//:__subpackages__"])
+
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
-    visibility = ["//:__subpackages__"],
 )
 
 py_interpreter_program(
diff --git a/tools/publish/BUILD.bazel b/tools/publish/BUILD.bazel
index 93b44b3..d24d30b 100644
--- a/tools/publish/BUILD.bazel
+++ b/tools/publish/BUILD.bazel
@@ -1,6 +1,9 @@
 load("//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
 load("//tools/private:publish_deps.bzl", "publish_deps")
 
+package(default_visibility = ["//:__subpackages__"])
+
 py_console_script_binary(
     name = "twine",
     # We transition to a specific python version in order to ensure that we
@@ -11,17 +14,8 @@
     visibility = ["//visibility:public"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = [
-        "BUILD.bazel",
-        "pyproject.toml",
-        "requirements_darwin.txt",
-        "requirements_linux.txt",
-        "requirements_universal.txt",
-        "requirements_windows.txt",
-    ],
-    visibility = ["//tools:__subpackages__"],
 )
 
 # Run bazel run //private:requirements.update to update the outs
diff --git a/tools/zipapp/BUILD.bazel b/tools/zipapp/BUILD.bazel
index 45bd126..9bc5fae 100644
--- a/tools/zipapp/BUILD.bazel
+++ b/tools/zipapp/BUILD.bazel
@@ -1,4 +1,5 @@
 load("//python:py_library.bzl", "py_library")
+load("//python/private:distribution_filegroup.bzl", "distribution_filegroup")  # buildifier: disable=bzl-visibility
 load("//python/private:py_interpreter_program.bzl", "py_interpreter_program")  # buildifier: disable=bzl-visibility
 load("//python/private:visibility.bzl", "NOT_ACTUALLY_PUBLIC")  # buildifier: disable=bzl-visibility
 
@@ -45,7 +46,6 @@
     srcs = ["zip_main_maker.py"],
 )
 
-filegroup(
+distribution_filegroup(
     name = "distribution",
-    srcs = glob(["**"]),
 )