diff --git a/CHANGELOG.md b/CHANGELOG.md index 6317404f66..7bf5e4d73c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,10 @@ END_UNRELEASED_TEMPLATE * (uv) use the astral.sh mirror as the preferred url for binary downloads, with github.com as a fallback; for uv >= 0.11.0, read the checksums directly from the dist-manifest contents. +* (py_binary) The `python_zip_file` output group now includes runfiles + contributed via `ctx.runfiles(symlinks=...)` and `ctx.runfiles(root_symlinks=...)`, + which were previously dropped. This notably restores `sh_binary` data deps + that rely on `@bazel_tools//tools/bash/runfiles` under bazel 9 / rules_shell 0.6+. {#v0-0-0-added} ### Added diff --git a/python/private/py_executable.bzl b/python/private/py_executable.bzl index 9c21e5d274..8d7b931727 100644 --- a/python/private/py_executable.bzl +++ b/python/private/py_executable.bzl @@ -1018,6 +1018,22 @@ def _create_zip_file(ctx, *, output, zip_main, runfiles): manifest.add_all(runfiles.files, map_each = map_zip_runfiles, allow_closure = True) + def map_zip_symlinks(entry): + # symlinks are workspace-relative, so prefix with the workspace name. + return _get_zip_runfiles_path(entry.path, workspace_name) + "=" + entry.target_file.path + + def map_zip_root_symlinks(entry): + # root_symlinks are runfiles-root-relative; no workspace prefix. + return _get_zip_runfiles_path(entry.path) + "=" + entry.target_file.path + + manifest.add_all(runfiles.symlinks, map_each = map_zip_symlinks, allow_closure = True) + manifest.add_all(runfiles.root_symlinks, map_each = map_zip_root_symlinks, allow_closure = True) + + symlink_targets = depset([ + entry.target_file + for entry in runfiles.symlinks.to_list() + runfiles.root_symlinks.to_list() + ]) + inputs = [zip_main] zip_repo_mapping_manifest = maybe_create_repo_mapping( ctx = ctx, @@ -1037,7 +1053,7 @@ def _create_zip_file(ctx, *, output, zip_main, runfiles): ctx.actions.run( executable = ctx.executable._zipper, arguments = [zip_cli_args, manifest], - inputs = depset(inputs, transitive = [runfiles.files]), + inputs = depset(inputs, transitive = [runfiles.files, symlink_targets]), outputs = [output], use_default_shell_env = True, mnemonic = "PythonZipper", diff --git a/tests/zip_runfiles_symlinks/BUILD.bazel b/tests/zip_runfiles_symlinks/BUILD.bazel new file mode 100644 index 0000000000..46cbcf0bd5 --- /dev/null +++ b/tests/zip_runfiles_symlinks/BUILD.bazel @@ -0,0 +1,45 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +load("//python:py_binary.bzl", "py_binary") +load("//python:py_test.bzl", "py_test") +load(":symlink_data.bzl", "symlink_data") + +symlink_data( + name = "symlink_data", + root_symlinked = "root_symlinked_source.txt", + symlinked = "symlinked_source.txt", +) + +py_binary( + name = "bin", + srcs = ["bin.py"], + data = [":symlink_data"], +) + +filegroup( + name = "bin_zip", + testonly = True, + srcs = [":bin"], + output_group = "python_zip_file", +) + +py_test( + name = "zip_contents_test", + srcs = ["zip_contents_test.py"], + data = [":bin_zip"], + env = { + "ZIP_RLOCATION": "$(rlocationpath :bin_zip)", + }, + deps = ["//python/runfiles"], +) diff --git a/tests/zip_runfiles_symlinks/bin.py b/tests/zip_runfiles_symlinks/bin.py new file mode 100644 index 0000000000..11b15b1a45 --- /dev/null +++ b/tests/zip_runfiles_symlinks/bin.py @@ -0,0 +1 @@ +print("hello") diff --git a/tests/zip_runfiles_symlinks/root_symlinked_source.txt b/tests/zip_runfiles_symlinks/root_symlinked_source.txt new file mode 100644 index 0000000000..38262240d3 --- /dev/null +++ b/tests/zip_runfiles_symlinks/root_symlinked_source.txt @@ -0,0 +1 @@ +via-root-symlink diff --git a/tests/zip_runfiles_symlinks/symlink_data.bzl b/tests/zip_runfiles_symlinks/symlink_data.bzl new file mode 100644 index 0000000000..380af254a6 --- /dev/null +++ b/tests/zip_runfiles_symlinks/symlink_data.bzl @@ -0,0 +1,34 @@ +# Copyright 2026 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Test rule that exposes data via runfiles.symlinks / runfiles.root_symlinks.""" + +def _symlink_data_impl(ctx): + return [DefaultInfo( + runfiles = ctx.runfiles( + symlinks = { + "symlink_data/via_symlink.txt": ctx.file.symlinked, + }, + root_symlinks = { + "via_root_symlink.txt": ctx.file.root_symlinked, + }, + ), + )] + +symlink_data = rule( + implementation = _symlink_data_impl, + attrs = { + "root_symlinked": attr.label(allow_single_file = True, mandatory = True), + "symlinked": attr.label(allow_single_file = True, mandatory = True), + }, +) diff --git a/tests/zip_runfiles_symlinks/symlinked_source.txt b/tests/zip_runfiles_symlinks/symlinked_source.txt new file mode 100644 index 0000000000..4fe042b4e2 --- /dev/null +++ b/tests/zip_runfiles_symlinks/symlinked_source.txt @@ -0,0 +1 @@ +via-symlink diff --git a/tests/zip_runfiles_symlinks/zip_contents_test.py b/tests/zip_runfiles_symlinks/zip_contents_test.py new file mode 100644 index 0000000000..5240ec816e --- /dev/null +++ b/tests/zip_runfiles_symlinks/zip_contents_test.py @@ -0,0 +1,33 @@ +import os +import unittest +import zipfile + +from python.runfiles import runfiles + + +class ZipContentsTest(unittest.TestCase): + def setUp(self): + super().setUp() + rf = runfiles.Create() + zip_rlocation = os.environ["ZIP_RLOCATION"] + zip_path = rf.Rlocation(zip_rlocation) + self.assertIsNotNone(zip_path, msg=f"Could not find zip at {zip_rlocation}") + with zipfile.ZipFile(zip_path) as zf: + self.names = set(zf.namelist()) + + def assertInZip(self, expected): + self.assertIn( + expected, + self.names, + msg=f"Expected {expected!r} in zip; got: {sorted(self.names)}", + ) + + def test_runfiles_symlink_is_present(self): + self.assertInZip("runfiles/_main/symlink_data/via_symlink.txt") + + def test_runfiles_root_symlink_is_present(self): + self.assertInZip("runfiles/via_root_symlink.txt") + + +if __name__ == "__main__": + unittest.main()