fix(zipapp): store _solib shared libraries as files, not absolute symlinks (#4211)
Since #4165, the zipper keeps a symlink for an input whose original
source is a
symlink, looking through the absolute symlink the Bazel sandbox exposes
the
input as. For the `_solib` entries Bazel creates for shared libraries,
the
original source is itself an absolute symlink into the build machine's
output
base, so the zipapp stored entries like
runfiles/_main/_solib_x86_64/libST-..._Slibgrpc.so
->
/home/<user>/.cache/bazel/<hash>/execroot/_main/bazel-out/.../libgrpc.so
which dangle on any other machine. Any zipapp whose Python code loads a
shared
library from `_solib` (a C extension that links a `cc_library`
dynamically, or
`libpython` for an extension that links it) fails to load it outside the
machine that built it. Before #4165 these files were copied.
To fix, only keep the looked-through target when it is relative, e.g.
`libpython3.10.so -> libpython3.10.so.1.0` inside the runtime, which is
what
the size optimization in #4165 is for. Otherwise the file is stored.
* Adds a zipper test with the `_solib` shape: a sandbox symlink to an
absolute
symlink to the library. It fails without the fix ("should NOT be a
symlink
but is").
(cherry picked from commit 381762af4e3e7d1c83e827aec9bb14b4c477f492)
Work towards #4232
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a008107..9d3e829 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,19 @@
directory, or view the [latest generated
changelog](https://rules-python.readthedocs.io/en/latest/changelog.html).
+{#v2-4-2}
+## [2.4.2] - 2026-10-08
+
+[2.4.2]: https://github.com/bazel-contrib/rules_python/releases/tag/2.4.2
+
+{#v2-4-2-fixed}
+### Fixed
+* (zipapp) Shared libraries that a {obj}`py_zipapp_binary` reaches through
+ Bazel's `_solib` symlinks are stored as files again, instead of as symlinks to
+ absolute paths in the build machine's output base that dangle wherever the
+ zipapp runs.
+
+
{#v2-4-1}
## [2.4.1] - 2026-10-05
diff --git a/tests/tools/zipapp/zipper_test.py b/tests/tools/zipapp/zipper_test.py
index e0ff155..4c66c7f 100644
--- a/tests/tools/zipapp/zipper_test.py
+++ b/tests/tools/zipapp/zipper_test.py
@@ -174,6 +174,36 @@
assert_zip_file_content(zf, "runfiles/my_ws/bin/python3.14", content="python")
+def test_create_zip_with_sandboxed_absolute_source_symlink(tmp_path):
+ # Bazel's _solib entries are absolute symlinks into the output base. Their
+ # target only exists on the build machine, so the file must be stored.
+ manifest_path = tmp_path / "manifest.txt"
+ output_zip = tmp_path / "output.zip"
+
+ library_dir = tmp_path / "external"
+ library_dir.mkdir()
+ library = library_dir / "libfoo.so"
+ library.write_text("library")
+
+ solib_dir = tmp_path / "solib"
+ solib_dir.mkdir()
+ solib_link = solib_dir / "libfoo.so"
+ solib_link.symlink_to(library)
+
+ sandbox_dir = tmp_path / "sandbox"
+ sandbox_dir.mkdir()
+ sandbox_link = sandbox_dir / "libfoo.so"
+ sandbox_link.symlink_to(solib_link)
+ manifest_path.write_text(f"rf-file|0|_solib/libfoo.so|{sandbox_link}")
+
+ create_zip(manifest_path, output_zip)
+
+ with zipfile.ZipFile(output_zip, "r") as zf:
+ assert_zip_file_content(
+ zf, "runfiles/my_ws/_solib/libfoo.so", content="library"
+ )
+
+
def test_pathsep_normalization(tmp_path):
manifest_path = tmp_path / "manifest.txt"
output_zip = tmp_path / "output.zip"
diff --git a/tools/zipapp/zipper.py b/tools/zipapp/zipper.py
index c0e5472..aa96160 100644
--- a/tools/zipapp/zipper.py
+++ b/tools/zipapp/zipper.py
@@ -130,8 +130,13 @@
# Bazel sandboxes expose regular inputs as absolute symlinks. Look through
# that indirection to detect whether the original source is also a symlink.
+ # Only a relative link can be kept: an absolute one points into this
+ # machine's Bazel output base (e.g. the _solib links Bazel creates for shared
+ # libraries) and would dangle wherever the zipapp runs, so store the file.
if os.path.islink(target):
- return os.readlink(target)
+ source_target = os.readlink(target)
+ if not os.path.isabs(source_target):
+ return source_target
return None