From 1d389dd0b76fe506fc0ce36094ad6a060d52fb00 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Fri, 30 Oct 2026 10:44:15 -0400 Subject: [PATCH 1/2] fix(pypi): generate namespace package initializers without a shell Wheel namespace packages use an __init__.py file generated from a checked-in template. Skylib's copy_file invokes cp through a shell on Unix, which fails on execution platforms that provide neither program. Use expand_template with no substitutions. This produces a regular file with the same bytes from the same template, without spawning a process on the execution platform. The namespace initializer generator was introduced in https://github.com/bazel-contrib/rules_python/commit/dce512024. --- news/wheel-namespace-initializers.fixed.md | 1 + python/private/pypi/BUILD.bazel | 2 +- python/private/pypi/namespace_pkgs.bzl | 13 +++++++------ .../namespace_pkgs/namespace_pkgs_tests.bzl | 17 ++++++++++------- 4 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 news/wheel-namespace-initializers.fixed.md diff --git a/news/wheel-namespace-initializers.fixed.md b/news/wheel-namespace-initializers.fixed.md new file mode 100644 index 0000000000..1edbf74111 --- /dev/null +++ b/news/wheel-namespace-initializers.fixed.md @@ -0,0 +1 @@ +(pypi) Wheel namespace package initializers no longer require a shell or `cp` on the execution platform. diff --git a/python/private/pypi/BUILD.bazel b/python/private/pypi/BUILD.bazel index 149074dd3f..1cf0cad986 100644 --- a/python/private/pypi/BUILD.bazel +++ b/python/private/pypi/BUILD.bazel @@ -229,7 +229,7 @@ bzl_library( bzl_library( name = "namespace_pkgs", srcs = ["namespace_pkgs.bzl"], - deps = ["@bazel_skylib//rules:copy_file"], + deps = ["@bazel_skylib//rules:expand_template"], ) bzl_library( diff --git a/python/private/pypi/namespace_pkgs.bzl b/python/private/pypi/namespace_pkgs.bzl index be6244efc7..7ecfe3033f 100644 --- a/python/private/pypi/namespace_pkgs.bzl +++ b/python/private/pypi/namespace_pkgs.bzl @@ -1,6 +1,6 @@ """Utilities to get where we should write namespace pkg paths.""" -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") +load("@bazel_skylib//rules:expand_template.bzl", "expand_template") _ext = struct( py = ".py", @@ -59,7 +59,7 @@ def get_files(*, srcs, ignored_dirnames = [], root = None): return sorted([d for d in dirs if d not in ignored]) -def create_inits(*, srcs, ignored_dirnames = [], root = None, copy_file = copy_file, **kwargs): +def create_inits(*, srcs, ignored_dirnames = [], root = None, write_init = expand_template, **kwargs): """Create init files and return the list to be included `py_library` srcs. Args: @@ -67,8 +67,8 @@ def create_inits(*, srcs, ignored_dirnames = [], root = None, copy_file = copy_f as `srcs` and `data`. This is usually a result of a {obj}`glob`. ignored_dirnames: {type}`str` a list of patterns to ignore. root: {type}`str` the prefix to use as the root. - copy_file: the `copy_file` rule to copy files in build context. - **kwargs: passed to {obj}`copy_file`. + write_init: rule that generates an initializer from the namespace template. + **kwargs: passed to `write_init`. Returns: {type}`list[str]` to be included as part of `py_library`. @@ -78,11 +78,12 @@ def create_inits(*, srcs, ignored_dirnames = [], root = None, copy_file = copy_f src = "{}/__init__.py".format(out) ret.append(src) - copy_file( + write_init( # For the target name, use a number instead of trying to convert an output # path into a valid label. name = "_cp_{}_namespace".format(i), - src = _TEMPLATE, + template = _TEMPLATE, + substitutions = {}, out = src, **kwargs ) diff --git a/tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl b/tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl index 9c382d070c..391b13fd66 100644 --- a/tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl +++ b/tests/pypi/namespace_pkgs/namespace_pkgs_tests.bzl @@ -167,33 +167,36 @@ def _test_create_inits(env): "nested/root/foo/buu/__init__.py", "nested/root/foo/buu/bii.py", ] - copy_file_calls = [] + write_init_calls = [] template = Label("//python/private/pypi:namespace_pkg_tmpl.py") got = create_inits( srcs = srcs, root = "nested/root", - copy_file = lambda **kwargs: copy_file_calls.append(kwargs), + write_init = lambda **kwargs: write_init_calls.append(kwargs), ) env.expect.that_collection(got).contains_exactly([ call["out"] - for call in copy_file_calls + for call in write_init_calls ]) - env.expect.that_collection(copy_file_calls).contains_exactly([ + env.expect.that_collection(write_init_calls).contains_exactly([ { "name": "_cp_0_namespace", "out": "nested/root/foo/__init__.py", - "src": template, + "substitutions": {}, + "template": template, }, { "name": "_cp_1_namespace", "out": "nested/root/foo/bar/__init__.py", - "src": template, + "substitutions": {}, + "template": template, }, { "name": "_cp_2_namespace", "out": "nested/root/foo/bee/__init__.py", - "src": template, + "substitutions": {}, + "template": template, }, ]) From 601ffc8a7f265a685353ae3f4bfb9f4d7504a05e Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Fri, 30 Oct 2026 11:14:57 -0400 Subject: [PATCH 2/2] docs(pypi): explain the namespace initializer template action --- python/private/pypi/namespace_pkgs.bzl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/private/pypi/namespace_pkgs.bzl b/python/private/pypi/namespace_pkgs.bzl index 7ecfe3033f..d58d1f2094 100644 --- a/python/private/pypi/namespace_pkgs.bzl +++ b/python/private/pypi/namespace_pkgs.bzl @@ -78,6 +78,8 @@ def create_inits(*, srcs, ignored_dirnames = [], root = None, write_init = expan src = "{}/__init__.py".format(out) ret.append(src) + # With no substitutions, Bazel writes the initializer verbatim. Skylib's + # copy_file would require a shell and cp on the Unix execution platform. write_init( # For the target name, use a number instead of trying to convert an output # path into a valid label.