Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
111 changes: 111 additions & 0 deletions tests/install/test_install_script.py
Original file line number Diff line number Diff line change
@@ -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-<unset>}\" > \"$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, "<unset>")


if __name__ == "__main__":
unittest.main(verbosity=2)