Skip to content

Commit c8c009c

Browse files
committed
fix(py_binary): include runfiles.symlinks/root_symlinks in python_zip_file
The legacy zipapp manifest builder only iterated `runfiles.files`, silently dropping any entries contributed via `ctx.runfiles(symlinks=...)` or `ctx.runfiles(root_symlinks=...)`. This surfaced under bazel 9 / rules_shell 0.6 because `@bazel_tools//tools/bash/runfiles:runfiles` became an alias whose legacy path is exposed via a root_symlink, so sh_binary deps of py_binary failed to bootstrap from the produced zip with "cannot find bazel_tools/tools/bash/runfiles/runfiles.bash".
1 parent a6e7d34 commit c8c009c

8 files changed

Lines changed: 136 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ END_UNRELEASED_TEMPLATE
7474
* (uv) use the astral.sh mirror as the preferred url for binary downloads,
7575
with github.com as a fallback; for uv >= 0.11.0, read the checksums directly
7676
from the dist-manifest contents.
77+
* (py_binary) The `python_zip_file` output group now includes runfiles
78+
contributed via `ctx.runfiles(symlinks=...)` and `ctx.runfiles(root_symlinks=...)`,
79+
which were previously dropped. This notably restores `sh_binary` data deps
80+
that rely on `@bazel_tools//tools/bash/runfiles` under bazel 9 / rules_shell 0.6+.
7781

7882
{#v0-0-0-added}
7983
### Added

python/private/py_executable.bzl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,22 @@ def _create_zip_file(ctx, *, output, zip_main, runfiles):
10181018

10191019
manifest.add_all(runfiles.files, map_each = map_zip_runfiles, allow_closure = True)
10201020

1021+
def map_zip_symlinks(entry):
1022+
# symlinks are workspace-relative, so prefix with the workspace name.
1023+
return _get_zip_runfiles_path(entry.path, workspace_name) + "=" + entry.target_file.path
1024+
1025+
def map_zip_root_symlinks(entry):
1026+
# root_symlinks are runfiles-root-relative; no workspace prefix.
1027+
return _get_zip_runfiles_path(entry.path) + "=" + entry.target_file.path
1028+
1029+
manifest.add_all(runfiles.symlinks, map_each = map_zip_symlinks, allow_closure = True)
1030+
manifest.add_all(runfiles.root_symlinks, map_each = map_zip_root_symlinks, allow_closure = True)
1031+
1032+
symlink_targets = [
1033+
entry.target_file
1034+
for entry in runfiles.symlinks.to_list() + runfiles.root_symlinks.to_list()
1035+
]
1036+
10211037
inputs = [zip_main]
10221038
zip_repo_mapping_manifest = maybe_create_repo_mapping(
10231039
ctx = ctx,
@@ -1037,7 +1053,7 @@ def _create_zip_file(ctx, *, output, zip_main, runfiles):
10371053
ctx.actions.run(
10381054
executable = ctx.executable._zipper,
10391055
arguments = [zip_cli_args, manifest],
1040-
inputs = depset(inputs, transitive = [runfiles.files]),
1056+
inputs = depset(inputs + symlink_targets, transitive = [runfiles.files]),
10411057
outputs = [output],
10421058
use_default_shell_env = True,
10431059
mnemonic = "PythonZipper",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2026 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
load("//python:py_binary.bzl", "py_binary")
15+
load("//python:py_test.bzl", "py_test")
16+
load(":symlink_data.bzl", "symlink_data")
17+
18+
symlink_data(
19+
name = "symlink_data",
20+
root_symlinked = "root_symlinked_source.txt",
21+
symlinked = "symlinked_source.txt",
22+
)
23+
24+
py_binary(
25+
name = "bin",
26+
srcs = ["bin.py"],
27+
data = [":symlink_data"],
28+
)
29+
30+
filegroup(
31+
name = "bin_zip",
32+
testonly = True,
33+
srcs = [":bin"],
34+
output_group = "python_zip_file",
35+
)
36+
37+
py_test(
38+
name = "zip_contents_test",
39+
srcs = ["zip_contents_test.py"],
40+
data = [":bin_zip"],
41+
env = {
42+
"ZIP_RLOCATION": "$(rlocationpath :bin_zip)",
43+
},
44+
deps = ["//python/runfiles"],
45+
)

tests/zip_runfiles_symlinks/bin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("hello")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
via-root-symlink
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2026 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Test rule that exposes data via runfiles.symlinks / runfiles.root_symlinks."""
15+
16+
def _symlink_data_impl(ctx):
17+
return [DefaultInfo(
18+
runfiles = ctx.runfiles(
19+
symlinks = {
20+
"symlink_data/via_symlink.txt": ctx.file.symlinked,
21+
},
22+
root_symlinks = {
23+
"via_root_symlink.txt": ctx.file.root_symlinked,
24+
},
25+
),
26+
)]
27+
28+
symlink_data = rule(
29+
implementation = _symlink_data_impl,
30+
attrs = {
31+
"root_symlinked": attr.label(allow_single_file = True, mandatory = True),
32+
"symlinked": attr.label(allow_single_file = True, mandatory = True),
33+
},
34+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
via-symlink
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import os
2+
import unittest
3+
import zipfile
4+
5+
from python.runfiles import runfiles
6+
7+
8+
class ZipContentsTest(unittest.TestCase):
9+
def setUp(self):
10+
super().setUp()
11+
rf = runfiles.Create()
12+
zip_rlocation = os.environ["ZIP_RLOCATION"]
13+
zip_path = rf.Rlocation(zip_rlocation)
14+
self.assertIsNotNone(zip_path, msg=f"Could not find zip at {zip_rlocation}")
15+
with zipfile.ZipFile(zip_path) as zf:
16+
self.names = set(zf.namelist())
17+
18+
def assertInZip(self, expected):
19+
self.assertIn(
20+
expected,
21+
self.names,
22+
msg=f"Expected {expected!r} in zip; got: {sorted(self.names)}",
23+
)
24+
25+
def test_runfiles_symlink_is_present(self):
26+
self.assertInZip("runfiles/_main/symlink_data/via_symlink.txt")
27+
28+
def test_runfiles_root_symlink_is_present(self):
29+
self.assertInZip("runfiles/via_root_symlink.txt")
30+
31+
32+
if __name__ == "__main__":
33+
unittest.main()

0 commit comments

Comments
 (0)