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) }) test('plugin wrapper bundles fcitx GTK3 input module before sealing AppImage', async () => { const root = await mkdtemp(join(tmpdir(), 'tolaria-appimage-fcitx-')) const appDir = join(root, 'Tolaria.AppDir') const wrapper = join(root, 'linuxdeploy-plugin-appimage.AppImage') const realPlugin = join(root, 'linuxdeploy-plugin-appimage.real.AppImage') const pluginMarker = join(root, 'plugin-ran') const hostModule = join(root, 'host', 'im-fcitx5.so') const hostLibraryDir = join(root, 'host-lib') const hostLibrary = join(hostLibraryDir, 'libFcitx5GClient.so.2') await mkdir(appDir) await mkdir(dirname(hostModule), { recursive: true }) await mkdir(hostLibraryDir) await writeFile(hostModule, 'fake fcitx gtk module', 'utf8') await writeFile(hostLibrary, 'fake fcitx client library', '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, TOLARIA_FCITX_GTK3_IM_MODULE: hostModule, TOLARIA_FCITX_LIBRARY_DIR: hostLibraryDir, }, }) assert.equal(result.status, 0, result.stderr) assert.equal(existsSync(pluginMarker), true) assert.equal( existsSync( join( appDir, 'usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so', ), ), true, ) assert.equal(existsSync(join(appDir, 'usr/lib/x86_64-linux-gnu/libFcitx5GClient.so.2')), true) })