diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f07fde754..9a9dc14c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 \ @@ -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-.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 @@ -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: | diff --git a/RELEASING.md b/RELEASING.md index ea0bd897b..98d5dd9c8 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -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` 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. diff --git a/desktop/scripts/fix-appimage.sh b/desktop/scripts/fix-appimage.sh new file mode 100755 index 000000000..45f537bb7 --- /dev/null +++ b/desktop/scripts/fix-appimage.sh @@ -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 +# +# 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 " >&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: .AppImage + .AppImage.sig (sign the AppImage directly) +# Old: .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//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"