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").
diff --git a/news/zipapp-absolute-source-symlinks.fixed.md b/news/zipapp-absolute-source-symlinks.fixed.md
new file mode 100644
index 0000000..2cc2b2f
--- /dev/null
+++ b/news/zipapp-absolute-source-symlinks.fixed.md
@@ -0,0 +1,4 @@
+(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.
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