fix(linux): make AppImage work on Mesa 25+ / GLib 2.88 distros (#1567)

This commit is contained in:
Will Pfleger
2026-07-08 13:18:48 -04:00
committed by GitHub
parent f968601ef6
commit 563dab92e4
3 changed files with 275 additions and 30 deletions
+101 -24
View File
@@ -455,14 +455,66 @@ jobs:
name: Release Linux
if: github.repository == 'block/buzz'
runs-on: ubuntu-latest
# Digest-pinned like the SHA-pinned actions below; Renovate keeps it fresh.
container: ubuntu:22.04@sha256:0e0a0fc6d18feda9db1590da249ac93e8d5abfea8f4c3c0c849ce512b5ef8982
needs: setup
timeout-minutes: 60
permissions:
contents: write
env:
# AppImage tools (linuxdeploy, appimagetool) are themselves AppImages.
# Containers lack FUSE, so we must use the extract-and-run fallback.
APPIMAGE_EXTRACT_AND_RUN: "1"
outputs:
archive_name: ${{ steps.linux-artifacts.outputs.archive_name }}
sig: ${{ steps.read-sig.outputs.sig }}
steps:
- name: Install system dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: |
# Must run first: bare ubuntu:22.04 ships without curl, wget, git, or
# ca-certificates. activate-hermit bootstraps via curl+HTTPS (needs
# both), and actions/checkout falls back to a REST tarball without git.
# Running as root — no sudo needed.
apt-get update \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30
apt-get install -y --no-install-recommends \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30 \
-o DPkg::Lock::Timeout=120 \
build-essential \
ca-certificates \
curl \
file \
git \
libasound2-dev \
libayatana-appindicator3-dev \
libgtk-3-dev \
librsvg2-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
patchelf \
pkg-config \
wget
# Install GitHub CLI — preinstalled on runners but absent in containers.
# wget and ca-certificates are now available from the step above.
mkdir -p -m 755 /etc/apt/keyrings
wget -q --tries=3 --timeout=30 -O /usr/share/keyrings/githubcli-archive-keyring.gpg \
https://cli.github.com/packages/githubcli-archive-keyring.gpg
# Pin the keyring like appimagetool below. If GitHub rotates the
# keyring this fails loudly — recompute and update the hash.
echo "6084d5d7bd8e288441e0e94fc6275570895da18e6751f70f057485dc2d1a811b /usr/share/keyrings/githubcli-archive-keyring.gpg" | sha256sum -c
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list
apt-get update
apt-get install -y --no-install-recommends gh
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event_name == 'push' && github.ref || inputs.ref }}
@@ -475,31 +527,40 @@ jobs:
workspaces: desktop/src-tauri
lookup-only: true
- name: Install Tauri dependencies (Linux)
env:
DEBIAN_FRONTEND: noninteractive
- name: Install appimagetool
run: |
sudo apt-get update \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30
sudo apt-get install -y --no-install-recommends \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30 \
-o DPkg::Lock::Timeout=120 \
build-essential \
curl \
file \
libasound2-dev \
libayatana-appindicator3-dev \
libgtk-3-dev \
librsvg2-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
patchelf \
wget
# Pin to an immutable release tag to avoid supply-chain drift from the
# mutable `continuous` tag. Tag: 1.9.1, asset: appimagetool-<arch>.AppImage
# (https://github.com/AppImage/appimagetool/releases/tag/1.9.1)
case "$(uname -m)" in
x86_64) ARCH_SUFFIX="x86_64" ;;
aarch64) ARCH_SUFFIX="aarch64" ;;
*)
echo "::error::Unsupported architecture: $(uname -m)"
exit 1
;;
esac
wget -q --tries=3 --timeout=30 -O /tmp/appimagetool \
"https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-${ARCH_SUFFIX}.AppImage"
# SHA256 integrity check. Refuse to run an unverified binary: if a new
# arch (e.g. aarch64) is enabled in CI, compute its hash and add it here.
if [[ "$ARCH_SUFFIX" == "x86_64" ]]; then
echo "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 /tmp/appimagetool" | sha256sum -c
else
echo "::error::No pinned SHA256 for appimagetool-${ARCH_SUFFIX} — add it before enabling this architecture"
exit 1
fi
install -m 755 /tmp/appimagetool /usr/local/bin/appimagetool
# appimagetool otherwise fetches the AppImage type2 runtime from the
# MUTABLE `continuous` tag at repack time — the runtime is the first
# code users execute, so pin it too. Tag: 20251108, hash is for the
# x86_64 asset (non-x86_64 already hard-fails above).
# (https://github.com/AppImage/type2-runtime/releases/tag/20251108)
wget -q --tries=3 --timeout=30 -O /tmp/appimage-runtime \
"https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-${ARCH_SUFFIX}"
echo "2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d /tmp/appimage-runtime" | sha256sum -c
install -D -m 644 /tmp/appimage-runtime /usr/local/lib/appimage-runtime
echo "APPIMAGETOOL_RUNTIME_FILE=/usr/local/lib/appimage-runtime" >> "$GITHUB_ENV"
- name: Install desktop dependencies
run: just desktop-install-ci
@@ -531,6 +592,22 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
- name: Fix AppImage (remove infra libs, symlink system GStreamer)
run: |
mapfile -t APPIMAGES < <(find desktop/src-tauri/target/release/bundle/appimage -name '*.AppImage' -type f)
if [[ ${#APPIMAGES[@]} -eq 0 ]]; then
echo "::error::No AppImage found to post-process"
exit 1
fi
if [[ ${#APPIMAGES[@]} -gt 1 ]]; then
echo "::error::Expected exactly one AppImage, found ${#APPIMAGES[@]}: ${APPIMAGES[*]}"
exit 1
fi
bash desktop/scripts/fix-appimage.sh "${APPIMAGES[0]}"
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
- name: Locate Linux build artifacts
id: linux-artifacts
run: |
+16 -6
View File
@@ -174,8 +174,9 @@ Each release produces two GitHub releases:
(macOS).
2. **`buzz-desktop-latest`** — a rolling pre-release for the Tauri
auto-updater containing `latest.json`, the signed `.tar.gz` archive,
and its `.sig` signature.
auto-updater containing `latest.json` and each platform's signed
updater artifact plus its `.sig` signature (`.tar.gz` on macOS,
`.AppImage` on Linux, and `_alpha-unsigned.exe` on Windows).
---
@@ -187,6 +188,15 @@ Silicon (`darwin-aarch64`, the `release` job) and Intel
`.AppImage`. Both macOS DMGs are codesigned, notarized, and attached to
the same `v<version>` release. Intel users download the `_x64.dmg`.
The Linux AppImage is post-processed by `desktop/scripts/fix-appimage.sh`,
which strips infra libraries over-bundled by linuxdeploy (they crash on
Mesa 25+ / GLib 2.88 distros — see
[tauri-apps/tauri#15665](https://github.com/tauri-apps/tauri/issues/15665))
and re-signs the artifact. As a result the AppImage relies on the
host's Wayland/GStreamer/graphics stack and requires GLib >= 2.72
(Ubuntu 22.04 or newer). The `release-linux` job builds inside a
`ubuntu:22.04` container for broad GLIBC compatibility.
---
## Prerequisites
@@ -219,7 +229,7 @@ The version string must be valid semver: `MAJOR.MINOR.PATCH` with an optional pr
### Auto-updater reports "no update available"
Verify that the `buzz-desktop-latest` release exists and contains a
valid `latest.json`. The auto-updater manifest currently lists
`darwin-aarch64` only, so Intel and Linux users do not yet receive
auto-updates — they download new versions manually from the release
page. (Adding their entries to `latest.json` is a follow-up.)
valid `latest.json`. The manifest covers all four platform keys
(`darwin-aarch64`, `darwin-x86_64`, `linux-x86_64`,
`windows-x86_64`); a missing entry usually means that platform's
release job failed — check the workflow run.
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# fix-appimage.sh — Remove infra libs from a Tauri-produced AppImage that crash
# on Mesa 25+ / GLib 2.88 distros (Ubuntu 26.04, Fedora 42+, etc.).
#
# Usage: fix-appimage.sh <path-to.AppImage>
#
# Set TAURI_SIGNING_PRIVATE_KEY / TAURI_SIGNING_PRIVATE_KEY_PASSWORD to
# re-sign after repacking (CI release builds). Without them the script
# repacks but skips signing, which is fine for local testing.
#
# Set APPIMAGETOOL_RUNTIME_FILE to a pre-downloaded AppImage type2 runtime to
# avoid appimagetool fetching one from its mutable `continuous` tag (CI pins
# this; unset is fine for local testing).
#
# Root cause — three interlocking failures (upstream: https://github.com/tauri-apps/tauri/issues/15665):
#
# 1. EGL crash: linuxdeploy bundles libwayland-client.so.0 (1.22) alongside
# the app. Mesa 25's libEGL calls the bundled version at runtime; the version
# skew causes eglGetDisplay to return EGL_BAD_PARAMETER under Wayland, which
# WebKitWebProcess treats as fatal and aborts before the window ever appears.
#
# 2. GStreamer crash: linuxdeploy also bundles libgst*.so* (GStreamer core libs).
# AppRun unconditionally sets GST_PLUGIN_SYSTEM_PATH_1_0 to a dir inside the
# AppImage that the bundler never populates (bundleMediaFramework is false by
# default), so GStreamer's plugin discovery yields an empty registry. The
# "GStreamer element appsink not found" error kills the render process; as a
# side effect the broken run poisons ~/.cache/gstreamer-1.0/registry.x86_64.bin.
#
# 3. WebKit helper mismatch (latent): the bundled WebKit helpers
# (WebKitNetworkProcess/WebKitWebProcess) have RUNPATH=$ORIGIN only, and
# linuxdeploy string-patches /usr -> ././ inside libwebkit2gtk so the helper
# dir is resolved relative to the process cwd. AppRun's chdir($APPDIR/usr)
# makes this work; any launch that bypasses AppRun (extracted-AppDir usage,
# repack workflows, dbus/systemd activation with cwd=/) resolves the helpers
# wrong -- spawning nothing, dying on unresolved bundled libs, or spawning
# the system helpers -- and the window never appears.
#
# Fix: remove the offending libs so the app uses the system copies (which are
# newer and ABI-compatible on any distro shipping glib >= 2.72 / Ubuntu 22.04+),
# and symlink the system GStreamer plugin directory so discovery works correctly.
# No tauri.conf.json knob can do this — bundle.linux.appimage only exposes
# bundleMediaFramework, files (copy-only, no remove/symlink), and bundleXdgOpen.
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: fix-appimage.sh <path-to.AppImage>" >&2
exit 1
fi
if [[ ! -f "$1" ]]; then
echo "Error: file not found: $1" >&2
exit 1
fi
APPIMAGE_ABS="$(realpath "$1")"
APPIMAGE_DIR="$(dirname "$APPIMAGE_ABS")"
APPIMAGE_NAME="$(basename "$APPIMAGE_ABS")"
# Locate the desktop/ directory (this script lives at desktop/scripts/).
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DESKTOP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Detect multiarch triplet for GStreamer plugin path.
case "$(uname -m)" in
x86_64) MULTIARCH="x86_64-linux-gnu" ;;
aarch64) MULTIARCH="aarch64-linux-gnu" ;;
*)
echo "Error: unsupported architecture: $(uname -m)" >&2
exit 1
;;
esac
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
echo "==> Extracting $APPIMAGE_NAME"
(cd "$WORKDIR" && APPIMAGE_EXTRACT_AND_RUN=1 "$APPIMAGE_ABS" --appimage-extract)
LIBDIR="$WORKDIR/squashfs-root/usr/lib"
# Guard against a bundler layout change: if the primary offending lib is not
# where we expect it, the rm globs below would silently no-op and we'd ship
# an unfixed artifact. Fail loudly instead so a tauri/linuxdeploy upgrade
# that changes the bundled lib set gets noticed here, not by users.
if ! compgen -G "$LIBDIR/libwayland-client.so*" > /dev/null; then
echo "Error: libwayland-client not found in $LIBDIR — bundler layout changed; update fix-appimage.sh" >&2
exit 1
fi
echo "==> Removing infra libs that conflict with system Mesa / GLib / GStreamer"
rm -f \
"$LIBDIR"/libwayland-client.so* \
"$LIBDIR"/libwayland-cursor.so* \
"$LIBDIR"/libwayland-egl.so* \
"$LIBDIR"/libwayland-server.so* \
"$LIBDIR"/libglib-2.0.so* \
"$LIBDIR"/libgio-2.0.so* \
"$LIBDIR"/libgobject-2.0.so* \
"$LIBDIR"/libgmodule-2.0.so* \
"$LIBDIR"/libmount.so* \
"$LIBDIR"/libblkid.so* \
"$LIBDIR"/libselinux.so* \
"$LIBDIR"/libpcre2-8.so* \
"$LIBDIR"/libgst*.so* \
"$LIBDIR"/libzstd.so* \
"$LIBDIR"/libelf.so* \
"$LIBDIR"/libffi.so*
echo "==> Symlinking system GStreamer plugin directory"
# On distros without the Debian multiarch layout (e.g. Arch), this symlink
# dangles — GStreamer then falls back to its default plugin discovery, which
# is a safe degradation (unlike the original empty in-bundle dir).
rm -rf "$LIBDIR/gstreamer-1.0"
ln -s "/usr/lib/$MULTIARCH/gstreamer-1.0" "$LIBDIR/gstreamer-1.0"
echo "==> Repacking AppImage"
# Pass a pinned type2 runtime when provided (CI sets APPIMAGETOOL_RUNTIME_FILE);
# without it appimagetool downloads the runtime from its mutable `continuous`
# tag at repack time — acceptable for local testing, not for release builds.
RUNTIME_ARGS=()
if [[ -n "${APPIMAGETOOL_RUNTIME_FILE:-}" ]]; then
RUNTIME_ARGS=(--runtime-file "$APPIMAGETOOL_RUNTIME_FILE")
fi
APPIMAGE_EXTRACT_AND_RUN=1 ARCH="$(uname -m)" appimagetool \
"${RUNTIME_ARGS[@]}" \
"$WORKDIR/squashfs-root" "$APPIMAGE_ABS"
# Re-sign after repack so the updater can verify the artifact.
# Tauri 2.11 with createUpdaterArtifacts=true produces two possible formats:
# New: <name>.AppImage + <name>.AppImage.sig (sign the AppImage directly)
# Old: <name>.AppImage.tar.gz + .tar.gz.sig (tar-wrapped, then signed)
# We handle both: always re-sign the AppImage; if a .tar.gz sibling exists
# alongside it, recreate it from the freshly repacked AppImage and re-sign that.
# Our release config pins createUpdaterArtifacts: true (build-release-config.mjs),
# so the tar.gz branch is dead in CI today — kept deliberately because the
# workflow's artifact-locate step prefers a tar.gz when one exists; dropping
# this branch could publish a stale tarball containing the unfixed AppImage.
if [[ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]]; then
# `tauri signer sign` reads TAURI_SIGNING_PRIVATE_KEY and
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD from the environment (same as the
# macOS jobs in release.yml) — never pass the password via argv, where
# it would be visible in /proc/<pid>/cmdline.
echo "==> Re-signing AppImage"
(cd "$DESKTOP_DIR" && pnpm tauri signer sign "$APPIMAGE_ABS")
TARBALL="$APPIMAGE_ABS.tar.gz"
if [[ -f "$TARBALL" ]]; then
echo "==> Recreating updater archive $TARBALL"
tar -czf "$TARBALL" -C "$APPIMAGE_DIR" "$APPIMAGE_NAME"
echo "==> Re-signing updater archive"
(cd "$DESKTOP_DIR" && pnpm tauri signer sign "$TARBALL")
fi
else
echo "==> TAURI_SIGNING_PRIVATE_KEY not set — skipping signing (local build)"
fi
echo "==> Done: $APPIMAGE_ABS"