super-productivity/tools/afterPack.js
David Vornholt a7845acd81
fix(electron): retry Wayland idle helper startup (#7527)
* fix(electron): retry wayland idle helper startup

* build(electron): enforce wayland helper in CI

* fix(electron): avoid blocking wayland idle promotion

* test(e2e): make reminder option selector exact

* test(e2e): resolve reminder option selector conflict

* fix(electron): gate wayland helper redetection

* fix(electron): avoid stale Wayland helper packaging

---------

Co-authored-by: Johannes Millan <johannes.millan@gmail.com>
2026-05-10 22:35:33 +02:00

120 lines
4.4 KiB
JavaScript

// Post-pack hook for Linux builds.
//
// Renames the main Electron binary to `superproductivity-bin` and installs
// a shell wrapper at the original name. The wrapper forces
// --class=superproductivity for stable desktop-file matching and forces
// --ozone-platform=x11 when running in our Snap sandbox on a Wayland session.
// Non-Snap launches (AppImage, .deb, .rpm) and X11 sessions only receive the
// stable class flag.
//
// Context: field reports on issue #7270 (v18.2.4/v18.2.5) show that
// app.commandLine.appendSwitch('ozone-platform','x11') from inside the
// main process is not equivalent to passing the flag on argv — Chromium's
// Ozone init in the browser process dlopens libEGL/libgbm on the core22
// Mesa path before the switch is honored, which segfaults under Mesa ABI
// drift. Injecting the flag via argv before Electron starts bypasses this.
//
// See docs/research/snap-wayland-gpu-fix-research.md §18.
const { promises: fs } = require('fs');
const { join } = require('path');
const BIN_NAME = 'superproductivity'; // must match linux.executableName
const RENAMED = 'superproductivity-bin';
const WRAPPER_SRC = join(__dirname, '..', 'build', 'linux', 'snap-wrapper.sh');
const WAYLAND_IDLE_HELPER_SRC = join(
__dirname,
'..',
'electron',
'bin',
'wayland-idle-helper',
);
const WAYLAND_IDLE_HELPER_DEST = 'wayland-idle-helper';
const isTruthyEnv = (value) => value === '1' || value?.toLowerCase() === 'true';
async function afterPack(context) {
if (context.electronPlatformName !== 'linux') return;
const { appOutDir } = context;
const binPath = join(appOutDir, BIN_NAME);
const renamedPath = join(appOutDir, RENAMED);
const helperDestPath = join(appOutDir, WAYLAND_IDLE_HELPER_DEST);
const installWaylandIdleHelper = async () => {
if (isTruthyEnv(process.env.SP_SKIP_WAYLAND_IDLE_HELPER_BUILD)) {
console.warn(
'[afterPack] Skipping Wayland idle helper copy because SP_SKIP_WAYLAND_IDLE_HELPER_BUILD is set',
);
return;
}
const helperStat = await fs.stat(WAYLAND_IDLE_HELPER_SRC).catch(() => null);
if (!helperStat) {
throw new Error(
`[afterPack] ${WAYLAND_IDLE_HELPER_SRC} not found. Linux packages must include the Wayland idle helper; install Rust/Cargo before packaging or set SP_SKIP_WAYLAND_IDLE_HELPER_BUILD=1 to intentionally omit ext-idle-notify support.`,
);
}
await fs.copyFile(WAYLAND_IDLE_HELPER_SRC, helperDestPath);
await fs.chmod(helperDestPath, 0o755);
};
// Read wrapper content BEFORE touching appOutDir. If the source file is
// missing or unreadable we fail fast with the Electron binary still in
// place — no broken intermediate state.
const wrapperContent = await fs.readFile(WRAPPER_SRC, 'utf8');
const [binStat, renamedStat] = await Promise.all([
fs.stat(binPath).catch(() => null),
fs.stat(renamedPath).catch(() => null),
]);
if (!binStat && !renamedStat) {
console.warn(`[afterPack] ${binPath} not found; skipping wrapper install`);
return;
}
// Idempotency: "already installed" requires both files + a shell shebang at
// binPath. A shebang-less binPath co-existing with renamedPath means a
// prior run crashed mid-way; fall through to re-write the wrapper.
if (binStat && renamedStat) {
const head = await fs.readFile(binPath, 'utf8').catch(() => '');
if (head.startsWith('#!')) {
await installWaylandIdleHelper();
console.log(`[afterPack] wrapper already installed`);
return;
}
throw new Error(
`[afterPack] unexpected state at ${appOutDir}: both ${BIN_NAME} and ` +
`${RENAMED} exist but ${BIN_NAME} is not a shell script. Refusing ` +
`to overwrite; investigate manually.`,
);
}
// Fresh install path: binStat exists, renamedStat doesn't → rename first.
// Partial-recovery path: only renamedStat exists → skip rename, just
// re-write the wrapper on top of the missing slot.
if (!renamedStat) {
await fs.rename(binPath, renamedPath);
}
try {
await fs.writeFile(binPath, wrapperContent, { mode: 0o755 });
} catch (err) {
// Best-effort rollback so the build doesn't ship a pkg with no launcher.
if (!renamedStat) {
await fs.rename(renamedPath, binPath).catch(() => {});
}
throw err;
}
await fs.chmod(renamedPath, 0o755);
await installWaylandIdleHelper();
console.log(
`[afterPack] Installed argv wrapper: ${BIN_NAME} -> ${RENAMED} + shell wrapper`,
);
}
module.exports = afterPack;