fix(packaging): preserve Flatpak Electron ELF

This commit is contained in:
4gray committed 2026-07-18 19:31:27 +02:00
1 parent d09237cbf0
commit 52e248ebf5
5 files changed
+268 -5

No files matched your search

+3 -1
View File
@@ -198,7 +198,9 @@ async function afterPackHook(params) {
}
);
await linuxAfterPack(params);
await linuxAfterPack(params, {
targetNames: linuxPackagingContext?.targetNames,
});
if (linuxPackagingContext) {
const graphicsMountPath = ensureSnapGraphicsContentMount(
params.appOutDir,
+20 -3
View File
@@ -1,5 +1,6 @@
const fs = require('fs/promises');
const path = require('path');
const { resolveLinuxLauncherLayout } = require('./linux-launcher-layout.cjs');
function log(message) {
console.log(` - ${message}`);
@@ -41,17 +42,33 @@ exec "$SCRIPT_DIR/${executableName}.bin" "\${EXEC_ARGS[@]}" "$@"
`;
}
async function afterPackHook(params) {
async function afterPackHook(params, { targetNames = params.targets } = {}) {
if (params.electronPlatformName !== 'linux') {
return;
}
const launcherLayout = resolveLinuxLauncherLayout(
targetNames,
params.packager.executableName
);
if (!launcherLayout.wrapperRequired) {
log('preserving the Electron ELF for isolated Flatpak packaging');
return;
}
log('applying Linux launcher sandbox fix');
const executable = path.join(params.appOutDir, params.packager.executableName);
const executable = path.join(
params.appOutDir,
params.packager.executableName
);
const electronBinary = path.join(
params.appOutDir,
launcherLayout.electronBinaryName
);
try {
await fs.rename(executable, `${executable}.bin`);
await fs.rename(executable, electronBinary);
await fs.writeFile(
executable,
createLoaderScript({
+187
View File
@@ -0,0 +1,187 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const linuxAfterPack = require('./linux-after-pack.cjs');
const { createLoaderScript } = linuxAfterPack;
const electronElf = Buffer.from([
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x49, 0x50, 0x54, 0x56,
]);
async function createAfterPackFixture(targets) {
const appOutDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'iptvnator-linux-after-pack-')
);
const executablePath = path.join(appOutDir, 'iptvnator');
await fs.writeFile(executablePath, electronElf, { mode: 0o755 });
return {
appOutDir,
executablePath,
params: {
appOutDir,
electronPlatformName: 'linux',
targets,
packager: {
executableName: 'iptvnator',
appInfo: {
productName: 'IPTVnator',
},
},
},
};
}
function resolveLinuxLauncherLayout(...args) {
return require('./linux-launcher-layout.cjs').resolveLinuxLauncherLayout(
...args
);
}
async function assertPathMissing(filePath) {
await assert.rejects(
fs.stat(filePath),
(error) => error?.code === 'ENOENT'
);
}
test('isolated Flatpak preserves the Electron ELF for Zypak', async (t) => {
const fixture = await createAfterPackFixture([{ name: 'flatpak' }]);
t.after(() =>
fs.rm(fixture.appOutDir, {
recursive: true,
force: true,
})
);
await linuxAfterPack(fixture.params);
assert.deepEqual(await fs.readFile(fixture.executablePath), electronElf);
await assertPathMissing(`${fixture.executablePath}.bin`);
});
test('resolves normalized string and Target-like launcher layouts', () => {
assert.deepEqual(resolveLinuxLauncherLayout([' FlatPak ']), {
targetNames: ['flatpak'],
electronBinaryName: 'iptvnator',
wrapperRequired: false,
});
assert.deepEqual(
resolveLinuxLauncherLayout(
[' AppImage ', { name: ' DEB ' }],
'iptvnator-player'
),
{
targetNames: ['appimage', 'deb'],
electronBinaryName: 'iptvnator-player.bin',
wrapperRequired: true,
}
);
});
test('rejects missing, empty, and non-array target lists', () => {
for (const targets of [undefined, null, 'flatpak', new Map(), {}]) {
assert.throws(
() => resolveLinuxLauncherLayout(targets),
/Linux launcher targets must be an array/
);
}
assert.throws(
() => resolveLinuxLauncherLayout([]),
/Linux launcher targets must contain at least one target/
);
});
test('rejects empty target names and normalized duplicates', () => {
for (const target of ['', ' ', {}, { name: '' }, null, 42]) {
assert.throws(
() => resolveLinuxLauncherLayout([target]),
/Linux launcher targets must expose a non-empty name/
);
}
assert.throws(
() => resolveLinuxLauncherLayout([{ name: ' DEB ' }, { name: 'deb' }]),
/Linux launcher target "deb" is duplicated/
);
});
test('rejects Flatpak mixed with another target', () => {
assert.throws(
() =>
resolveLinuxLauncherLayout([
{ name: 'flatpak' },
{ name: 'AppImage' },
]),
/Flatpak must be packaged in an isolated Electron Builder pass so Zypak receives the Electron ELF directly/
);
});
for (const targetName of ['appimage', 'deb', 'rpm', 'pacman', 'snap']) {
test(`${targetName} retains the launcher wrapper and Electron ELF binary`, async (t) => {
const fixture = await createAfterPackFixture([{ name: targetName }]);
t.after(() =>
fs.rm(fixture.appOutDir, {
recursive: true,
force: true,
})
);
await linuxAfterPack(fixture.params);
assert.equal(
await fs.readFile(fixture.executablePath, 'utf8'),
createLoaderScript({
executableName: 'iptvnator',
productName: 'IPTVnator',
})
);
assert.deepEqual(
await fs.readFile(`${fixture.executablePath}.bin`),
electronElf
);
assert.equal(
(await fs.stat(fixture.executablePath)).mode & 0o777,
0o755
);
});
}
test('mixed Flatpak targets fail before mutating the Electron ELF', async (t) => {
const fixture = await createAfterPackFixture([
{ name: 'flatpak' },
{ name: 'appimage' },
]);
t.after(() =>
fs.rm(fixture.appOutDir, {
recursive: true,
force: true,
})
);
await assert.rejects(
linuxAfterPack(fixture.params),
/Flatpak must be packaged in an isolated Electron Builder pass so Zypak receives the Electron ELF directly/
);
assert.deepEqual(await fs.readFile(fixture.executablePath), electronElf);
await assertPathMissing(`${fixture.executablePath}.bin`);
});
test('packaging Nx tests register the Linux afterPack regression suite', async () => {
const project = JSON.parse(
await fs.readFile(new URL('./project.json', import.meta.url), 'utf8')
);
const testFile =
'{workspaceRoot}/tools/packaging/linux-after-pack.test.mjs';
assert.ok(project.targets.test.inputs.includes(testFile));
assert.match(
project.targets.test.options.command,
/node --test .*tools\/packaging\/linux-after-pack\.test\.mjs/
);
});
+56
View File
@@ -0,0 +1,56 @@
'use strict';
function normalizedTargetName(target) {
const value =
typeof target === 'string'
? target
: target && typeof target === 'object'
? target.name
: null;
if (typeof value !== 'string' || value.trim() === '') {
throw new Error('Linux launcher targets must expose a non-empty name.');
}
return value.trim().toLowerCase();
}
function resolveLinuxLauncherLayout(targets, executableName = 'iptvnator') {
if (!Array.isArray(targets)) {
throw new TypeError('Linux launcher targets must be an array.');
}
if (targets.length === 0) {
throw new Error(
'Linux launcher targets must contain at least one target.'
);
}
const targetNames = [];
for (const target of targets) {
const targetName = normalizedTargetName(target);
if (targetNames.includes(targetName)) {
throw new Error(
`Linux launcher target "${targetName}" is duplicated.`
);
}
targetNames.push(targetName);
}
const flatpakSelected = targetNames.includes('flatpak');
if (flatpakSelected && targetNames.length !== 1) {
throw new Error(
'Flatpak must be packaged in an isolated Electron Builder pass so Zypak receives the Electron ELF directly.'
);
}
const wrapperRequired = !flatpakSelected;
return {
targetNames,
electronBinaryName: wrapperRequired
? `${executableName}.bin`
: executableName,
wrapperRequired,
};
}
module.exports = {
resolveLinuxLauncherLayout,
};
+2 -1
View File
@@ -24,6 +24,7 @@
"{workspaceRoot}/tools/packaging/embedded-mpv-arch.test.mjs",
"{workspaceRoot}/tools/packaging/configure-linux-frame-copy-build.mjs",
"{workspaceRoot}/tools/packaging/configure-linux-frame-copy-build.test.mjs",
"{workspaceRoot}/tools/packaging/linux-after-pack.test.mjs",
"{workspaceRoot}/tools/packaging/linux-frame-copy-profile.cjs",
"{workspaceRoot}/tools/packaging/linux-frame-copy-profile.test.mjs",
"{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.cjs",
@@ -49,7 +50,7 @@
"{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs"
],
"options": {
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/configure-linux-frame-copy-build.test.mjs tools/packaging/linux-after-pack.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/packaging/prepare-linux-runtime-source-snapshot.test.mjs tools/packaging/publish-snap-workflow.test.mjs tools/packaging/release-snap-assets.test.mjs tools/packaging/verify-linux-frame-copy-runtime.test.mjs tools/embedded-mpv/build-linux-runtime.test.mjs tools/embedded-mpv/generate-linux-runtime-notices.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"cwd": "{workspaceRoot}"
}
},