fix(appimage): ship the compatibility launcher

This commit is contained in:
debpalash
2026-08-09 23:12:04 +00:00
parent 7f67e1622b
commit 5be4a26903
8 changed files with 101 additions and 91 deletions
+3 -1
View File
@@ -89,7 +89,9 @@ jobs:
# never been wired into CI, so its cases were a regression test nothing
# ran. Cheap (pure bash, stubs pkg-config) and it gates the class.
- name: AppImage launcher (AppRun) unit tests
run: bash frontend/src-tauri/appimage/AppRun.test.sh
run: |
bash frontend/src-tauri/appimage/AppRun.test.sh
bash scripts/inject-apprun.test.sh
# `backend/tests/` mounts routers on bare FastAPI apps (no heavy main
# import chain) with a hermetic data dir from its conftest.py. It no
+7
View File
@@ -751,6 +751,13 @@ jobs:
"$APPIMAGE" --appimage-extract >/dev/null
ROOT="$EXTRACT_DIR/squashfs-root"
fail() { echo "FAIL — $1"; find "$ROOT" -maxdepth 5 -type f 2>/dev/null | head -40; exit 1; }
# Regression gate: beforeBundleCommand runs before Tauri creates the
# AppDir. The v0.4.2 artifact therefore silently shipped Tauri's
# stock AppRun and bypassed every WebKit/Mesa compatibility fix.
cmp -s "$ROOT/AppRun" "$GITHUB_WORKSPACE/frontend/src-tauri/appimage/AppRun" \
|| fail "custom AppRun missing from final AppImage"
[ -s "$ROOT/usr/lib/.bundled-webkitgtk-version" ] \
|| fail "bundled WebKitGTK version marker missing"
# Thin uv-venv installer: verify the AppImage carries the shell binary,
# the bundled uv sidecar, and the backend source resources.
{ [ -f "$ROOT/AppRun" ] || find "$ROOT" -type f \( -name "VoiceStudio" -o -name "omnivoice-studio" \) | grep -q .; } || fail "shell binary / AppRun missing"
+1
View File
@@ -41,6 +41,7 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
### Fixed
- Linux releases now verify that the AppImage actually contains the compatibility launcher, instead of silently shipping Tauri's stock launcher and opening as a blank window on newer Mesa systems.
- Server-mode settings mutations require the admin API key, while host destinations and executable paths can only be selected through the native desktop app. (#1448)
- Automatic model-mirror checks now reject untrusted URLs before opening a network connection. (#1447)
- Sidecar engines no longer break when a library they load prints to the console. Those bytes landed in the middle of the engine's data stream, failing the generation and leaving the connection scrambled for every request after it. (#1428) — thanks @1335-Group!
+27 -29
View File
@@ -19,13 +19,12 @@ Tauri 2's AppImage bundler auto-generates an `AppRun` shell launcher inside the
config key in Tauri 2.x as of this writing. The chosen injection strategy is:
> **A custom `AppRun` template sourced from `frontend/src-tauri/appimage/AppRun`
> is copied into the AppImage staging directory by a `beforeBundleCommand`
> (Tauri 2 supports this hook).**
> is installed into Tauri's project-local AppImage tool cache by a
> `beforeBundleCommand`. Tauri then copies that launcher into the AppDir.**
The script that copies it lives at `scripts/inject-apprun.sh` and is invoked
from `package.json` via the existing `bun run build` pipeline, so the
operation is wired into the same `cargo tauri build --bundles appimage` call
the release pipeline already uses.
The script that seeds the cache lives at `scripts/inject-apprun.sh`. The final
release smoke test extracts the AppImage and compares its `AppRun` byte-for-byte
with the source template, so a stock launcher cannot ship silently again.
---
@@ -58,25 +57,21 @@ Tauri's `bundle.linux.appimage` accepts `bundleMediaFramework: bool` and
`files: HashMap<PathBuf, PathBuf>` but **not** an `appRun` / `template` key.
Tracking issue: `tauri-apps/tauri#7616` is still open.
### B. `beforeBundleCommand` hook (CHOSEN)
### B. Replace the staged AppDir from `beforeBundleCommand` (REJECTED)
Tauri 2 added `build.beforeBundleCommand` as a sibling to `beforeBuildCommand`.
It runs AFTER `cargo build` but BEFORE the bundler packs the AppDir into an
AppImage. This is exactly the right point to swap the AppRun:
- The AppDir staging directory exists at a predictable path
(`target/${profile}/bundle/appimage/*.AppDir/`)
- The auto-generated `AppRun` is already in place
- We just overwrite it with our version before `appimagetool` runs
`beforeBundleCommand` runs before `tauri-bundler` creates the AppDir. The old
implementation globbed for that future directory, found nothing, exited zero,
and v0.4.2 shipped Tauri's stock launcher. This timing cannot be made reliable.
Tradeoffs:
- ✓ No re-packing the squashfs after the fact (faster, simpler)
- ✓ One-line tauri.conf.json change + small shell script
- ✓ Cross-platform safe: the hook only fires when Linux + AppImage are in the
active target list, so macOS/Windows builds are unaffected
- ✗ The staging path is glob-discovered (the .AppDir name follows
`productName`), so the script handles `productName` changes gracefully
### C. Seed Tauri's local AppImage tool cache (CHOSEN)
### C. Post-bundle re-pack with `appimagetool`
With `bundle.useLocalToolsDir`, Tauri reads its launcher from
`target/.tauri/AppRun-<arch>` and copies it into the newly created AppDir. The
hook installs our launcher at that existing extension point before the bundler
runs. `bundle.linux.appimage.files` carries the generated WebKitGTK version
marker into `usr/lib`, where the launcher reads it.
### D. Post-bundle re-pack with `appimagetool`
Status: **rejected.** This would require unpacking the AppImage's squashfs
after Tauri produces it, replacing AppRun, re-running `appimagetool --no-appstream`,
@@ -87,7 +82,7 @@ release-pipeline dep. Strategy B avoids both.
## Chosen strategy
**Strategy B`beforeBundleCommand` hook + custom AppRun template.**
**Strategy Cproject-local Tauri tool cache + custom AppRun template.**
### Files
@@ -95,8 +90,10 @@ release-pipeline dep. Strategy B avoids both.
|---|---|
| `frontend/src-tauri/appimage/AppRun` | The custom launcher shell script (source of truth, version-controlled) |
| `frontend/src-tauri/appimage/AppRun.test.sh` | Shell unit test (W-1) — 4 cases for the WebKit version conditional |
| `scripts/inject-apprun.sh` | Glob the AppDir under `frontend/src-tauri/target/release/bundle/appimage/*.AppDir/` and `cp -f` the AppRun in. Idempotent. |
| `frontend/src-tauri/tauri.conf.json` | `build.beforeBundleCommand` wires the hook into the bundler pipeline |
| `scripts/inject-apprun.sh` | Seed `target/.tauri/AppRun-<arch>` and generate the bundled WebKitGTK marker |
| `scripts/inject-apprun.test.sh` | Regression test for cache seeding, executable mode and version stamping |
| `frontend/src-tauri/tauri.conf.json` | Enable the local tool cache and package its generated marker |
| `.github/workflows/release.yml` | Extract the final artifact and reject a stock launcher or missing marker |
### Wire-up
@@ -104,14 +101,15 @@ release-pipeline dep. Strategy B avoids both.
// frontend/src-tauri/tauri.conf.json
{
"build": {
"beforeBundleCommand": "bash ../../scripts/inject-apprun.sh"
"beforeBundleCommand": "bash ../scripts/inject-apprun.sh"
// ... existing keys
}
}
```
The hook is a no-op when `--bundles appimage` is not in the active target
list (the script `glob`s for the AppDir; if none exist, it exits 0).
The hook is a no-op on non-Linux hosts. On Linux, an unknown architecture or
missing WebKitGTK version is a build failure rather than a silently broken
artifact.
### Why `WEBKIT_DISABLE_COMPOSITING_MODE` is conditional, not unconditional
@@ -130,7 +128,7 @@ When Tauri 2 ships a first-class `appRun` template key (see open
`tauri-apps/tauri#7616`), the migration is:
1. Move `frontend/src-tauri/appimage/AppRun` content into the new config key
2. Delete `scripts/inject-apprun.sh`
2. Delete `scripts/inject-apprun.sh` and its cache-seeding test
3. Remove the `beforeBundleCommand` line that invokes it
4. Keep `AppRun.test.sh` as-is — it still validates the conditional logic
+4 -4
View File
@@ -11,7 +11,7 @@
# known-broken ranges. Setting it unconditionally regresses healthy WebKit
# versions (2.48+) where the compositing path works fine.
#
# This file is copied into the AppImage staging directory by
# This file is installed into Tauri's local tool cache by
# scripts/inject-apprun.sh (wired into Tauri's beforeBundleCommand). See
# .planning/decisions/apprun-strategy.md for the decision rationale.
@@ -30,13 +30,13 @@ HERE="$(dirname -- "$(readlink -f -- "$0")")"
# dev packages installed, so pkg-config answers with their system's healthy
# 2.48 while the bundle runs an older lib — skipping a workaround the running
# library needs). inject-apprun.sh stamps the bundled version into
# .bundled-webkitgtk-version at build time, where it is knowable by
# usr/lib/.bundled-webkitgtk-version at build time, where it is knowable by
# construction; the host pkg-config path survives only as a fallback for
# bundles predating the stamp. OMNIVOICE_APPRUN_WK_MARKER exists for the
# unit tests to point at a fixture marker.
_detect_webkit_workaround() {
local wk_version="0.0"
local marker="${OMNIVOICE_APPRUN_WK_MARKER:-$HERE/.bundled-webkitgtk-version}"
local marker="${OMNIVOICE_APPRUN_WK_MARKER:-$HERE/usr/lib/.bundled-webkitgtk-version}"
if [ -r "$marker" ]; then
# Empty/unreadable marker content → "0.0" (unknown) → fail-safe workaround,
# same philosophy as the missing-pkg-config branch below.
@@ -125,7 +125,7 @@ _prefer_system_webkit() {
# Only meaningful when we know what we bundled; an unstamped bundle keeps
# the old ordering rather than guessing.
local marker="${OMNIVOICE_APPRUN_WK_MARKER:-$HERE/.bundled-webkitgtk-version}"
local marker="${OMNIVOICE_APPRUN_WK_MARKER:-$HERE/usr/lib/.bundled-webkitgtk-version}"
[ -r "$marker" ] || return 1
local bundled
bundled="$(cat "$marker" 2>/dev/null | tr -d '[:space:]')"
+8
View File
@@ -55,6 +55,7 @@
},
"bundle": {
"active": true,
"useLocalToolsDir": true,
"targets": [
"dmg",
"app",
@@ -83,6 +84,13 @@
"binaries/ffmpeg",
"binaries/ffprobe"
],
"linux": {
"appimage": {
"files": {
"usr/lib/.bundled-webkitgtk-version": "target/.tauri/bundled-webkitgtk-version"
}
}
},
"macOS": {
"minimumSystemVersion": "13.3",
"signingIdentity": "-",
+32 -57
View File
@@ -1,75 +1,50 @@
#!/usr/bin/env bash
# Inject our custom AppRun into Tauri's auto-generated AppImage staging dir.
# Seed Tauri's AppImage tool cache with VoiceStudio's launcher.
#
# Wired into tauri.conf.json's `build.beforeBundleCommand`, this script runs
# AFTER `cargo build` but BEFORE `appimagetool` packs the .AppDir. We overwrite
# the default AppRun (which Tauri generates without WEBKIT_DISABLE_COMPOSITING_MODE
# handling) with our conditional launcher.
#
# Issue: #56 (AppImage white-screen on Fedora 44 / Ubuntu 24.04)
# Decision: docs/adr/apprun-strategy.md
#
# Idempotent + safe on non-Linux: if no AppDir staging exists (e.g. macOS
# build, or `--bundles app` only), the script exits 0 cleanly.
# Tauri copies target/.tauri/AppRun-<arch> into the AppDir after
# beforeBundleCommand returns. Replacing an AppDir/AppRun here cannot work:
# the AppDir does not exist until the bundler runs.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
APPRUN_SRC="$REPO_ROOT/frontend/src-tauri/appimage/AppRun"
# beforeBundleCommand also runs for macOS and Windows bundles.
case "${OSTYPE:-}" in
linux*) ;;
*) exit 0 ;;
esac
if [ ! -f "$APPRUN_SRC" ]; then
echo "inject-apprun: source not found: $APPRUN_SRC" >&2
exit 1
fi
# Tauri's AppImage staging dir: frontend/src-tauri/target/{profile}/bundle/appimage/*.AppDir/
# The .AppDir name follows productName (e.g. "VoiceStudio.AppDir").
# Glob across both release and debug profiles in case the caller used --debug.
STAGE_BASE_RELEASE="$REPO_ROOT/frontend/src-tauri/target/release/bundle/appimage"
STAGE_BASE_DEBUG="$REPO_ROOT/frontend/src-tauri/target/debug/bundle/appimage"
TOOLS_DIR="${OMNIVOICE_TAURI_TOOLS_DIR:-$REPO_ROOT/frontend/src-tauri/target/.tauri}"
ARCH="${OMNIVOICE_TARGET_ARCH:-$(uname -m)}"
case "$ARCH" in
x86_64|amd64) ARCH=x86_64 ;;
aarch64|arm64) ARCH=aarch64 ;;
armv7l|armhf) ARCH=armhf ;;
*)
echo "inject-apprun: unsupported Linux architecture: $ARCH" >&2
exit 1
;;
esac
found=0
for stage_base in "$STAGE_BASE_RELEASE" "$STAGE_BASE_DEBUG"; do
if [ ! -d "$stage_base" ]; then
continue
fi
# Use a glob loop instead of `find` to avoid surprises with names containing spaces.
shopt -s nullglob
for appdir in "$stage_base"/*.AppDir; do
if [ -d "$appdir" ]; then
echo "inject-apprun: replacing AppRun in $appdir"
cp -f "$APPRUN_SRC" "$appdir/AppRun"
chmod 755 "$appdir/AppRun"
# Stamp the bundled WebKitGTK version (#961 follow-up). The AppImage
# bundles THIS build host's libwebkit2gtk, so the host's pkg-config
# answer here is the version the shipped bundle will actually run —
# knowable by construction at bundle time, unknowable reliably at
# runtime (a user's pkg-config reports their SYSTEM's version, which
# LD_LIBRARY_PATH overrides with the bundled copy). AppRun's workaround
# auto-detection reads this marker first and only falls back to host
# pkg-config when the marker is absent (bundles predating the stamp).
wk_bundled="$(pkg-config --modversion webkit2gtk-4.1 2>/dev/null \
|| pkg-config --modversion webkit2gtk-4.0 2>/dev/null \
|| echo "")"
if [ -n "$wk_bundled" ]; then
printf '%s\n' "$wk_bundled" > "$appdir/.bundled-webkitgtk-version"
echo "inject-apprun: stamped bundled WebKitGTK version: $wk_bundled"
else
echo "inject-apprun: WARNING — could not read the bundled WebKitGTK version (pkg-config missing?); AppRun will use its runtime fallback" >&2
fi
found=1
fi
done
shopt -u nullglob
done
mkdir -p "$TOOLS_DIR"
install -m 755 "$APPRUN_SRC" "$TOOLS_DIR/AppRun-$ARCH"
if [ $found -eq 0 ]; then
# Not necessarily an error — beforeBundleCommand runs unconditionally even
# when the active target list does not include appimage. Stay quiet so
# macOS/Windows builds do not see noisy stderr.
echo "inject-apprun: no AppDir staging found (skipping — not an AppImage build)"
# Tauri's appimage.files copies this into usr/lib. AppRun reads the marker
# there to compare the bundled WebKitGTK with the host copy it may prefer.
WK_VERSION="${OMNIVOICE_WEBKIT_VERSION:-$(pkg-config --modversion webkit2gtk-4.1 2>/dev/null \
|| pkg-config --modversion webkit2gtk-4.0 2>/dev/null || true)}"
if [ -z "$WK_VERSION" ]; then
echo "inject-apprun: bundled WebKitGTK version is unavailable" >&2
exit 1
fi
printf '%s\n' "$WK_VERSION" > "$TOOLS_DIR/bundled-webkitgtk-version"
exit 0
echo "inject-apprun: seeded AppRun-$ARCH (WebKitGTK $WK_VERSION)"
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
OMNIVOICE_TAURI_TOOLS_DIR="$TMP/tools" \
OMNIVOICE_TARGET_ARCH=amd64 \
OMNIVOICE_WEBKIT_VERSION=2.48.7 \
bash "$REPO_ROOT/scripts/inject-apprun.sh"
cmp -s \
"$REPO_ROOT/frontend/src-tauri/appimage/AppRun" \
"$TMP/tools/AppRun-x86_64"
[ -x "$TMP/tools/AppRun-x86_64" ]
[ "$(cat "$TMP/tools/bundled-webkitgtk-version")" = "2.48.7" ]
echo "PASS: Tauri AppImage tool cache seeded"