fix(packaging): preserve Flatpak Electron ELF for Zypak (#1205)

Fixes the launcher/Zypak regression reported in #1203. The additional GPU/video.js behavior remains tracked separately in that issue.
This commit is contained in:
4gray authored and GitHub committed 2026-07-18 22:42:45 +02:00
1 parent 8fdac824fd
commit c266eaa680
22 files changed
+1679 -38

No files matched your search

+7 -5
View File
@@ -695,11 +695,13 @@ Linux release profiles:
source-built closure and manifest origin. Its app-level probe reconstructs
only the exact Freedesktop 24.08 EGL external-platform search path inside the
trusted `/app` payload.
- The three profiles are separate packaging passes; mixing target sets fails
closed. Linux packages for other architectures (arm64, armv7l) must not ship
x64 native artifacts. `afterPack` replaces the native directory with
`embedded-mpv-unavailable.txt`, and package verification requires that
marker.
- Flatpak is an isolated packaging pass and keeps `iptvnator` as the real
Electron ELF so Electron Builder's `electron-wrapper` passes it directly to
Zypak. Other Linux targets retain the conditional `iptvnator` wrapper and
`iptvnator.bin`. Mixed Flatpak/non-Flatpak target sets fail before mutation.
- Linux packages for other architectures (arm64, armv7l) must not ship x64
native artifacts. `afterPack` replaces the native directory with
`embedded-mpv-unavailable.txt`, and package verification requires that marker.
- Every packaged manifest names its exact artifacts, profile/targets, libmpv
SONAME, loader closure, byte sizes, SHA-256 hashes, package dependencies, and
native-view fallback. Artifact modes and ELF dependency isolation are
@@ -0,0 +1,738 @@
# Flatpak Zypak Launcher Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use
> superpowers:subagent-driven-development (recommended) or
> superpowers:executing-plans to implement this plan task-by-task. Steps use
> checkbox (`- [ ]`) syntax for tracking.
**Goal:** Keep the Flatpak `iptvnator` entry as the real Electron ELF so
Electron Builder passes it directly to Zypak, while preserving the existing
Linux sandbox wrapper for every other package target.
**Architecture:** A small CommonJS launcher-layout contract resolves the
Electron binary name from normalized target names and rejects mixed Flatpak
passes. The afterPack hook, unpacked-layout validators, final-artifact
validator, and CI all consume the same target-dependent contract.
**Tech Stack:** Node.js CommonJS/ESM, `node:test`, Nx packaging targets,
Electron Builder 26, Flatpak/Zypak, GitHub Actions.
---
### Task 1: Add the shared launcher contract and fix afterPack
**Files:**
- Create: `tools/packaging/linux-launcher-layout.cjs`
- Create: `tools/packaging/linux-after-pack.test.mjs`
- Modify: `tools/packaging/linux-after-pack.cjs`
- Modify: `tools/packaging/electron-after-pack.cjs`
- Modify: `tools/packaging/project.json`
- [ ] **Step 1: Write the failing isolated-Flatpak hook test**
Create a temporary executable with ELF magic, call the real hook, and assert
that Flatpak retains the exact original file:
```js
test('preserves the Electron ELF for an isolated Flatpak target', async (t) => {
const fixture = createLauncherFixture();
t.after(() => fs.rmSync(fixture.root, { recursive: true, force: true }));
await linuxAfterPack(createAfterPackParams(fixture.appOutDir, ['flatpak']));
assert.deepEqual(
fs.readFileSync(fixture.executablePath),
fixture.executableBytes
);
assert.equal(fs.existsSync(`${fixture.executablePath}.bin`), false);
});
```
- [ ] **Step 2: Run the test and verify RED**
Run:
```bash
node --test tools/packaging/linux-after-pack.test.mjs
```
Expected: FAIL because the current hook replaces `iptvnator` with a Bash
script and creates `iptvnator.bin`.
- [ ] **Step 3: Add the pure launcher-layout resolver**
Implement a strict resolver with this public contract:
```js
function resolveLinuxLauncherLayout(targets, executableName = 'iptvnator') {
if (!Array.isArray(targets) || targets.length === 0) {
throw new Error(
'Linux launcher layout requires at least one Electron Builder target.'
);
}
const targetNames = targets.map((target) => {
const value = typeof target === 'string' ? target : target?.name;
const name = String(value ?? '')
.trim()
.toLowerCase();
if (!name) {
throw new Error(
'Linux launcher targets must expose a non-empty name.'
);
}
return name;
});
if (new Set(targetNames).size !== targetNames.length) {
throw new Error('Linux launcher targets must be unique.');
}
const flatpak = targetNames.includes('flatpak');
if (flatpak && targetNames.length !== 1) {
throw new Error(
'Flatpak must be packaged in an isolated Electron Builder pass so Zypak receives the Electron ELF directly.'
);
}
return {
targetNames,
electronBinaryName: flatpak ? executableName : `${executableName}.bin`,
wrapperRequired: !flatpak,
};
}
```
Export `resolveLinuxLauncherLayout`.
- [ ] **Step 4: Make the Linux hook preserve isolated Flatpak**
Resolve the layout before any filesystem mutation. For Flatpak, log that the
ELF is preserved and return. For other targets, rename to the resolved
`electronBinaryName` and create the unchanged sandbox wrapper:
```js
async function afterPackHook(params, { targetNames = params.targets } = {}) {
if (params.electronPlatformName !== 'linux') {
return;
}
const layout = resolveLinuxLauncherLayout(
targetNames,
params.packager.executableName
);
if (!layout.wrapperRequired) {
log('preserving Flatpak Electron ELF for direct Zypak launch');
return;
}
const executable = path.join(
params.appOutDir,
params.packager.executableName
);
const electronBinary = path.join(
params.appOutDir,
layout.electronBinaryName
);
try {
await fs.rename(executable, electronBinary);
await fs.writeFile(
executable,
createLoaderScript({
executableName: params.packager.executableName,
productName: params.packager.appInfo.productName,
})
);
await fs.chmod(executable, 0o755);
} catch (error) {
log(`failed to create launcher wrapper: ${error.message}`);
throw new Error('Failed to create launcher wrapper');
}
log('Linux launcher sandbox fix applied');
}
```
Pass `linuxPackagingContext?.targetNames` from `electron-after-pack.cjs` so
the hook consumes the already validated afterPack target names:
```js
await linuxAfterPack(params, {
targetNames: linuxPackagingContext?.targetNames,
});
```
- [ ] **Step 5: Add non-Flatpak and mixed-target regression cases**
Use the real hook to prove:
```js
test('keeps the sandbox wrapper for non-Flatpak targets', async (t) => {
for (const targetName of ['appimage', 'deb', 'rpm', 'pacman', 'snap']) {
const fixture = createLauncherFixture();
t.after(() =>
fs.rmSync(fixture.root, { recursive: true, force: true })
);
await linuxAfterPack(
createAfterPackParams(fixture.appOutDir, [targetName])
);
assert.deepEqual(
fs.readFileSync(`${fixture.executablePath}.bin`),
fixture.executableBytes
);
assert.match(
fs.readFileSync(fixture.executablePath, 'utf8'),
/exec "\$SCRIPT_DIR\/iptvnator\.bin"/
);
}
});
test('rejects a mixed Flatpak pass before mutating the executable', async (t) => {
const fixture = createLauncherFixture();
t.after(() => fs.rmSync(fixture.root, { recursive: true, force: true }));
await assert.rejects(
linuxAfterPack(
createAfterPackParams(fixture.appOutDir, ['flatpak', 'appimage'])
),
/Flatpak must be packaged in an isolated Electron Builder pass/
);
assert.deepEqual(
fs.readFileSync(fixture.executablePath),
fixture.executableBytes
);
assert.equal(fs.existsSync(`${fixture.executablePath}.bin`), false);
});
```
- [ ] **Step 6: Register and run the focused test**
Add `linux-after-pack.test.mjs` to the explicit `packaging:test` command and
inputs in `tools/packaging/project.json`.
Run:
```bash
node --test tools/packaging/linux-after-pack.test.mjs
```
Expected: all launcher tests PASS.
- [ ] **Step 7: Commit Task 1**
```bash
git add tools/packaging/linux-launcher-layout.cjs \
tools/packaging/linux-after-pack.cjs \
tools/packaging/linux-after-pack.test.mjs \
tools/packaging/electron-after-pack.cjs \
tools/packaging/project.json
git commit -m "fix(packaging): preserve Flatpak Electron ELF"
```
### Task 2: Make unpacked-layout validation target-aware
**Files:**
- Modify: `tools/packaging/embedded-mpv-packaging.cjs`
- Modify: `tools/packaging/embedded-mpv-arch.test.mjs`
- Modify: `tools/packaging/verify-electron-package-layout.mjs`
- Modify: `tools/packaging/electron-package-identity.test.mjs`
- [ ] **Step 1: Change the Flatpak fixture and verify RED**
In `prepares portable and Flatpak manifests with the exact bundled closure`,
rename only the Flatpak fixture's Electron binary:
```js
fs.renameSync(
join(flatpak.appOutDir, 'iptvnator.bin'),
join(flatpak.appOutDir, 'iptvnator')
);
```
Teach the test ELF inspector about both legitimate basenames:
```js
['iptvnator', { needed: ['libc.so.6'], rpath: [], runpath: [] }],
['iptvnator.bin', { needed: ['libc.so.6'], rpath: [], runpath: [] }],
```
Run:
```bash
node --test --test-name-pattern='prepares portable and Flatpak manifests' \
tools/packaging/embedded-mpv-arch.test.mjs
```
Expected: FAIL with a missing `iptvnator.bin` validation error.
- [ ] **Step 2: Resolve the pristine Electron ELF through the shared contract**
Require `resolveLinuxLauncherLayout` in
`embedded-mpv-packaging.cjs`. In `inspectLinuxElfIsolation`, resolve from
`options.targetNames` and use its `electronBinaryName`:
```js
let launcherLayout;
try {
launcherLayout = resolveLinuxLauncherLayout(
options.targetNames,
options.executableName ?? 'iptvnator'
);
} catch (error) {
errors.push(
`Unable to resolve Linux launcher layout: ${
error instanceof Error ? error.message : String(error)
}`
);
return;
}
const inspectedPaths = {
electron: path.join(
path.dirname(resourceDir),
launcherLayout.electronBinaryName
),
addon: path.join(nativeDir, linuxFrameCopyArtifacts.addon.name),
reader: path.join(nativeDir, linuxFrameCopyArtifacts.frameReader.name),
helper: path.join(nativeDir, linuxFrameCopyArtifacts.helper.name),
};
for (const [index, libraryPath] of listElectronShippedLinuxLibraries(
resourceDir,
{ artifactFormat: options.artifactFormat }
).entries()) {
inspectedPaths[`electronLibrary:${index}`] = libraryPath;
}
```
Pass the normalized `targetNames` already calculated by
`validateLinuxPackagedEmbeddedMpv` into the inspection options.
- [ ] **Step 3: Make the general package-layout verifier profile-aware**
Require the same resolver in `verify-electron-package-layout.mjs`, change
`verifyLinuxLauncher` to accept `targetNames`, and call it with
`linuxTargetNames`.
For Flatpak:
```js
if (!layout.wrapperRequired) {
if (fileExists(`${launcherPath}.bin`)) {
errors.push(
`Flatpak must not contain the Linux sandbox wrapper binary: ${launcherPath}.bin`
);
}
if (!fileHasElfMagic(launcherPath)) {
errors.push(
`Flatpak launcher target must be an ELF executable: ${launcherPath}`
);
}
return;
}
const launcherBinaryPath = path.join(appDir, layout.electronBinaryName);
if (!fileExists(launcherBinaryPath)) {
errors.push(
`Missing Linux launcher binary in ${appDir}: ${path.basename(launcherBinaryPath)}`
);
return;
}
if (!fileExists(launcherPath)) {
errors.push(
`Missing Linux launcher wrapper in ${appDir}: ${path.basename(launcherPath)}`
);
return;
}
const launcherScript = fs.readFileSync(launcherPath, 'utf8');
const requiredMarkers = [
'SCRIPT_PATH="${BASH_SOURCE[0]}"',
'readlink -f "$SCRIPT_PATH"',
`exec "$SCRIPT_DIR/${linuxExecutableName}.bin"`,
];
const missingMarkers = requiredMarkers.filter(
(marker) => !launcherScript.includes(marker)
);
if (missingMarkers.length > 0) {
errors.push(
[
`Linux launcher wrapper is missing symlink-safe logic in ${launcherPath}.`,
'Missing markers:',
...missingMarkers.map((marker) => `- ${marker}`),
].join('\n')
);
}
```
Implement `fileHasElfMagic` with one four-byte `fs.readSync` call and always
close the descriptor:
```js
function fileHasElfMagic(filePath) {
const descriptor = fs.openSync(filePath, 'r');
try {
const magic = Buffer.alloc(4);
return (
fs.readSync(descriptor, magic, 0, magic.length, 0) ===
magic.length &&
magic.equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
);
} finally {
fs.closeSync(descriptor);
}
}
```
- [ ] **Step 4: Extend the package-identity source contract test**
Assert that the general verifier imports the shared resolver, calls
`verifyLinuxLauncher(resourceDir, linuxTargetNames, errors)`, checks ELF magic,
and does not unconditionally set `launcherBinaryPath` before resolving target
layout.
- [ ] **Step 5: Run targeted validation**
```bash
node --test --test-name-pattern='prepares portable and Flatpak manifests' \
tools/packaging/embedded-mpv-arch.test.mjs
node --test --test-name-pattern='package layout verifier uses' \
tools/packaging/electron-package-identity.test.mjs
```
Expected: both commands PASS.
- [ ] **Step 6: Commit Task 2**
```bash
git add tools/packaging/embedded-mpv-packaging.cjs \
tools/packaging/embedded-mpv-arch.test.mjs \
tools/packaging/verify-electron-package-layout.mjs \
tools/packaging/electron-package-identity.test.mjs
git commit -m "fix(packaging): validate Flatpak launcher ELF"
```
### Task 3: Update final-artifact verification, CI, and documentation
**Files:**
- Modify: `tools/packaging/verify-linux-frame-copy-runtime.mjs`
- Modify: `tools/packaging/verify-linux-frame-copy-runtime.test.mjs`
- Modify: `.github/workflows/build-and-make.yaml`
- Modify: `tools/packaging/configure-linux-frame-copy-build.test.mjs`
- Modify: `docs/architecture/embedded-mpv-native.md`
- Modify: `tools/embedded-mpv/README.md`
- Modify: `docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md`
- Modify: `AGENTS.md`
- Modify: `CLAUDE.md`
- [ ] **Step 1: Add a failing extracted-Flatpak regression**
Allow the fixture helper to select the Electron filename:
```js
function createSystemPayload({
architecture = 'x64',
electronBinaryName = 'iptvnator.bin',
} = {}) {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-verifier-layout-')
);
const appDir = path.join(root, 'opt', 'IPTVnator');
const resourceDir = path.join(appDir, 'resources');
const nativeDir = path.join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(nativeDir, { recursive: true });
fs.writeFileSync(
path.join(appDir, electronBinaryName),
elfHeader(architecture)
);
if (architecture === 'x64') {
fs.writeFileSync(path.join(nativeDir, 'embedded_mpv.node'), 'addon', {
mode: 0o644,
});
fs.writeFileSync(
path.join(nativeDir, 'embedded_mpv_frame_reader.node'),
'reader',
{ mode: 0o644 }
);
fs.writeFileSync(
path.join(nativeDir, 'iptvnator_mpv_helper'),
'helper',
{ mode: 0o755 }
);
fs.writeFileSync(
path.join(nativeDir, 'embedded-mpv-runtime.json'),
`${JSON.stringify(SYSTEM_MANIFEST, null, 2)}\n`,
{ mode: 0o644 }
);
} else {
fs.writeFileSync(
path.join(nativeDir, 'embedded-mpv-unavailable.txt'),
`Unavailable for ${architecture}\n`
);
}
return { root, appDir, resourceDir, nativeDir };
}
```
Use a foreign-architecture marker fixture so this test isolates launcher
selection without needing a bundled x64 manifest:
```js
test('validates a marker-only Flatpak with an unwrapped Electron ELF', () => {
const fixture = createSystemPayload({
architecture: 'arm64',
electronBinaryName: 'iptvnator',
});
try {
assert.deepEqual(
verifyExtractedLinuxFrameCopyRuntime({
resourceDir: fixture.resourceDir,
artifactFormat: 'flatpak',
profileName: 'flatpak',
packageDependencies: [],
elfInspector: validElfInspector,
probeRunner() {
assert.fail(
'foreign Flatpak must not run the helper probe'
);
},
}),
[]
);
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}
});
```
Run:
```bash
node --test --test-name-pattern='marker-only Flatpak with an unwrapped' \
tools/packaging/verify-linux-frame-copy-runtime.test.mjs
```
Expected: FAIL because the verifier reads `iptvnator.bin`.
- [ ] **Step 2: Resolve every final-artifact Electron path consistently**
Require `resolveLinuxLauncherLayout` and add:
```js
function resolveElectronBinaryPath(resourceDir, artifactFormat) {
const layout = resolveLinuxLauncherLayout([artifactFormat]);
return path.join(path.dirname(resourceDir), layout.electronBinaryName);
}
```
Use it in:
- `validateElectronIsolation`;
- `verifyExtractedLinuxFrameCopyRuntime` architecture detection;
- the architecture returned from `verifyLinuxFrameCopyArtifact`.
Keep Snap/AppImage/DEB/RPM/Pacman expectations on `iptvnator.bin`.
- [ ] **Step 3: Add an outer artifact-verifier Flatpak regression**
Create a temporary `.flatpak` file, inject an extractor that writes
`iptvnator` ELF and the marker-only native directory under its supplied
destination, and assert:
```js
assert.deepEqual(
verifyLinuxFrameCopyArtifact({
artifactPath,
profileName: 'flatpak',
extractArtifact({ destination }) {
const appDir = path.join(
destination,
'files',
'lib',
'com.fourgray.iptvnator'
);
const resourceDir = path.join(appDir, 'resources');
const nativeDir = path.join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(nativeDir, { recursive: true });
fs.writeFileSync(
path.join(appDir, 'iptvnator'),
elfHeader('arm64')
);
fs.writeFileSync(
path.join(nativeDir, 'embedded-mpv-unavailable.txt'),
'Unavailable for arm64\n'
);
return destination;
},
metadataReader: () => ({
declaredArch: 'arm64',
dependencies: [],
}),
elfInspector: validElfInspector,
probeRunner() {
assert.fail('foreign Flatpak must not probe');
},
}),
{
artifactPath: path.resolve(artifactPath),
format: 'flatpak',
profileName: 'flatpak',
architecture: 'arm64',
}
);
```
- [ ] **Step 4: Invert the installed-Flatpak CI layout assertion**
Inside the existing sandbox shell check:
```bash
LAUNCHER_PATH="$(readlink -f /app/bin/iptvnator)"
test -f "${LAUNCHER_PATH}"
test ! -e "${LAUNCHER_PATH}.bin"
ELF_MAGIC="$(od -An -tx1 -N4 "${LAUNCHER_PATH}" | tr -d "[:space:]")"
test "${ELF_MAGIC}" = "7f454c46"
```
Capture the application-level probe output and fail on the historical Zypak
diagnostics:
```bash
PROBE_OUTPUT="$(
xvfb-run -a dbus-run-session -- flatpak run \
--env=LIBGL_ALWAYS_SOFTWARE=1 \
com.fourgray.iptvnator \
--embedded-mpv-runtime-probe 2>&1
)"
printf '%s\n' "${PROBE_OUTPUT}"
if printf '%s\n' "${PROBE_OUTPUT}" |
grep -Eq 'not an ELF file|Zypak needs to be called directly'; then
echo "::error::Flatpak launched a wrapper instead of the Electron ELF."
exit 1
fi
```
Update `configure-linux-frame-copy-build.test.mjs` to require the ELF magic
check, `.bin` rejection, warning guard, and absence of the old wrapper-marker
greps.
- [ ] **Step 5: Update canonical launcher documentation**
Document the exact invariant in all listed documentation:
```text
Flatpak is an isolated packaging pass and keeps `iptvnator` as the real
Electron ELF so Electron Builder's `electron-wrapper` passes it directly to
Zypak. Other Linux targets retain the conditional `iptvnator` wrapper and
`iptvnator.bin`. Mixed Flatpak/non-Flatpak target sets fail before mutation.
```
In the earlier frame-copy design, replace the unconditional
`Electron executable (iptvnator.bin)` wording with
`iptvnator for Flatpak; iptvnator.bin for other Linux targets`.
- [ ] **Step 6: Run targeted tests and formatting**
```bash
node --test --test-name-pattern='Flatpak|Linux CI verifies' \
tools/packaging/verify-linux-frame-copy-runtime.test.mjs \
tools/packaging/configure-linux-frame-copy-build.test.mjs
pnpm prettier --check \
tools/packaging/verify-linux-frame-copy-runtime.mjs \
tools/packaging/verify-linux-frame-copy-runtime.test.mjs \
tools/packaging/configure-linux-frame-copy-build.test.mjs \
.github/workflows/build-and-make.yaml \
docs/architecture/embedded-mpv-native.md \
tools/embedded-mpv/README.md \
docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md \
AGENTS.md CLAUDE.md
```
Expected: tests and formatting PASS.
- [ ] **Step 7: Commit Task 3**
```bash
git add tools/packaging/verify-linux-frame-copy-runtime.mjs \
tools/packaging/verify-linux-frame-copy-runtime.test.mjs \
.github/workflows/build-and-make.yaml \
tools/packaging/configure-linux-frame-copy-build.test.mjs \
docs/architecture/embedded-mpv-native.md \
tools/embedded-mpv/README.md \
docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md \
AGENTS.md CLAUDE.md
git commit -m "test(packaging): enforce direct Flatpak Zypak launch"
```
### Task 4: Verify the integrated fix
**Files:**
- Verify only; do not add unrelated changes.
- [ ] **Step 1: Run the complete packaging tests**
```bash
pnpm nx test packaging --skip-nx-cache
```
Expected: all tests PASS, including the new launcher tests.
- [ ] **Step 2: Run packaging lint**
```bash
pnpm nx lint packaging --skip-nx-cache
```
Expected: zero ESLint errors.
- [ ] **Step 3: Run repository formatting checks for changed files**
```bash
pnpm prettier --check \
tools/packaging/linux-launcher-layout.cjs \
tools/packaging/linux-after-pack.cjs \
tools/packaging/linux-after-pack.test.mjs \
tools/packaging/electron-after-pack.cjs \
tools/packaging/embedded-mpv-packaging.cjs \
tools/packaging/embedded-mpv-arch.test.mjs \
tools/packaging/verify-electron-package-layout.mjs \
tools/packaging/electron-package-identity.test.mjs \
tools/packaging/verify-linux-frame-copy-runtime.mjs \
tools/packaging/verify-linux-frame-copy-runtime.test.mjs \
tools/packaging/configure-linux-frame-copy-build.test.mjs \
tools/packaging/project.json \
.github/workflows/build-and-make.yaml \
docs/architecture/embedded-mpv-native.md \
tools/embedded-mpv/README.md \
docs/superpowers/specs/2026-07-17-linux-embedded-mpv-frame-copy-packaging-design.md \
docs/superpowers/specs/2026-07-18-flatpak-zypak-launcher-design.md \
docs/superpowers/plans/2026-07-18-flatpak-zypak-launcher.md \
AGENTS.md CLAUDE.md
```
Expected: all changed files use repository formatting.
- [ ] **Step 4: Inspect the final diff**
```bash
git diff 8fdac824..HEAD --check
git status --short
```
Expected: no whitespace errors and only scoped launcher, validator, CI, test,
plan, and documentation changes.
@@ -55,6 +55,11 @@ that the selected target set matches the runtime mode. It must fail closed if
an official x64 package is requested with an absent, incomplete, or ambiguous
profile.
Flatpak is an isolated packaging pass and keeps `iptvnator` as the real
Electron ELF so Electron Builder's `electron-wrapper` passes it directly to
Zypak. Other Linux targets retain the conditional `iptvnator` wrapper and
`iptvnator.bin`. Mixed Flatpak/non-Flatpak target sets fail before mutation.
System package dependencies are:
- DEB: `libmpv2`, `libegl1`, `libgl1`, `libgbm1`
@@ -131,8 +136,9 @@ the complete non-system dependency closure. ELF dependencies inside that
closure and the helper use only SONAMEs plus `$ORIGIN`-relative RPATH/RUNPATH;
they may not retain build-prefix paths.
`embedded_mpv.node`, the Electron executable (`iptvnator.bin`), and Electron's
shipped libraries must not have a direct `DT_NEEDED` entry for libmpv.
`embedded_mpv.node`, the Electron executable (`iptvnator` for Flatpak;
`iptvnator.bin` for other Linux targets), and Electron's shipped libraries must
not have a direct `DT_NEEDED` entry for libmpv.
`iptvnator_mpv_helper` must have one. Process isolation is an invariant, not a
profile-specific choice. The source `electron-backend/native{,/**/*}` tree is
excluded from `app.asar`; `afterPack` is the sole owner of the normalized
@@ -0,0 +1,69 @@
# Flatpak Zypak Launcher Design
## Goal
Package Flatpak so Electron Builder's generated `electron-wrapper` passes the
real IPTVnator ELF executable directly to Zypak. Preserve the existing
conditional Linux sandbox wrapper for AppImage, DEB, RPM, Pacman, and Snap.
## Root Cause
The common Linux `afterPack` hook currently renames the Electron executable
from `iptvnator` to `iptvnator.bin` and writes a shell script at `iptvnator`.
Electron Builder's Flatpak launcher calls `zypak-wrapper iptvnator`, so Zypak
receives the shell script instead of an ELF executable. Zypak rejects that
layout; the shell script can then mask the failure by adding `--no-sandbox` on
hosts with restricted user namespaces.
## Launcher Contract
Introduce one packaging-owned launcher-layout helper:
- an isolated Flatpak target uses `iptvnator` as the Electron ELF and does not
apply the custom Linux sandbox wrapper;
- every other supported Linux target keeps the existing `iptvnator` shell
wrapper and `iptvnator.bin` Electron ELF;
- a target set containing Flatpak and any other target fails before filesystem
mutation because Electron Builder shares one unpacked application tree
across those targets;
- target matching is case-insensitive and uses Electron Builder's documented
`AfterPackContext.targets[].name` values, not output paths or environment
heuristics.
The launcher hook, pristine-layout validation, and extracted-artifact
validation must all resolve the Electron ELF path through this contract.
## Validation
Regression coverage will prove the following:
1. The Linux launcher hook preserves the original executable bytes and creates
no `.bin` file for an isolated Flatpak target.
2. The hook retains the existing wrapper layout for a non-Flatpak Linux target.
3. A mixed Flatpak/non-Flatpak target set fails before renaming the executable.
4. Pristine Flatpak validation inspects `iptvnator`, while other profiles
inspect `iptvnator.bin`.
5. Extracted Flatpak verification reads architecture and checks process
isolation from `iptvnator`.
6. CI asserts that the installed Flatpak target is an ELF, rejects a sibling
`.bin` layout, and fails on the known Zypak wrapper warnings.
The existing application-level `--embedded-mpv-runtime-probe` remains the
sandboxed launch check. Once the custom wrapper is absent, it can no longer
silently add `--no-sandbox`; reaching the Electron main-process probe therefore
also verifies the corrected Zypak entry path.
## Documentation
Update the canonical Linux Embedded MPV packaging documentation and the living
`AGENTS.md`/`CLAUDE.md` summaries to state the launcher split explicitly.
Correct the earlier Linux frame-copy design document's claim that every Linux
Electron executable is named `iptvnator.bin`.
## Non-Goals
- Changing the Chromium GPU/Video.js behavior reported in issue #1203.
- Removing the conditional sandbox wrapper from non-Flatpak Linux packages.
- Adding `--no-sandbox`, changing `chrome-sandbox` permissions, or bypassing
Zypak.
- Changing the bundled Embedded MPV runtime or Flatpak permissions.