Skip to content
Open
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
3 changes: 3 additions & 0 deletions .github/workflows/release-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ jobs:
gettext \
libadwaita-1-dev \
libepoxy-dev \
libgdk-pixbuf2.0-bin \
libgtk-4-dev \
librsvg2-common \
libxml2-utils \
libwebkitgtk-6.0-dev \
meson \
Expand Down Expand Up @@ -90,6 +92,7 @@ jobs:
env:
APPIMAGE_EXTRACT_AND_RUN: "1"
LIMUX_MAX_GLIBC: "2.39"
LIMUX_REQUIRE_SVG_LOADER: "1"
run: ./scripts/package.sh "${{ steps.version.outputs.version }}"

- name: Upload packages to release
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release-rpm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ jobs:
gettext \
libadwaita-1-dev \
libepoxy-dev \
libgdk-pixbuf2.0-bin \
libgtk-4-dev \
librsvg2-common \
libxml2-utils \
libwebkitgtk-6.0-dev \
meson \
Expand All @@ -72,6 +74,7 @@ jobs:
- name: Build RPM package
env:
LIMUX_MAX_GLIBC: "2.39"
LIMUX_REQUIRE_SVG_LOADER: "1"
run: ./scripts/package.sh "${{ steps.version.outputs.version }}"

- name: Resolve RPM path
Expand Down
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ rg -n "PaneCallbacks \{" rust/limux-host-linux/src/win
`ghostty_surface_new` call.
- **Vendored `ghostty/` is read-only.** Work through the C API in
`ghostty/include/ghostty.h`.
- **AppImage must ship the gdk-pixbuf SVG loader.** Limux's toolbar
uses `-symbolic` SVG icons; without `libpixbufloader-svg.so` and
its `librsvg-2.so.2` closure inside the AppImage's `usr/lib/`,
GTK falls back to the broken-image glyph on hosts that don't have
the loader installed system-wide (Fedora 44+, minimal containers).
The bundling logic lives in `scripts/package.sh` and is guarded by
`assert_pixbuf_svg_loader_bundle`; gate releases on
`LIMUX_REQUIRE_SVG_LOADER=1` to make a missing loader a hard
failure.
- **Clippy is a hard gate** (`-D warnings`). Fix lints, don't suppress.
- **Don't commit** `target/` or other build artifacts.

Expand Down
149 changes: 149 additions & 0 deletions scripts/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,30 @@ assert_no_legacy_host_entrypoint() {
fi
}

assert_pixbuf_svg_loader_bundle() {
local appdir="$1"
local loader_dir_rel="$2"
local cache_dir_rel="$3"

if ! ls "${appdir}/${loader_dir_rel}/"libpixbufloader[-_]svg.so >/dev/null 2>&1; then
echo "ERROR: AppImage is missing libpixbufloader-svg.so under ${loader_dir_rel}/"
exit 1
fi
if [ ! -f "${appdir}/usr/lib/librsvg-2.so.2" ]; then
echo "ERROR: AppImage is missing librsvg-2.so.2 (loader closure copy failed?)"
exit 1
fi
if [ ! -s "${appdir}/${cache_dir_rel}/loaders.cache.template" ]; then
echo "ERROR: AppImage is missing loaders.cache.template under ${cache_dir_rel}/"
exit 1
fi
if ! grep -q "@LOADER_DIR@" "${appdir}/${cache_dir_rel}/loaders.cache.template"; then
echo "ERROR: loaders.cache.template missing @LOADER_DIR@ placeholder — AppRun substitution will fail"
exit 1
fi
echo "Verified bundled SVG pixbuf loader, librsvg-2.so.2, and loaders.cache.template"
}

install_desktop_file() {
local src="$1"
local dest="$2"
Expand Down Expand Up @@ -764,13 +788,138 @@ if [ -f "$APP_ICONS_DIR/256.png" ]; then
cp "$APP_ICONS_DIR/256.png" "$APPDIR/.DirIcon"
fi

# Bundle gdk-pixbuf SVG loader + librsvg closure. Without these, symbolic SVG
# icons (the pane action toolbar) fail to render on hosts that don't ship
# rsvg-pixbuf-loader — e.g. Fedora 44+, which dropped the package — because
# the AppImage's bundled libgdk_pixbuf has no loader to delegate to.
# See https://github.com/am-will/limux/issues/80
PIXBUF_LOADER_DIR_REL="usr/lib/gdk-pixbuf-2.0/2.10.0/loaders"
PIXBUF_CACHE_DIR_REL="usr/lib/gdk-pixbuf-2.0/2.10.0"
PIXBUF_SVG_LOADER=""

# Map `uname -m` to the Debian multiarch tuple. dpkg-architecture is the
# authoritative source on Debian/Ubuntu; the case statement is a fallback for
# hosts without dpkg (Fedora, Arch, …) where the multiarch path is unused
# anyway. Also covers /usr/lib/gdk-pixbuf-2.0/... (no arch infix) used by Arch.
PIXBUF_MULTIARCH=""
if command -v dpkg-architecture >/dev/null 2>&1; then
PIXBUF_MULTIARCH="$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null || true)"
fi
if [ -z "$PIXBUF_MULTIARCH" ]; then
case "$(uname -m)" in
x86_64) PIXBUF_MULTIARCH="x86_64-linux-gnu" ;;
aarch64) PIXBUF_MULTIARCH="aarch64-linux-gnu" ;;
i386|i486|i586|i686) PIXBUF_MULTIARCH="i386-linux-gnu" ;;
armv7l|armv7) PIXBUF_MULTIARCH="arm-linux-gnueabihf" ;;
armv6l) PIXBUF_MULTIARCH="arm-linux-gnueabi" ;;
*) PIXBUF_MULTIARCH="$(uname -m)-linux-gnu" ;;
esac
fi

for candidate in \
/usr/lib/${PIXBUF_MULTIARCH}/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-svg.so \
/usr/lib/${PIXBUF_MULTIARCH}/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader_svg.so \
/usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-svg.so \
/usr/lib64/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader_svg.so \
/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-svg.so \
/usr/lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader_svg.so
do
if [ -f "$candidate" ]; then
PIXBUF_SVG_LOADER="$candidate"
break
fi
done

if [ -n "$PIXBUF_SVG_LOADER" ]; then
mkdir -p "$APPDIR/$PIXBUF_LOADER_DIR_REL"
cp "$PIXBUF_SVG_LOADER" "$APPDIR/$PIXBUF_LOADER_DIR_REL/"

# Drag in librsvg-2 and its closure — the loader dlopens it at runtime.
copy_appimage_library_closure "$APPDIR/usr/lib" "$PIXBUF_SVG_LOADER"

# Generate a relocatable loaders.cache template. AppRun substitutes
# @LOADER_DIR@ with the live mount path at runtime. Use a tab delimiter
# for sed since neither the AppDir path nor "@LOADER_DIR@" can contain
# a literal tab.
# Debian/Ubuntu ship `gdk-pixbuf-query-loaders` under the multiarch lib
# dir, not in $PATH — check there before falling back to PATH.
QUERY_LOADERS=""
for candidate in \
/usr/lib/${PIXBUF_MULTIARCH}/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders \
/usr/lib64/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders \
/usr/lib/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders
do
if [ -x "$candidate" ]; then
QUERY_LOADERS="$candidate"
break
fi
done
if [ -z "$QUERY_LOADERS" ]; then
if command -v gdk-pixbuf-query-loaders >/dev/null 2>&1; then
QUERY_LOADERS=gdk-pixbuf-query-loaders
elif command -v gdk-pixbuf-query-loaders-64 >/dev/null 2>&1; then
QUERY_LOADERS=gdk-pixbuf-query-loaders-64
fi
fi

if [ -n "$QUERY_LOADERS" ]; then
GDK_PIXBUF_MODULEDIR="$APPDIR/$PIXBUF_LOADER_DIR_REL" "$QUERY_LOADERS" \
| sed -e $'s\t'"$APPDIR/$PIXBUF_LOADER_DIR_REL"$'\t@LOADER_DIR@\tg' \
> "$APPDIR/$PIXBUF_CACHE_DIR_REL/loaders.cache.template"
else
echo "WARNING: gdk-pixbuf-query-loaders not found; AppImage SVG loader not registered."
echo " Install libgdk-pixbuf2.0-bin (Debian/Ubuntu) or gdk-pixbuf2-modules (Fedora)."
echo " AppImage will ship without SVG symbolic icons working on hosts without rsvg-pixbuf-loader."
if [ "${LIMUX_REQUIRE_SVG_LOADER:-}" = "1" ]; then
exit 1
fi
fi

# Smoke check: assert the loader bundle is wired correctly (skipped if
# the query step warned and continued without producing the template).
if [ -s "$APPDIR/$PIXBUF_CACHE_DIR_REL/loaders.cache.template" ]; then
assert_pixbuf_svg_loader_bundle "$APPDIR" "$PIXBUF_LOADER_DIR_REL" "$PIXBUF_CACHE_DIR_REL"
fi
else
echo "WARNING: libpixbufloader-svg.so not found on build host."
echo " Install rsvg-pixbuf-loader (Fedora), librsvg2-common (Debian/Ubuntu),"
echo " or librsvg (Arch) to bundle the loader. AppImage will ship without"
echo " SVG symbolic icons working on hosts that don't ship the loader (e.g. Fedora 44+)."
echo " Set LIMUX_REQUIRE_SVG_LOADER=1 to make this a hard error (used by official CI)."
if [ "${LIMUX_REQUIRE_SVG_LOADER:-}" = "1" ]; then
exit 1
fi
fi

# AppRun entry point — sets up library path and launches the binary
cat > "$APPDIR/AppRun" << 'APPRUN_EOF'
#!/bin/bash
HERE="$(dirname "$(readlink -f "$0")")"
cd "$HERE"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH:-}"
export XDG_DATA_DIRS="${HERE}/usr/share:${XDG_DATA_DIRS:-/usr/share}"

# Activate bundled gdk-pixbuf SVG loader by materializing loaders.cache with
# the current mount path. Written to $XDG_CACHE_HOME so it works on a
# read-only FUSE-mounted AppImage. See packaging step that bundles the loader.
# Only export GDK_PIXBUF_MODULE_FILE if mkdir and sed both succeed — otherwise
# pointing at a missing/stale cache silently breaks SVG rendering.
PIXBUF_DIR="${HERE}/usr/lib/gdk-pixbuf-2.0/2.10.0"
if [ -f "${PIXBUF_DIR}/loaders.cache.template" ] && [ -d "${PIXBUF_DIR}/loaders" ]; then
LIMUX_CACHE_BASE="${XDG_CACHE_HOME:-$HOME/.cache}"
LIMUX_CACHE="${LIMUX_CACHE_BASE}/limux"
PIXBUF_CACHE="${LIMUX_CACHE}/pixbuf-loaders.cache.$$"
if [ -n "${LIMUX_CACHE_BASE}" ] \
&& mkdir -p "$LIMUX_CACHE" 2>/dev/null \
&& sed -e $'s\t@LOADER_DIR@\t'"${PIXBUF_DIR}/loaders"$'\tg' \
"${PIXBUF_DIR}/loaders.cache.template" > "${PIXBUF_CACHE}" 2>/dev/null \
&& [ -s "${PIXBUF_CACHE}" ]; then
export GDK_PIXBUF_MODULE_FILE="${PIXBUF_CACHE}"
fi
unset LIMUX_CACHE_BASE LIMUX_CACHE PIXBUF_CACHE
fi
unset PIXBUF_DIR

export WEBKIT_EXEC_PATH="${HERE}/usr/lib/webkitgtk-6.0"
export WEBKIT_INJECTED_BUNDLE_PATH="${HERE}/usr/lib/webkitgtk-6.0/injected-bundle"
exec "${HERE}/usr/bin/limux" "$@"
Expand Down
170 changes: 170 additions & 0 deletions scripts/tests/test-package-svg-loader.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env bash
# Unit tests for the AppImage SVG-loader bundling logic in scripts/package.sh.
#
# Tests are pure-shell. They isolate the relevant blocks so they can be
# exercised without running a full package build.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PACKAGE_SH="$SCRIPT_DIR/../package.sh"

PASS=0
FAIL=0

pass() { PASS=$((PASS+1)); printf ' ✓ %s\n' "$1"; }
fail() { FAIL=$((FAIL+1)); printf ' ✗ %s\n %s\n' "$1" "$2"; }

# ----- T1: PIXBUF_MULTIARCH mapping per uname -m -----

multiarch_for() {
# Replicates the case branch from package.sh.
case "$1" in
x86_64) echo "x86_64-linux-gnu" ;;
aarch64) echo "aarch64-linux-gnu" ;;
i386|i486|i586|i686) echo "i386-linux-gnu" ;;
armv7l|armv7) echo "arm-linux-gnueabihf" ;;
armv6l) echo "arm-linux-gnueabi" ;;
*) echo "$1-linux-gnu" ;;
esac
}

echo "T1: PIXBUF_MULTIARCH mapping"
for input in x86_64:x86_64-linux-gnu \
aarch64:aarch64-linux-gnu \
i686:i386-linux-gnu \
i386:i386-linux-gnu \
armv7l:arm-linux-gnueabihf \
armv6l:arm-linux-gnueabi \
unknownarch:unknownarch-linux-gnu
do
arch="${input%%:*}"
expected="${input##*:}"
actual="$(multiarch_for "$arch")"
if [ "$actual" = "$expected" ]; then
pass "uname -m=$arch → $expected"
else
fail "uname -m=$arch" "expected '$expected', got '$actual'"
fi
done

# ----- T2: sed delimiter handles paths with `|` -----

echo 'T2: sed substitution with "|" in source path'
HEREDOC_TMP=$(mktemp -d)
trap 'rm -rf "$HEREDOC_TMP"' EXIT

# Simulate a build path containing a `|` (rare but legal).
EVIL_APPDIR="$HEREDOC_TMP/build|dir"
mkdir -p "$EVIL_APPDIR"
PIXBUF_LOADER_DIR_REL="usr/lib/gdk-pixbuf-2.0/2.10.0/loaders"
mkdir -p "$EVIL_APPDIR/$PIXBUF_LOADER_DIR_REL"

# Fake gdk-pixbuf-query-loaders output containing the abs path.
QUERY_OUT="\"$EVIL_APPDIR/$PIXBUF_LOADER_DIR_REL/libpixbufloader_svg.so\""
TEMPLATE=$(printf '%s\n' "$QUERY_OUT" | sed -e $'s\t'"$EVIL_APPDIR/$PIXBUF_LOADER_DIR_REL"$'\t@LOADER_DIR@\tg')

if [[ "$TEMPLATE" == "\"@LOADER_DIR@/libpixbufloader_svg.so\"" ]]; then
pass "tab-delimited sed handles '|' in source path"
else
fail "tab-delimited sed with '|' in path" "got: $TEMPLATE"
fi

# Test that the old pipe-delimited sed would have failed.
if ! BAD=$(printf '%s\n' "$QUERY_OUT" | sed "s|$EVIL_APPDIR/$PIXBUF_LOADER_DIR_REL|@LOADER_DIR@|g" 2>/dev/null) \
|| [[ "$BAD" == *"build|dir"* ]]; then
pass "regression: pipe-delimited sed fails with '|' in path (expected)"
else
fail "regression check" "old sed unexpectedly worked: $BAD"
fi

# ----- T3: AppRun materializes cache only on successful mkdir+sed -----

echo "T3: AppRun GDK_PIXBUF_MODULE_FILE conditional export"

# Run the AppRun pixbuf block in isolation with read-only HOME.
run_apprun_block() {
local home_dir="$1"
local pixbuf_dir="$2"

HOME="$home_dir" XDG_CACHE_HOME="" \
PIXBUF_DIR="$pixbuf_dir" \
bash -c '
if [ -f "${PIXBUF_DIR}/loaders.cache.template" ] && [ -d "${PIXBUF_DIR}/loaders" ]; then
LIMUX_CACHE_BASE="${XDG_CACHE_HOME:-$HOME/.cache}"
LIMUX_CACHE="${LIMUX_CACHE_BASE}/limux"
PIXBUF_CACHE="${LIMUX_CACHE}/pixbuf-loaders.cache.$$"
if [ -n "${LIMUX_CACHE_BASE}" ] \
&& mkdir -p "$LIMUX_CACHE" 2>/dev/null \
&& sed -e $'"'"'s\t@LOADER_DIR@\t'"'"'"${PIXBUF_DIR}/loaders"$'"'"'\tg'"'"' \
"${PIXBUF_DIR}/loaders.cache.template" > "${PIXBUF_CACHE}" 2>/dev/null \
&& [ -s "${PIXBUF_CACHE}" ]; then
export GDK_PIXBUF_MODULE_FILE="${PIXBUF_CACHE}"
fi
fi
# Report whether the export happened.
echo "${GDK_PIXBUF_MODULE_FILE:-NOT_SET}"
'
}

# Setup a valid pixbuf dir with template + loaders.
PIXBUF_DIR="$HEREDOC_TMP/pixbuf"
mkdir -p "$PIXBUF_DIR/loaders"
printf '"@LOADER_DIR@/libpixbufloader_svg.so"\n' > "$PIXBUF_DIR/loaders.cache.template"

# Case 1: writable HOME → export should happen
WRITABLE_HOME="$HEREDOC_TMP/home1"
mkdir -p "$WRITABLE_HOME"
result=$(run_apprun_block "$WRITABLE_HOME" "$PIXBUF_DIR")
if [[ "$result" == *"pixbuf-loaders.cache."* ]] && [[ "$result" != "NOT_SET" ]]; then
pass "export with writable HOME"
else
fail "export with writable HOME" "got: $result"
fi

# Case 2: read-only HOME (mkdir fails) → export should NOT happen
RO_HOME="$HEREDOC_TMP/home2_ro"
mkdir -p "$RO_HOME"
chmod 555 "$RO_HOME"
result=$(run_apprun_block "$RO_HOME" "$PIXBUF_DIR")
chmod 755 "$RO_HOME"
if [[ "$result" == "NOT_SET" ]]; then
pass "skip export on read-only HOME"
else
fail "skip export on read-only HOME" "expected NOT_SET, got: $result"
fi

# ----- T4: LIMUX_REQUIRE_SVG_LOADER hard-fail vs warn-only -----

echo "T4: LIMUX_REQUIRE_SVG_LOADER gates exit 1"

# Excerpt the warn-or-exit logic — paste-equivalent to the script.
warn_or_exit() {
local require="${1:-}"
echo "WARNING: simulated loader missing"
if [ "${require:-}" = "1" ]; then
return 99
fi
return 0
}

ec=0
warn_or_exit "" >/dev/null 2>&1 || ec=$?
if [ "$ec" = "0" ]; then
pass "no env var → warn and continue (exit 0)"
else
fail "warn-only path" "expected exit 0, got $ec"
fi

ec=0
warn_or_exit "1" >/dev/null 2>&1 || ec=$?
if [ "$ec" = "99" ]; then
pass "LIMUX_REQUIRE_SVG_LOADER=1 → hard fail (exit 99)"
else
fail "hard-fail path" "expected exit 99, got $ec"
fi

# ----- Summary -----

echo ""
echo "Results: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ]