desktop AppImage: de-bundle host-coupled graphics libs (fixes black window)
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m14s

Tauri's AppImage bundles the build host's graphics/display stack
(libEGL/libGL/libdrm/libgbm/libwayland-* + Mesa dri drivers) into usr/lib.
On many end-user systems (Arch/CachyOS, NVIDIA, Wayland) those clash with
the running kernel driver + Mesa and abort with EGL_BAD_PARAMETER -> a black
window (issue 2021). They load before any renderer choice, so the runtime
env fallbacks can't rescue it; per the AppImage excludelist they must come
from the host.

Add a CI-only post-build step (desktop/packaging/appimage/debundle-graphics.sh)
that extracts the built AppImage, strips exactly that graphics/display subset
(keeping webkit/gtk bundled for portability), and repackages in place so the
app falls through to the system's graphics libs. Wired into desktop.yml
between the Tauri build and the artifact upload.

Task 2023. Makes the AppImage the zero-install taste-test vehicle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-24 18:10:02 -04:00
co-authored by Claude Opus 4.8
parent 98d918dda2
commit b08cdb92b5
2 changed files with 127 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env bash
#
# De-bundle host-coupled graphics/display libraries from the built Tauri AppImage.
#
# WHY: Tauri's AppImage bundles the BUILD host's libEGL/libGL/libdrm/libgbm/
# libwayland-* into usr/lib. On many end-user systems (Arch/CachyOS, NVIDIA,
# Wayland) those bundled copies clash with the running kernel driver + Mesa and
# abort with "Could not create default EGL display: EGL_BAD_PARAMETER" -> a black
# window (issue 2021). They load before any renderer choice, so the runtime env
# fallbacks (WEBKIT_DISABLE_DMABUF_RENDERER etc.) cannot rescue it. Per the
# AppImage excludelist these libraries MUST come from the host. We strip exactly
# that graphics/display subset -- keeping webkit/gtk/javascriptcore bundled for
# portability -- so the dynamic loader falls through to the system copies (the
# AppRun keeps usr/lib first on LD_LIBRARY_PATH, then system paths).
#
# CI-only post-build step: extract -> remove libs -> repackage, overwriting the
# original AppImage in place so the desktop.yml artifact glob emits the
# de-bundled one.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
BUNDLE_DIR="$REPO_ROOT/desktop/src-tauri/target/release/bundle/appimage"
# appimagetool is published only under the rolling "continuous" tag (the project
# cuts no semver releases), so this URL is the pinned distribution channel.
# Preferred source is Tauri's own already-downloaded copy (see below); this is
# the network fallback.
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
# Graphics/display libraries that must come from the host, per the AppImage
# excludelist (github.com/AppImage/pkg2appimage/blob/master/excludelist), plus
# the libwayland-* family that slips in through the GTK/WebKit dependency chain.
# Globs (.so*) catch every versioned variant. Only the GPU/driver stack is
# stripped; webkit/gtk stay bundled.
REMOVE_GLOBS=(
'libGL.so*' 'libEGL.so*' 'libGLdispatch.so*' 'libGLX.so*' 'libOpenGL.so*'
'libglapi.so*' 'libgbm.so*' 'libdrm.so*'
'libxcb.so*' 'libxcb-dri2.so*' 'libxcb-dri3.so*'
'libX11.so*' 'libX11-xcb.so*'
'libwayland-client.so*' 'libwayland-cursor.so*'
'libwayland-egl.so*' 'libwayland-server.so*'
)
echo "==> Locating built AppImage under $BUNDLE_DIR"
shopt -s nullglob
appimages=("$BUNDLE_DIR"/*.AppImage)
if [ ${#appimages[@]} -eq 0 ]; then
echo "ERROR: no .AppImage in $BUNDLE_DIR (did the tauri build run?)" >&2
exit 1
fi
APPIMAGE="${appimages[0]}"
echo " $APPIMAGE"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
echo "==> Extracting AppImage (FUSE-free)"
cp "$APPIMAGE" "$WORK/app.AppImage"
chmod +x "$WORK/app.AppImage"
(cd "$WORK" && ./app.AppImage --appimage-extract >/dev/null)
LIBDIR="$WORK/squashfs-root/usr/lib"
[ -d "$LIBDIR" ] || {
echo "ERROR: no usr/lib in the extracted AppImage" >&2
exit 1
}
echo "==> Removing host-coupled graphics/display libraries"
before=$(find "$LIBDIR" -maxdepth 1 -type f -name '*.so*' | wc -l)
removed=0
for pat in "${REMOVE_GLOBS[@]}"; do
for f in "$LIBDIR"/$pat; do
echo " - $(basename "$f")"
rm -f "$f"
removed=$((removed + 1))
done
done
if [ -d "$LIBDIR/dri" ]; then
n=$(find "$LIBDIR/dri" -type f | wc -l)
echo " - dri/ (${n} Mesa GPU drivers)"
rm -rf "$LIBDIR/dri"
removed=$((removed + 1))
fi
after=$(find "$LIBDIR" -maxdepth 1 -type f -name '*.so*' | wc -l)
echo " removed ${removed} entries (usr/lib .so files: ${before} -> ${after})"
if [ "$removed" -eq 0 ]; then
echo "WARNING: nothing removed -- excludelist libs were not bundled here." >&2
fi
echo "==> Locating appimagetool"
# Prefer the copy Tauri already downloaded during the build (no network, and
# version-matched to the toolchain that produced the AppImage).
APPIMAGETOOL="$(find "$REPO_ROOT/desktop/src-tauri/target" -name 'appimagetool-*.AppImage' -type f 2>/dev/null | head -n1 || true)"
if [ -z "${APPIMAGETOOL:-}" ]; then
echo " not cached by Tauri; downloading from continuous channel"
APPIMAGETOOL="$WORK/appimagetool"
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o "$APPIMAGETOOL" "$APPIMAGETOOL_URL"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$APPIMAGETOOL" "$APPIMAGETOOL_URL"
else
echo "ERROR: need curl or wget to fetch appimagetool" >&2
exit 1
fi
else
echo " reusing Tauri's copy: $APPIMAGETOOL"
fi
chmod +x "$APPIMAGETOOL"
echo "==> Repackaging AppImage (overwriting original)"
rm -f "$APPIMAGE"
# APPIMAGE_EXTRACT_AND_RUN lets appimagetool (itself an AppImage) run without
# FUSE in the CI container; ARCH is required by the embedded runtime.
export APPIMAGE_EXTRACT_AND_RUN=1
ARCH=x86_64 "$APPIMAGETOOL" "$WORK/squashfs-root" "$APPIMAGE"
echo "==> Done: de-bundled AppImage at"
ls -lh "$APPIMAGE"