From 607bd0aa9569f07ad80334675d0200bbea88c962 Mon Sep 17 00:00:00 2001 From: Frank Liu Date: Sun, 4 Oct 2026 05:42:18 +0000 Subject: [PATCH] fix(zipapp): store _solib shared libraries as files, not absolute symlinks 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//.cache/bazel//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"). --- news/zipapp-absolute-source-symlinks.fixed.md | 4 +++ tests/tools/zipapp/zipper_test.py | 30 +++++++++++++++++++ tools/zipapp/zipper.py | 7 ++++- 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 news/zipapp-absolute-source-symlinks.fixed.md diff --git a/news/zipapp-absolute-source-symlinks.fixed.md b/news/zipapp-absolute-source-symlinks.fixed.md new file mode 100644 index 0000000000..2cc2b2fe0e --- /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 e0ff1557d5..4c66c7fc5f 100644 --- a/tests/tools/zipapp/zipper_test.py +++ b/tests/tools/zipapp/zipper_test.py @@ -174,6 +174,36 @@ def test_create_zip_with_sandboxed_source_symlink(tmp_path): 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 c0e547284a..aa96160f11 100644 --- a/tools/zipapp/zipper.py +++ b/tools/zipapp/zipper.py @@ -130,8 +130,13 @@ def _source_symlink_target(content_path, is_symlink_str): # 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