Files
FreeCrawl-SEO-Tool/apps/desktop/scripts/after-pack.mjs
T

85 lines
3.8 KiB
JavaScript

// electron-builder `afterPack` hook, wired up in electron-builder.yml.
//
// Linux AppImage only: puts a small shell launcher in front of the Electron
// binary that adds `--no-sandbox` on systems where Chromium's sandbox cannot
// work, and leaves the sandbox on everywhere else.
//
// Why a launcher and not app code: Chromium's sandbox needs unprivileged user
// namespaces. Ubuntu 23.10+ denies them to every process that has no AppArmor
// profile (kernel.apparmor_restrict_unprivileged_userns=1), Chromium then
// falls back to the setuid `chrome-sandbox` helper, and that helper can never
// be setuid inside an AppImage (the FUSE mount is nosuid). The check runs in
// content's ContentMainRunnerImpl::Initialize, before the main process has
// executed a single line of JavaScript, so `app.commandLine.appendSwitch(
// 'no-sandbox')` is too late; the flag has to come from whatever execs the
// binary. electron-builder's own AppRun just execs `$APPDIR/<executableName>`
// with the user's arguments, so a script at that path is the one place that
// can decide.
//
// The .deb/.rpm are deliberately not wrapped: electron-builder's after-install
// script installs an AppArmor profile (Ubuntu 24.04+) and chmods
// chrome-sandbox 4755 where user namespaces are unavailable, so the sandbox
// keeps working there. That is why `build:linux` runs the AppImage in its own
// electron-builder invocation and this hook refuses mixed target lists.
import { chmodSync, renameSync, writeFileSync } from 'node:fs';
import path from 'node:path';
const REAL_SUFFIX = '.bin';
function launcherScript(exe) {
// Written with LF regardless of this file's own line endings: a CRLF shebang
// is "bad interpreter" on Linux.
return `#!/bin/sh
# FreeCrawl SEO AppImage launcher (generated by scripts/after-pack.mjs).
#
# Chromium's sandbox needs unprivileged user namespaces. When the kernel denies
# them to ordinary processes, Chromium falls back to a setuid chrome-sandbox
# helper that cannot exist inside an AppImage, and aborts with "No usable
# sandbox!" before the app starts. On such systems start with --no-sandbox;
# everywhere else the sandbox stays on.
here=$(dirname "$(readlink -f "$0")")
bin="$here/${exe}${REAL_SUFFIX}"
sysctl_is() {
[ -r "$1" ] && [ "$(cat "$1" 2>/dev/null)" = "$2" ]
}
for arg in "$@"; do
case "$arg" in
--no-sandbox) exec "$bin" "$@" ;;
esac
done
# Ubuntu 23.10+ (AppArmor), Debian/Arch style kernels, RHEL 7 style kernels.
if sysctl_is /proc/sys/kernel/apparmor_restrict_unprivileged_userns 1 \\
|| sysctl_is /proc/sys/kernel/unprivileged_userns_clone 0 \\
|| sysctl_is /proc/sys/user/max_user_namespaces 0; then
echo "FreeCrawl SEO: this system restricts unprivileged user namespaces; starting with --no-sandbox." >&2
exec "$bin" --no-sandbox "$@"
fi
exec "$bin" "$@"
`.replace(/\r\n/g, '\n');
}
export default async function afterPack(context) {
if (context.electronPlatformName !== 'linux') return;
const targets = context.targets.map((t) => t.name);
if (!targets.includes('appImage')) return;
const others = targets.filter((t) => t !== 'appImage');
if (others.length > 0) {
throw new Error(
`after-pack: the AppImage must be built in its own electron-builder run ` +
`(got targets: ${targets.join(', ')}). The launcher must not end up in the ` +
`.deb/.rpm; see build:linux in apps/desktop/package.json.`,
);
}
const exe = context.packager.executableName;
const binPath = path.join(context.appOutDir, exe);
const realPath = binPath + REAL_SUFFIX;
renameSync(binPath, realPath);
writeFileSync(binPath, launcherScript(exe), { mode: 0o755 });
chmodSync(binPath, 0o755); // the mode in writeFileSync is subject to umask
console.log(` • after-pack: AppImage launcher installed as ${exe}, Electron binary is now ${exe}${REAL_SUFFIX}`);
}