From 41f0e00cfb0844068283c42a0c0bcb5600eeb6ea Mon Sep 17 00:00:00 2001 From: lucaronin Date: Wed, 13 May 2026 16:06:57 +0200 Subject: [PATCH] fix: support symlinked appimage launches --- .github/workflows/release-stable.yml | 13 +- .github/workflows/release.yml | 13 +- docs/ABSTRACTIONS.md | 4 +- docs/ARCHITECTURE.md | 13 +- docs/GETTING-STARTED.md | 14 ++ scripts/appimage-launcher-tools.mjs | 262 +++++++++++++++++++++++ scripts/appimage-launcher-tools.test.mjs | 114 ++++++++++ 7 files changed, 427 insertions(+), 6 deletions(-) create mode 100644 scripts/appimage-launcher-tools.mjs create mode 100644 scripts/appimage-launcher-tools.test.mjs diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 50f1141c..686a1904 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -325,6 +325,9 @@ jobs: run: | rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle + - name: Prepare AppImage symlink launcher patch + run: node scripts/appimage-launcher-tools.mjs prepare-plugin + - name: Set version run: | VERSION="${{ needs.version.outputs.version }}" @@ -346,8 +349,11 @@ jobs: - name: Validate Linux bundles run: | shopt -s nullglob - installers=( + appimages=( src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage + ) + installers=( + "${appimages[@]}" src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm ) @@ -356,6 +362,10 @@ jobs: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig ) + if [ ${#appimages[@]} -eq 0 ]; then + echo "::error::Linux build produced no AppImage bundle." + exit 1 + fi if [ ${#installers[@]} -eq 0 ]; then echo "::error::Linux build produced no AppImage, deb or rpm bundle." exit 1 @@ -364,6 +374,7 @@ jobs: echo "::error::Linux build produced no updater signature (.sig) artifact." exit 1 fi + node scripts/appimage-launcher-tools.mjs validate-appimages "${appimages[@]}" - name: Upload Linux bundles uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cbb6d79f..72bbd02e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -381,6 +381,9 @@ jobs: run: | rm -rf src-tauri/target/x86_64-unknown-linux-gnu/release/bundle + - name: Prepare AppImage symlink launcher patch + run: node scripts/appimage-launcher-tools.mjs prepare-plugin + - name: Set version run: | VERSION="${{ needs.version.outputs.version }}" @@ -402,8 +405,11 @@ jobs: - name: Validate Linux bundles run: | shopt -s nullglob - installers=( + appimages=( src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage + ) + installers=( + "${appimages[@]}" src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/rpm/*.rpm ) @@ -412,6 +418,10 @@ jobs: src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage.tar.gz.sig src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/*.deb.sig ) + if [ ${#appimages[@]} -eq 0 ]; then + echo "::error::Linux build produced no AppImage bundle." + exit 1 + fi if [ ${#installers[@]} -eq 0 ]; then echo "::error::Linux build produced no AppImage, deb or rpm bundle." exit 1 @@ -420,6 +430,7 @@ jobs: echo "::error::Linux build produced no updater signature (.sig) artifact." exit 1 fi + node scripts/appimage-launcher-tools.mjs validate-appimages "${appimages[@]}" - name: Upload Linux bundles uses: actions/upload-artifact@v4 diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 1214e16a..2081545b 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -905,6 +905,6 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins - **`download_and_install_app_update`** — Channel-aware download/install with streamed progress events. ### CI/CD -- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. The docs/release Pages job reads the stable manifest from the latest stable release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so the public updater JSON changes as part of the release workflow. macOS release assets use `Tolaria__macOS_Silicon` and `Tolaria__macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`, which is retained as a diagnostic build-version tag but not registered as a normal Sentry release for alpha builds. -- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon and Intel DMG/updater artifacts, Windows x64 installers/updater bundles, Linux x86_64 `.deb` / AppImage artifacts, and a static public download page that starts the selected installer without replacing the page with a blank download navigation. The Pages job reads the alpha manifest from the latest alpha release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so stable and alpha manifests stay fresh. Stable macOS DMG/updater assets use the same `Tolaria__macOS_Silicon` and `Tolaria__macOS_Intel` base names. Packaged builds pass the computed stable version as `VITE_SENTRY_RELEASE`, which is registered as Sentry's release. +- **`.github/workflows/release.yml`** — Alpha prereleases from every push to `main` using calendar-semver technical versions (`YYYY.M.D-alpha.N`) and clean `Alpha YYYY.M.D.N` release names. GitHub alpha tags zero-pad the prerelease sequence (`alpha-vYYYY.M.D-alpha.NNNN`) so GitHub release ordering stays chronological while the shipped app version remains `YYYY.M.D-alpha.N`. Publishes `alpha/latest.json` with macOS Apple Silicon/Intel, Linux x64, and Windows x64 updater entries, then refreshes the legacy `latest.json` / `latest-canary.json` aliases to the alpha feed. The Linux job pre-seeds Tauri's AppImage output plugin with `scripts/appimage-launcher-tools.mjs` so linuxdeploy's generated AppRun wrapper follows `$0` before taking `dirname`, then extracts the sealed AppImage launcher to validate symlink-safe direct, absolute-symlink, and relative-symlink launch support. The docs/release Pages job reads the stable manifest from the latest stable release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so the public updater JSON changes as part of the release workflow. macOS release assets use `Tolaria__macOS_Silicon` and `Tolaria__macOS_Intel` base names. Packaged builds pass the computed version as `VITE_SENTRY_RELEASE`, which is retained as a diagnostic build-version tag but not registered as a normal Sentry release for alpha builds. +- **`.github/workflows/release-stable.yml`** — Stable releases from `stable-vYYYY.M.D` tags. Publishes `stable/latest.json`, macOS Apple Silicon and Intel DMG/updater artifacts, Windows x64 installers/updater bundles, Linux x86_64 `.deb` / `.rpm` / AppImage artifacts, and a static public download page that starts the selected installer without replacing the page with a blank download navigation. The Linux job uses the same AppImage symlink-safe launcher patch and sealed-AppImage validation as alpha releases. The Pages job reads the alpha manifest from the latest alpha release asset instead of copying the live Pages URL, uploads the built site as a Pages artifact, and deploys it with GitHub's official Pages action so stable and alpha manifests stay fresh. Stable macOS DMG/updater assets use the same `Tolaria__macOS_Silicon` and `Tolaria__macOS_Intel` base names. Packaged builds pass the computed stable version as `VITE_SENTRY_RELEASE`, which is registered as Sentry's release. - **Beta cohorts** are handled in PostHog targeting only. There is no beta updater feed. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 710291b3..7917e746 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -922,6 +922,11 @@ push to main → build-windows job: → pnpm install, stamp version, tauri build --target x86_64-pc-windows-msvc --bundles nsis → upload NSIS installer, optional MSI artifacts, and signed Windows updater bundles + → build-linux job: + → pnpm install, prepare AppImage symlink-safe launcher patch, stamp version + → tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage + → extract the sealed AppImage AppRun and verify linuxdeploy resolves $0 before dirname + → upload .deb, .rpm, .AppImage, and signed Linux updater bundles → release job: → generate alpha-latest.json with darwin-aarch64, darwin-x86_64, Linux, and Windows updater URLs → publish GitHub prerelease alpha-vYYYY.M.D-alpha.NNNN named Tolaria Alpha YYYY.M.D.N @@ -944,8 +949,10 @@ push stable-vYYYY.M.D tag → pnpm install, stamp version, pnpm build, tauri build --target x86_64-apple-darwin → upload signed Apple Silicon and Intel .app.tar.gz + .sig and .dmg artifacts named Tolaria__macOS_Silicon and Tolaria__macOS_Intel → build-linux job: - → pnpm install, stamp version, tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage - → upload .deb, .AppImage, and signed Linux updater bundles + → pnpm install, prepare AppImage symlink-safe launcher patch, stamp version + → tauri build --target x86_64-unknown-linux-gnu --bundles deb,rpm,appimage + → extract the sealed AppImage AppRun and verify linuxdeploy resolves $0 before dirname + → upload .deb, .rpm, .AppImage, and signed Linux updater bundles → build-windows job: → pnpm install, stamp version, tauri build --target x86_64-pc-windows-msvc --bundles nsis → upload NSIS installer, optional MSI artifacts, and signed Windows updater bundles @@ -961,6 +968,8 @@ push stable-vYYYY.M.D tag → deploy to gh-pages ``` +Linux AppImage release jobs run `scripts/appimage-launcher-tools.mjs prepare-plugin` before Tauri packaging. The script pre-seeds Tauri's Linux tools cache with a small `linuxdeploy-plugin-appimage` shim. When linuxdeploy is about to seal the AppImage, that shim patches linuxdeploy's generated GTK AppRun wrapper from `dirname "$0"`-then-`readlink -f` to `readlink -f "$0"`-then-`dirname`, preserving direct launches while allowing absolute and relative symlinks such as `/usr/local/bin/tolaria -> Tolaria.AppImage`. The validation step then extracts `AppRun` from every produced AppImage and fails the release if the symlink-safe resolver is missing. + ### Versioning - Stable promotions use git tags in the form `stable-vYYYY.M.D` and stamp the technical version `YYYY.M.D`. diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index c744ae27..6cce7d58 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -45,6 +45,20 @@ WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 LD_PRELOAD=/u If your distribution stores the 64-bit library elsewhere, use that path instead, for example `/usr/lib/x86_64-linux-gnu/libwayland-client.so.0`. On 64-bit Fedora, avoid `/usr/lib/libwayland-client.so.0`; that path can point at a 32-bit library and be ignored by the loader with a wrong ELF class warning. +### Linux AppImage packaging checks + +Linux release CI prepares Tauri's AppImage tools cache before `pnpm tauri build`: + +```bash +node scripts/appimage-launcher-tools.mjs prepare-plugin +``` + +That step patches linuxdeploy's generated GTK `AppRun` wrapper before the AppImage is sealed so direct launches, absolute symlinks, and relative symlinks resolve the real AppRun path before choosing its directory. After build, CI extracts every produced AppImage launcher and verifies the symlink-safe resolver: + +```bash +node scripts/appimage-launcher-tools.mjs validate-appimages src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/*.AppImage +``` + ## Quick Start ```bash diff --git a/scripts/appimage-launcher-tools.mjs b/scripts/appimage-launcher-tools.mjs new file mode 100644 index 00000000..386cbcc3 --- /dev/null +++ b/scripts/appimage-launcher-tools.mjs @@ -0,0 +1,262 @@ +import { spawnSync } from 'node:child_process' +import { error as logError, log } from 'node:console' +import { existsSync } from 'node:fs' +import { + chmod, + mkdir, + mkdtemp, + readFile, + rename, + rm, + writeFile, +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +export const BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE = + 'this_dir="$(readlink -f "$(dirname "$0")")"' +export const FIXED_LINUXDEPLOY_APPRUN_DIR_LINE = + 'this_dir="$(dirname "$(readlink -f "$0")")"' +export const APPIMAGE_PLUGIN_WRAPPER_NAME = 'linuxdeploy-plugin-appimage.AppImage' +export const REAL_APPIMAGE_PLUGIN_NAME = 'linuxdeploy-plugin-appimage.real.AppImage' +export const DEFAULT_APPIMAGE_PLUGIN_URL = + 'https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage' + +const WRAPPER_MARKER = 'Tolaria AppImage symlink launcher shim' + +export function tauriToolsCacheDir(env = process.env) { + if (env.TOLARIA_TAURI_TOOLS_DIR) { + return resolve(env.TOLARIA_TAURI_TOOLS_DIR) + } + + if (env.XDG_CACHE_HOME) { + return resolve(env.XDG_CACHE_HOME, 'tauri') + } + + if (!env.HOME) { + throw new Error('HOME or XDG_CACHE_HOME is required to locate the Tauri tools cache') + } + + return resolve(env.HOME, '.cache', 'tauri') +} + +export function patchAppRunText(text) { + if (text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE)) { + return { + changed: true, + text: text.replaceAll( + BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE, + FIXED_LINUXDEPLOY_APPRUN_DIR_LINE, + ), + } + } + + return { changed: false, text } +} + +export function assertSymlinkSafeAppRunText(text, label = 'AppRun') { + if (text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE)) { + throw new Error(`${label} still resolves dirname before following AppRun symlinks`) + } + + if (!text.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE)) { + throw new Error(`${label} is missing the symlink-safe AppRun directory resolver`) + } +} + +export function appImagePluginWrapperSource({ + pluginUrl = DEFAULT_APPIMAGE_PLUGIN_URL, +} = {}) { + return `#!/usr/bin/env bash +set -euo pipefail + +# ${WRAPPER_MARKER} +PLUGIN_URL="\${TOLARIA_APPIMAGE_PLUGIN_URL:-${pluginUrl}}" +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +REAL_PLUGIN="\${TOLARIA_APPIMAGE_REAL_PLUGIN:-"$SCRIPT_DIR/${REAL_APPIMAGE_PLUGIN_NAME}"}" + +appdir_from_args() { + local previous="" + + for arg in "$@"; do + if [ "$previous" = "--appdir" ]; then + printf '%s\\n' "$arg" + return 0 + fi + + case "$arg" in + --appdir=*) + printf '%s\\n' "\${arg#--appdir=}" + return 0 + ;; + esac + + previous="$arg" + done +} + +download_real_plugin() { + if [ -x "$REAL_PLUGIN" ]; then + return 0 + fi + + local tmp_plugin="$REAL_PLUGIN.tmp.$$" + rm -f "$tmp_plugin" + + if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "$tmp_plugin" "$PLUGIN_URL" + elif command -v wget >/dev/null 2>&1; then + wget -q -O "$tmp_plugin" "$PLUGIN_URL" + else + echo "curl or wget is required to fetch the real linuxdeploy AppImage output plugin" >&2 + return 1 + fi + + chmod +x "$tmp_plugin" + mv "$tmp_plugin" "$REAL_PLUGIN" +} + +patch_apprun() { + local appdir="\${APPDIR:-}" + + if [ -z "$appdir" ]; then + appdir="$(appdir_from_args "$@" || true)" + fi + + if [ -z "$appdir" ] || [ ! -f "$appdir/AppRun" ]; then + return 0 + fi + + python3 - "$appdir/AppRun" <<'PY' +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +broken = 'this_dir="$(readlink -f "$(dirname "$0")")"' +fixed = 'this_dir="$(dirname "$(readlink -f "$0")")"' +text = path.read_text(encoding="utf-8") + +if broken in text: + path.write_text(text.replace(broken, fixed), encoding="utf-8") + print(f"Patched linuxdeploy AppRun symlink resolution in {path}", file=sys.stderr) +elif fixed in text: + pass +elif "autogenerated by linuxdeploy" in text and "AppRun.wrapped" in text: + raise SystemExit(f"{path} is a linuxdeploy wrapper but does not contain the expected AppRun resolver") +PY +} + +download_real_plugin +patch_apprun "$@" +exec "$REAL_PLUGIN" "$@" +` +} + +export async function preparePluginWrapper({ + env = process.env, + toolsDir = tauriToolsCacheDir(env), +} = {}) { + await mkdir(toolsDir, { recursive: true }) + + const wrapperPath = join(toolsDir, APPIMAGE_PLUGIN_WRAPPER_NAME) + const realPluginPath = join(toolsDir, REAL_APPIMAGE_PLUGIN_NAME) + + if (existsSync(wrapperPath) && !existsSync(realPluginPath)) { + const existing = await readFile(wrapperPath, 'utf8').catch(() => '') + if (!existing.includes(WRAPPER_MARKER)) { + await rename(wrapperPath, realPluginPath) + } + } + + await writeFile(wrapperPath, appImagePluginWrapperSource(), 'utf8') + await chmod(wrapperPath, 0o755) + + return { realPluginPath, wrapperPath } +} + +export async function validateAppRunFile(path) { + const text = await readFile(path, 'utf8') + assertSymlinkSafeAppRunText(text, path) +} + +export async function validateAppImages(paths) { + if (paths.length === 0) { + throw new Error('At least one AppImage path is required for launcher validation') + } + + for (const appImage of paths.map((path) => resolve(path))) { + const tempDir = await mkdtemp(join(tmpdir(), 'tolaria-appimage-')) + try { + const result = spawnSync(appImage, ['--appimage-extract', 'AppRun'], { + cwd: tempDir, + encoding: 'utf8', + }) + + if (result.status !== 0) { + throw new Error( + [ + `Failed to extract AppRun from ${appImage}`, + result.stdout.trim(), + result.stderr.trim(), + ] + .filter(Boolean) + .join('\n'), + ) + } + + await validateAppRunFile(join(tempDir, 'squashfs-root', 'AppRun')) + } finally { + await rm(tempDir, { recursive: true, force: true }) + } + } +} + +async function preparePluginCommand() { + const { wrapperPath, realPluginPath } = await preparePluginWrapper() + log(`Prepared ${wrapperPath}`) + log(`Real plugin cache: ${realPluginPath}`) +} + +async function validateAppRunFilesCommand(paths) { + for (const path of paths) { + await validateAppRunFile(path) + log(`Validated ${path}`) + } +} + +async function validateAppImagesCommand(paths) { + await validateAppImages(paths) + for (const path of paths) { + log(`Validated AppImage launcher in ${path}`) + } +} + +const COMMANDS = new Map([ + ['prepare-plugin', preparePluginCommand], + ['validate-apprun-file', validateAppRunFilesCommand], + ['validate-appimages', validateAppImagesCommand], +]) + +function usage() { + return 'Usage: node scripts/appimage-launcher-tools.mjs prepare-plugin | validate-apprun-file | validate-appimages ' +} + +async function main() { + const [command, ...args] = process.argv.slice(2) + const handler = COMMANDS.get(command) + + if (!handler) { + throw new Error(usage()) + } + + await handler(args) +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main().catch((error) => { + logError(error.message) + process.exit(1) + }) +} diff --git a/scripts/appimage-launcher-tools.test.mjs b/scripts/appimage-launcher-tools.test.mjs new file mode 100644 index 00000000..2dad3378 --- /dev/null +++ b/scripts/appimage-launcher-tools.test.mjs @@ -0,0 +1,114 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import { existsSync, realpathSync } from 'node:fs' +import { + chmod, + mkdir, + mkdtemp, + readFile, + symlink, + writeFile, +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, dirname, join } from 'node:path' +import process from 'node:process' +import test from 'node:test' + +import { + BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE, + FIXED_LINUXDEPLOY_APPRUN_DIR_LINE, + appImagePluginWrapperSource, + patchAppRunText, +} from './appimage-launcher-tools.mjs' + +function brokenResolverDir(invokedPath) { + return realpathSync(dirname(invokedPath)) +} + +function fixedResolverDir(invokedPath) { + return dirname(realpathSync(invokedPath)) +} + +test('patches linuxdeploy AppRun wrapper to resolve the invoked path before dirname', () => { + const original = [ + '#! /usr/bin/env bash', + '# autogenerated by linuxdeploy', + BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE, + 'exec "$this_dir"/AppRun.wrapped "$@"', + ].join('\n') + + const patched = patchAppRunText(original) + + assert.equal(patched.changed, true) + assert.equal(patched.text.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false) + assert.equal(patched.text.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true) +}) + +test('fixed resolver follows absolute and relative symlinks before choosing AppDir', async () => { + const root = await mkdtemp(join(tmpdir(), 'tolaria-apprun-resolver-')) + const appDir = join(root, 'Tolaria.AppDir') + const binDir = join(root, 'bin') + const relativeDir = join(root, 'relative-bin') + const appRun = join(appDir, 'AppRun') + + await mkdir(appDir) + await mkdir(binDir) + await mkdir(relativeDir) + await writeFile(appRun, '#! /usr/bin/env bash\n', 'utf8') + + const absoluteSymlink = join(binDir, 'tolaria') + const relativeSymlink = join(relativeDir, 'tolaria') + + await symlink(appRun, absoluteSymlink) + await symlink(`../${basename(appDir)}/AppRun`, relativeSymlink) + + assert.equal(brokenResolverDir(absoluteSymlink), realpathSync(binDir)) + assert.equal(fixedResolverDir(absoluteSymlink), realpathSync(appDir)) + assert.equal(brokenResolverDir(relativeSymlink), realpathSync(relativeDir)) + assert.equal(fixedResolverDir(relativeSymlink), realpathSync(appDir)) +}) + +test('plugin wrapper patches AppRun before delegating to the real output plugin', async () => { + const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-plugin-')) + const appDir = join(root, 'Tolaria.AppDir') + const appRun = join(appDir, 'AppRun') + const wrapper = join(root, 'linuxdeploy-plugin-appimage.AppImage') + const realPlugin = join(root, 'linuxdeploy-plugin-appimage.real.AppImage') + const pluginMarker = join(root, 'plugin-ran') + + await mkdir(appDir) + await writeFile( + appRun, + [ + '#! /usr/bin/env bash', + '# autogenerated by linuxdeploy', + BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE, + 'exec "$this_dir"/AppRun.wrapped "$@"', + ].join('\n'), + 'utf8', + ) + await writeFile(wrapper, appImagePluginWrapperSource(), 'utf8') + await chmod(wrapper, 0o755) + await writeFile( + realPlugin, + `#!/usr/bin/env bash\nset -euo pipefail\ntouch "${pluginMarker}"\n`, + 'utf8', + ) + await chmod(realPlugin, 0o755) + + const result = spawnSync(wrapper, [], { + encoding: 'utf8', + env: { + ...process.env, + APPDIR: appDir, + TOLARIA_APPIMAGE_REAL_PLUGIN: realPlugin, + }, + }) + + assert.equal(result.status, 0, result.stderr) + assert.equal(existsSync(pluginMarker), true) + + const patched = await readFile(appRun, 'utf8') + assert.equal(patched.includes(BROKEN_LINUXDEPLOY_APPRUN_DIR_LINE), false) + assert.equal(patched.includes(FIXED_LINUXDEPLOY_APPRUN_DIR_LINE), true) +})