fix(linux): preserve Wayland in AppImage bundles

Closes #8333
This commit is contained in:
t8y2
2026-09-26 03:06:08 +00:00
parent 99d80f7a83
commit 6b67957ecf
5 changed files with 263 additions and 3 deletions
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
upstream_plugin="$script_dir/linuxdeploy-plugin-gtk-upstream.sh"
[[ -x "$upstream_plugin" ]] || {
echo "Missing executable upstream GTK plugin: $upstream_plugin" >&2
exit 1
}
# linuxdeploy queries this before invoking the plugin. Preserve the upstream
# protocol response without requiring an AppDir.
for argument in "$@"; do
case "$argument" in
--plugin-api-version | --help)
exec "$upstream_plugin" "$@"
;;
esac
done
appdir=''
arguments=("$@")
for ((index = 0; index < ${#arguments[@]}; index += 1)); do
case "${arguments[index]}" in
--appdir)
index=$((index + 1))
appdir=${arguments[index]:-}
;;
--appdir=*)
appdir=${arguments[index]#--appdir=}
;;
esac
done
[[ -n "$appdir" ]] || {
echo "GTK plugin wrapper did not receive --appdir" >&2
exit 1
}
"$upstream_plugin" "$@"
# AppImages must use the target system's display stack. Bundling Ubuntu's
# Wayland/XKB/XCB libraries ahead of a newer host Mesa stack can abort before
# GTK creates a window (tauri-apps/tauri#15976).
display_library_patterns=(
'libwayland-*.so*'
'libxkbcommon.so*'
'libxcb-randr.so*'
'libxcb-render.so*'
'libxcb-shm.so*'
'libXau.so*'
'libXdmcp.so*'
)
for pattern in "${display_library_patterns[@]}"; do
find "$appdir/usr" \( -type f -o -type l \) -name "$pattern" -print -delete
done
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail
die() {
echo "AppImage bundler setup failed: $*" >&2
exit 1
}
if [[ $# -ne 1 ]]; then
die "usage: $0 <Linux target triple>"
fi
: "${XDG_CACHE_HOME:?XDG_CACHE_HOME must point to the isolated release cache}"
target=$1
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
case "$target" in
x86_64-unknown-linux-gnu)
tools_arch=x86_64
linuxdeploy_sha256=36a2d7e274d12e1050d0e9ecfe11d339ed54720b2bec464c286d53f8b07f5c62
;;
aarch64-unknown-linux-gnu)
tools_arch=aarch64
linuxdeploy_sha256=556ab80baa98e600aa80f0dcedfb70bca0e1ce7e9f147fb345be3fcc3e91b2b1
;;
*)
die "unsupported Linux target: $target"
;;
esac
# Tauri CLI 2.11.4 only downloads these tools when their cache entries are
# absent, so an older runner cache can keep generating an AppRun hook that
# unconditionally forces X11. Pin the tools from the accepted upstream fix
# until a stable Tauri CLI release includes tauri-apps/tauri#16062.
tauri_fix_commit=8e7028331ad37ac2db74d4ec20e66be5cacf2c40
linuxdeploy_revision=07333c6
gtk_plugin_sha256=ef6b9a980417243bc62e0241b51dc49876032afd1bab9b4762389f961b406d9b
tools_dir="$XDG_CACHE_HOME/tauri"
mkdir -p "$tools_dir"
download_path=''
cleanup() {
if [[ -n "$download_path" ]]; then
rm -f -- "$download_path"
fi
}
trap cleanup EXIT
install_verified_tool() {
local name=$1
local url=$2
local expected_sha256=$3
local destination="$tools_dir/$name"
local actual_sha256
if [[ -f "$destination" ]]; then
actual_sha256=$(sha256sum "$destination")
actual_sha256=${actual_sha256%% *}
if [[ "$actual_sha256" == "$expected_sha256" ]]; then
chmod 0755 "$destination"
echo "Using verified cached AppImage tool: $name"
return
fi
fi
download_path=$(mktemp "$tools_dir/.${name}.XXXXXX")
curl --fail --location --retry 3 --retry-all-errors --silent --show-error \
"$url" --output "$download_path"
actual_sha256=$(sha256sum "$download_path")
actual_sha256=${actual_sha256%% *}
[[ "$actual_sha256" == "$expected_sha256" ]] \
|| die "$name SHA-256 mismatch: expected $expected_sha256, got $actual_sha256"
chmod 0755 "$download_path"
mv -f -- "$download_path" "$destination"
download_path=''
echo "Installed verified AppImage tool: $name"
}
linuxdeploy_name="linuxdeploy-${tools_arch}.AppImage"
install_verified_tool \
"$linuxdeploy_name" \
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy-${linuxdeploy_revision}/${linuxdeploy_name}" \
"$linuxdeploy_sha256"
gtk_plugin_name=linuxdeploy-plugin-gtk-upstream.sh
install_verified_tool \
"$gtk_plugin_name" \
"https://raw.githubusercontent.com/tauri-apps/tauri/${tauri_fix_commit}/crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy-plugin-gtk.sh" \
"$gtk_plugin_sha256"
upstream_gtk_plugin="$tools_dir/$gtk_plugin_name"
gtk_plugin="$tools_dir/linuxdeploy-plugin-gtk.sh"
install -m 0755 "$script_dir/linuxdeploy-plugin-gtk-wrapper.sh" "$gtk_plugin"
bash -n "$upstream_gtk_plugin" "$gtk_plugin"
if grep -Eq '^[[:space:]]*(export[[:space:]]+)?GDK_BACKEND[[:space:]]*=' "$upstream_gtk_plugin"; then
die "pinned GTK plugin still contains an active GDK_BACKEND assignment"
fi
echo "Prepared Wayland-capable AppImage tools for $target in $tools_dir"
@@ -2,7 +2,7 @@
set -euo pipefail
die() {
echo "AppImage input-method verification failed: $*" >&2
echo "AppImage runtime verification failed: $*" >&2
exit 1
}
@@ -37,6 +37,38 @@ appdir="$work_dir/squashfs-root"
[[ -d "$appdir" ]] || die "AppImage extraction did not create squashfs-root"
appdir=$(realpath "$appdir")
gtk_hook="$appdir/apprun-hooks/linuxdeploy-plugin-gtk.sh"
[[ -f "$gtk_hook" ]] || die "missing GTK AppRun hook"
if grep -Eq '^[[:space:]]*(export[[:space:]]+)?GDK_BACKEND[[:space:]]*=' "$gtk_hook"; then
die "GTK AppRun hook overrides GDK_BACKEND"
fi
for requested_backend in wayland x11; do
preserved_backend=$(
APPDIR="$appdir" GDK_BACKEND="$requested_backend" XDG_DATA_DIRS='' \
bash --noprofile --norc -c 'source "$APPDIR/apprun-hooks/linuxdeploy-plugin-gtk.sh"; printf "%s" "$GDK_BACKEND"'
)
[[ "$preserved_backend" == "$requested_backend" ]] \
|| die "GTK AppRun hook changed explicit GDK_BACKEND=$requested_backend to '$preserved_backend'"
done
echo "Verified GTK AppRun hook preserves explicit Wayland and X11 backends"
display_library_patterns=(
'libwayland-*.so*'
'libxkbcommon.so*'
'libxcb-randr.so*'
'libxcb-render.so*'
'libxcb-shm.so*'
'libXau.so*'
'libXdmcp.so*'
)
for pattern in "${display_library_patterns[@]}"; do
bundled_library=$(find "$appdir/usr" \( -type f -o -type l \) -name "$pattern" -print -quit)
[[ -z "$bundled_library" ]] \
|| die "bundled display-stack library must come from the host: ${bundled_library#"$appdir"/}"
done
echo "Verified AppImage relies on the host Wayland/XKB/XCB display stack"
require_artifact() {
local basename=$1
local match
+10 -1
View File
@@ -256,6 +256,15 @@ jobs:
# instead of loading potentially incompatible modules from the host.
sudo apt-get install -y fcitx5-frontend-gtk3 ibus-gtk3 libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev xdg-utils
- name: Prepare Wayland-capable AppImage bundler
if: startsWith(matrix.platform, 'ubuntu')
shell: bash
env:
XDG_CACHE_HOME: ${{ runner.temp }}/dbx-tauri-cache
run: |
echo "XDG_CACHE_HOME=${XDG_CACHE_HOME}" >> "$GITHUB_ENV"
./.github/scripts/prepare-appimage-bundler.sh "${{ matrix.target }}"
- name: Setup Tauri signing key
shell: bash
run: |
@@ -281,7 +290,7 @@ jobs:
assetNamePattern: ${{ matrix.asset_pattern }}
args: --target ${{ matrix.target }}
- name: Verify AppImage input methods
- name: Verify AppImage runtime bundle
if: startsWith(matrix.platform, 'ubuntu')
shell: bash
run: ./.github/scripts/verify-appimage-input-methods.sh "target/${{ matrix.target }}/release/bundle/appimage"
+65 -1
View File
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
@@ -11,6 +11,9 @@ const releaseScript = join(repoRoot, "scripts/release.mjs");
const releaseWorkflow = join(repoRoot, ".github/workflows/release.yml");
const packagesWorkflow = join(repoRoot, ".github/workflows/mcp-release.yml");
const vsignConfigPath = join(repoRoot, "src-tauri/tauri.vsign.conf.json");
const appImageBundlerSetup = join(repoRoot, ".github/scripts/prepare-appimage-bundler.sh");
const appImageGtkWrapper = join(repoRoot, ".github/scripts/linuxdeploy-plugin-gtk-wrapper.sh");
const appImageVerifier = join(repoRoot, ".github/scripts/verify-appimage-input-methods.sh");
function runRelease(args, env = {}) {
return spawnSync(process.execPath, [releaseScript, ...args], {
@@ -169,6 +172,67 @@ test("Windows 7 release build does not use sccache", () => {
assert.doesNotMatch(win7Job, /RUSTC_WRAPPER|SCCACHE_|sccache/i);
});
test("Linux releases pin the Wayland-capable AppImage bundler before upload", () => {
const workflow = readFileSync(releaseWorkflow, "utf8");
const setup = readFileSync(appImageBundlerSetup, "utf8");
const wrapper = readFileSync(appImageGtkWrapper, "utf8");
const verifier = readFileSync(appImageVerifier, "utf8");
const prepareStep = workflow.indexOf("Prepare Wayland-capable AppImage bundler");
const buildStep = workflow.indexOf("Build Tauri app", prepareStep);
const verifyStep = workflow.indexOf("Verify AppImage runtime bundle", buildStep);
assert.notEqual(prepareStep, -1);
assert.ok(prepareStep < buildStep);
assert.ok(buildStep < verifyStep);
assert.match(workflow, /XDG_CACHE_HOME: \$\{\{ runner\.temp \}\}\/dbx-tauri-cache/);
assert.match(setup, /tauri_fix_commit=8e7028331ad37ac2db74d4ec20e66be5cacf2c40/);
assert.match(setup, /linuxdeploy_revision=07333c6/);
assert.match(setup, /36a2d7e274d12e1050d0e9ecfe11d339ed54720b2bec464c286d53f8b07f5c62/);
assert.match(setup, /556ab80baa98e600aa80f0dcedfb70bca0e1ce7e9f147fb345be3fcc3e91b2b1/);
assert.match(wrapper, /libwayland-\*\.so\*/);
assert.match(wrapper, /libxkbcommon\.so\*/);
assert.match(verifier, /GTK AppRun hook overrides GDK_BACKEND/);
assert.match(verifier, /for requested_backend in wayland x11/);
assert.match(verifier, /host Wayland\/XKB\/XCB display stack/);
});
test("AppImage GTK wrapper removes bundled display-stack libraries", () => {
const directory = mkdtempSync(join(tmpdir(), "dbx-appimage-wrapper-test-"));
const wrapper = join(directory, "linuxdeploy-plugin-gtk.sh");
const upstream = join(directory, "linuxdeploy-plugin-gtk-upstream.sh");
const appDir = join(directory, "DBX.AppDir");
const libDir = join(appDir, "usr", "lib");
const nestedLibDir = join(libDir, "nested");
copyFileSync(appImageGtkWrapper, wrapper);
writeFileSync(
upstream,
'#!/usr/bin/env bash\nif [[ "$1" == "--plugin-api-version" ]]; then printf "0\\n"; fi\n',
);
chmodSync(wrapper, 0o755);
chmodSync(upstream, 0o755);
mkdirSync(nestedLibDir, { recursive: true });
for (const path of [
join(libDir, "libwayland-client.so.0"),
join(libDir, "libxkbcommon.so.0"),
join(nestedLibDir, "libXau.so.6"),
join(libDir, "libsafe.so.1"),
]) {
writeFileSync(path, "");
}
const apiVersion = spawnSync(wrapper, ["--plugin-api-version"], { encoding: "utf8" });
assert.equal(apiVersion.status, 0, apiVersion.stderr);
assert.equal(apiVersion.stdout.trim(), "0");
const cleanup = spawnSync(wrapper, ["--appdir", appDir], { encoding: "utf8" });
assert.equal(cleanup.status, 0, cleanup.stderr);
assert.equal(existsSync(join(libDir, "libwayland-client.so.0")), false);
assert.equal(existsSync(join(libDir, "libxkbcommon.so.0")), false);
assert.equal(existsSync(join(nestedLibDir, "libXau.so.6")), false);
assert.equal(existsSync(join(libDir, "libsafe.so.1")), true);
});
test("desktop-only Cargo.lock version refresh does not count as a Node package change", () => {
const before = `
[[package]]