Skip to content
Merged
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
107 changes: 92 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -445,42 +445,42 @@ 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:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.event_name == 'push' && github.ref || inputs.ref }}
persist-credentials: false

- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: desktop/src-tauri
lookup-only: true

- name: Install Tauri dependencies (Linux)
- name: Install system dependencies
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo apt-get update \
# 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
sudo apt-get install -y --no-install-recommends \
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 \
Expand All @@ -489,7 +489,68 @@ jobs:
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 }}
persist-credentials: false

- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: desktop/src-tauri
lookup-only: true

- name: Install appimagetool
run: |
# 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
Expand Down Expand Up @@ -521,6 +582,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: |
Expand Down
22 changes: 16 additions & 6 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

---

Expand All @@ -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
Expand Down Expand Up @@ -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 changes: 158 additions & 0 deletions desktop/scripts/fix-appimage.sh
Original file line number Diff line number Diff line change
@@ -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"