327 lines
8.9 KiB
JavaScript
327 lines
8.9 KiB
JavaScript
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 APPIMAGE_FCITX_GTK3_IM_MODULE_PATH =
|
|
'usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so'
|
|
export const APPIMAGE_FCITX_GCLIENT_LIBRARY_PATH =
|
|
'usr/lib/x86_64-linux-gnu/libFcitx5GClient.so.2'
|
|
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'
|
|
const REQUIRED_APPIMAGE_PATHS = [
|
|
'AppRun',
|
|
APPIMAGE_FCITX_GTK3_IM_MODULE_PATH,
|
|
APPIMAGE_FCITX_GCLIENT_LIBRARY_PATH,
|
|
]
|
|
|
|
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}"}"
|
|
FCITX_GTK3_IM_MODULE="\${TOLARIA_FCITX_GTK3_IM_MODULE:-/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules/im-fcitx5.so}"
|
|
FCITX_LIBRARY_DIR="\${TOLARIA_FCITX_LIBRARY_DIR:-/usr/lib/x86_64-linux-gnu}"
|
|
|
|
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
|
|
}
|
|
|
|
bundle_fcitx_gtk3_module() {
|
|
local appdir="\${APPDIR:-}"
|
|
|
|
if [ -z "$appdir" ]; then
|
|
appdir="$(appdir_from_args "$@" || true)"
|
|
fi
|
|
|
|
if [ -z "$appdir" ] || [ ! -d "$appdir" ] || [ ! -f "$FCITX_GTK3_IM_MODULE" ]; then
|
|
return 0
|
|
fi
|
|
|
|
local module_dest="$appdir/${APPIMAGE_FCITX_GTK3_IM_MODULE_PATH}"
|
|
local library_dest_dir="$appdir/usr/lib/x86_64-linux-gnu"
|
|
|
|
mkdir -p "$(dirname -- "$module_dest")" "$library_dest_dir"
|
|
cp -a "$FCITX_GTK3_IM_MODULE" "$module_dest"
|
|
|
|
local copied_library=0
|
|
shopt -s nullglob
|
|
for lib in "$FCITX_LIBRARY_DIR"/libFcitx5GClient.so* "$FCITX_LIBRARY_DIR"/libFcitx5Utils.so*; do
|
|
cp -a "$lib" "$library_dest_dir/"
|
|
copied_library=1
|
|
done
|
|
shopt -u nullglob
|
|
|
|
if [ "$copied_library" -eq 0 ]; then
|
|
echo "No fcitx GTK client libraries found in $FCITX_LIBRARY_DIR" >&2
|
|
fi
|
|
}
|
|
|
|
download_real_plugin
|
|
patch_apprun "$@"
|
|
bundle_fcitx_gtk3_module "$@"
|
|
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)
|
|
}
|
|
|
|
function extractAppImagePath(appImage, requiredPath, tempDir) {
|
|
const result = spawnSync(appImage, ['--appimage-extract', requiredPath], {
|
|
cwd: tempDir,
|
|
encoding: 'utf8',
|
|
})
|
|
|
|
if (result.status === 0) {
|
|
return
|
|
}
|
|
|
|
throw new Error(
|
|
[
|
|
`Failed to extract ${requiredPath} from ${appImage}`,
|
|
result.stdout.trim(),
|
|
result.stderr.trim(),
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n'),
|
|
)
|
|
}
|
|
|
|
function assertAppImagePathsExtracted(appImage, tempDir, requiredPaths) {
|
|
for (const requiredPath of requiredPaths) {
|
|
const extractedPath = join(tempDir, 'squashfs-root', requiredPath)
|
|
if (!existsSync(extractedPath)) {
|
|
throw new Error(`${appImage} is missing ${requiredPath}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function validateExtractedAppImage(appImage, tempDir) {
|
|
for (const requiredPath of REQUIRED_APPIMAGE_PATHS) {
|
|
extractAppImagePath(appImage, requiredPath, tempDir)
|
|
}
|
|
|
|
await validateAppRunFile(join(tempDir, 'squashfs-root', 'AppRun'))
|
|
assertAppImagePathsExtracted(appImage, tempDir, REQUIRED_APPIMAGE_PATHS.slice(1))
|
|
}
|
|
|
|
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 {
|
|
await validateExtractedAppImage(appImage, tempDir)
|
|
} 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 <AppRun...> | validate-appimages <AppImage...>'
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|