From 28005c80f221a3d7f289993e6035bb1a0c163919 Mon Sep 17 00:00:00 2001 From: Yuu <206304251+nekomario28@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:26:19 +0900 Subject: [PATCH] fix(installer): strip AppImage library paths from children --- install.sh | 37 +++++++++ tests/install/test_install_script.py | 111 +++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 tests/install/test_install_script.py diff --git a/install.sh b/install.sh index a7df8327..008f058c 100755 --- a/install.sh +++ b/install.sh @@ -30,6 +30,43 @@ # variant, a mismatched cache wheel, or development builds. set -euo pipefail +# AppImage launchers prepend bundle-private libraries to LD_LIBRARY_PATH so the +# GUI can resolve its own dependencies. That environment is inherited here by +# system children such as curl; on newer distros, system libcurl can then bind +# an older bundled libnghttp2 with the same SONAME and fail before uv bootstrap. +# Strip only entries inside APPDIR: preserve user/system paths and leave every +# non-AppImage invocation unchanged. +_sanitize_appimage_ld_library_path() { + [ -n "${APPDIR:-}" ] || return 0 + [ -n "${LD_LIBRARY_PATH:-}" ] || return 0 + + local appdir="${APPDIR%/}" + [ -n "$appdir" ] || appdir="$APPDIR" + local entry cleaned="" rest="${LD_LIBRARY_PATH}:" + local kept=0 + while [ -n "$rest" ]; do + entry="${rest%%:*}" + rest="${rest#*:}" + case "$entry" in + "$appdir"|"$appdir"/*) continue ;; + esac + if [ "$kept" -eq 1 ]; then + cleaned="$cleaned:$entry" + else + cleaned="$entry" + kept=1 + fi + done + + if [ "$kept" -eq 1 ]; then + export LD_LIBRARY_PATH="$cleaned" + else + unset LD_LIBRARY_PATH + fi +} +_sanitize_appimage_ld_library_path +unset -f _sanitize_appimage_ld_library_path + DEFAULT_WHEEL_URL="" # filled in once GitHub Releases are live DEFAULT_KERNEL_CACHE_WHEEL_URL="" # filled in once GitHub Releases are live diff --git a/tests/install/test_install_script.py b/tests/install/test_install_script.py new file mode 100644 index 00000000..e2c35280 --- /dev/null +++ b/tests/install/test_install_script.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import os +from pathlib import Path +import stat +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +INSTALL_SH = ROOT / "install.sh" + + +@unittest.skipUnless(os.name == "posix", "AppImage installer is Linux-only") +class AppImageInstallerEnvironmentTests(unittest.TestCase): + def _capture_curl_library_path( + self, + *, + ld_library_path: str, + appdir: str | None, + ) -> tuple[subprocess.CompletedProcess[str], str]: + with tempfile.TemporaryDirectory() as td: + tmp = Path(td) + fakebin = tmp / "fakebin" + fakebin.mkdir() + capture = tmp / "curl-env.txt" + + curl = fakebin / "curl" + curl.write_text( + "#!/bin/sh\n" + "printf '%s' \"${LD_LIBRARY_PATH-}\" > \"$FREETOKEN_TEST_CAPTURE\"\n" + "exit 42\n", + encoding="utf-8", + ) + curl.chmod(curl.stat().st_mode | stat.S_IXUSR) + (fakebin / "sh").symlink_to("/bin/sh") + (fakebin / "mkdir").symlink_to("/bin/mkdir") + + env = { + "HOME": str(tmp / "home"), + "PATH": str(fakebin), + "LD_LIBRARY_PATH": ld_library_path, + "FREETOKEN_TEST_CAPTURE": str(capture), + "FREETOKEN_BIN_DIR": str(tmp / "bin"), + "FREETOKEN_HOME": str(tmp / "freetoken-home"), + "FREETOKEN_ENV_DIR": str(tmp / "env"), + } + if appdir is not None: + env["APPDIR"] = appdir + + result = subprocess.run( + ["/bin/bash", str(INSTALL_SH), "--yes"], + cwd=ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + self.assertEqual( + result.returncode, + 42, + msg=f"installer did not reach the fake curl:\nstdout={result.stdout}\nstderr={result.stderr}", + ) + self.assertTrue(capture.exists(), "fake curl did not record its environment") + return result, capture.read_text(encoding="utf-8") + + def test_appimage_entries_are_removed_before_system_curl(self) -> None: + appdir = "/tmp/.mount_freetoken-test" + near_prefix = f"{appdir}-other/usr/lib" + original = ":".join( + [ + f"{appdir}/usr/lib", + "/opt/user/lib", + f"{appdir}/usr/lib/x86_64-linux-gnu", + near_prefix, + "/usr/local/lib", + ] + ) + + _, seen = self._capture_curl_library_path( + ld_library_path=original, + appdir=appdir, + ) + + self.assertEqual(seen, f"/opt/user/lib:{near_prefix}:/usr/local/lib") + + def test_non_appimage_invocation_preserves_library_path(self) -> None: + original = "/opt/user/lib:/usr/local/lib" + + _, seen = self._capture_curl_library_path( + ld_library_path=original, + appdir=None, + ) + + self.assertEqual(seen, original) + + def test_all_appimage_entries_unset_library_path(self) -> None: + appdir = "/tmp/.mount_freetoken-test" + + _, seen = self._capture_curl_library_path( + ld_library_path=f"{appdir}/usr/lib:{appdir}/usr/lib/x86_64-linux-gnu", + appdir=appdir, + ) + + self.assertEqual(seen, "") + + +if __name__ == "__main__": + unittest.main(verbosity=2)