fix(packaging): enforce direct Flatpak Zypak launch

This commit is contained in:
4gray committed 2026-07-18 20:18:20 +02:00
1 parent fc76dd1ad2
commit 0962765793
9 files changed
+206 -27

No files matched your search

+31 -7
View File
@@ -1104,14 +1104,38 @@ jobs:
LAUNCHER_PATH="$(readlink -f /app/bin/iptvnator)"
test -f "${LAUNCHER_PATH}"
test -f "${LAUNCHER_PATH}.bin"
grep -q '\''readlink -f "$SCRIPT_PATH"'\'' "${LAUNCHER_PATH}"
grep -q '\''exec "$SCRIPT_DIR/iptvnator.bin"'\'' "${LAUNCHER_PATH}"
test -x "${LAUNCHER_PATH}"
if [ -e "${LAUNCHER_PATH}.bin" ] || [ -L "${LAUNCHER_PATH}.bin" ]; then
echo "::error::Flatpak must not contain ${LAUNCHER_PATH}.bin"
exit 1
fi
ELF_MAGIC="$(od -An -tx1 -N4 "${LAUNCHER_PATH}" | tr -d "[:space:]")"
test "${ELF_MAGIC}" = "7f454c46"
'
xvfb-run -a dbus-run-session -- flatpak run \
--env=LIBGL_ALWAYS_SOFTWARE=1 \
com.fourgray.iptvnator \
--embedded-mpv-runtime-probe
set +e
PROBE_OUTPUT="$(
xvfb-run -a dbus-run-session -- flatpak run \
--env=LIBGL_ALWAYS_SOFTWARE=1 \
com.fourgray.iptvnator \
--embedded-mpv-runtime-probe 2>&1
)"
PROBE_STATUS=$?
set -e
PROBE_OUTPUT_LIMIT=16384
printf '%s\n' "${PROBE_OUTPUT:0:PROBE_OUTPUT_LIMIT}"
if [ "${#PROBE_OUTPUT}" -gt "${PROBE_OUTPUT_LIMIT}" ]; then
echo "::warning::Flatpak runtime probe output was truncated to ${PROBE_OUTPUT_LIMIT} characters."
fi
if [[ "${PROBE_OUTPUT}" == *"not an ELF file"* ]] ||
[[ "${PROBE_OUTPUT}" == *"Zypak needs to be called directly"* ]]; then
echo "::error::Flatpak launched a wrapper instead of the Electron ELF."
exit 1
fi
if [ "${PROBE_STATUS}" -ne 0 ]; then
echo "::error::Flatpak application runtime probe failed with status ${PROBE_STATUS}."
exit "${PROBE_STATUS}"
fi
- name: Upload artifacts (macOS)
if: matrix.os == 'macos'
+4
View File
@@ -229,6 +229,10 @@ Key files:
Pacman=`mpv,libglvnd,mesa`
- `portable`: AppImage/Snap with the pinned LGPL-compatible closure
- `flatpak`: Flatpak with the same pinned closure
- 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.
- The DEB system-runtime contract is Ubuntu 24.04+ (`libmpv2`). Ubuntu 22.04
provides `libmpv1`, so use the x64 AppImage on Jammy instead of weakening the
package dependency or advertising frame-copy without a compatible runtime.
+6 -2
View File
@@ -639,8 +639,12 @@ engine` (restart required) or
Official x64 packages use three separate profiles:
DEB/RPM/Pacman depend on system libmpv plus the helper's direct
EGL/GL/GBM interfaces, AppImage/Snap bundle the pinned LGPL closure, and
Flatpak bundles the same closure. Exact system dependencies are
DEB=`libmpv2,libegl1,libgl1,libgbm1`,
Flatpak bundles the same closure. 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. Exact system
dependencies are DEB=`libmpv2,libegl1,libgl1,libgbm1`,
RPM=`mpv-libs,libglvnd-egl,libglvnd-glx,mesa-libgbm`, and
Pacman=`mpv,libglvnd,mesa`. The DEB contract is verified on Ubuntu 24.04+;
Ubuntu 22.04 users need the x64 AppImage because Jammy provides `libmpv1`.
+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
@@ -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
+6 -3
View File
@@ -190,9 +190,12 @@ controlled status `1`, then reconnects the provider and requires a successful
diagnostic. This keeps the canonical layouts and missing-provider fallback in
the same regression contract.
Profiles cannot share one Electron Builder pass because its targets reuse the
same unpacked application directory. A missing or unsupported profile, or a
target from another profile, fails packaging.
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. A
missing or unsupported profile, or a target from another profile, fails
packaging.
Linux frame-copy release artifacts are x64-only. Non-x64 packages are always
marker-only even if environment variables point at the x64 staged runtime.
@@ -555,6 +555,52 @@ test('Flatpak application runtime probe runs under an isolated D-Bus session', (
);
});
test('Flatpak CI verifies the direct Zypak ELF and preserves probe status', () => {
const flatpakVerificationStep = workflowStep(
'Verify Flatpak payload, launcher, and sandboxed runtime'
);
assert.match(
flatpakVerificationStep,
/LAUNCHER_PATH="\$\(readlink -f \/app\/bin\/iptvnator\)"/
);
assert.match(
flatpakVerificationStep,
/test -f "\$\{LAUNCHER_PATH\}"[\s\S]*test -x "\$\{LAUNCHER_PATH\}"/
);
assert.match(
flatpakVerificationStep,
/if \[ -e "\$\{LAUNCHER_PATH\}\.bin" \] \|\| \[ -L "\$\{LAUNCHER_PATH\}\.bin" \]; then/
);
assert.match(
flatpakVerificationStep,
/ELF_MAGIC="\$\(od -An -tx1 -N4 "\$\{LAUNCHER_PATH\}" \| tr -d "\[:space:\]"\)"[\s\S]*test "\$\{ELF_MAGIC\}" = "7f454c46"/
);
assert.doesNotMatch(
flatpakVerificationStep,
/grep -q .*readlink -f "\$SCRIPT_PATH"/
);
assert.doesNotMatch(
flatpakVerificationStep,
/grep -q .*exec "\$SCRIPT_DIR\/iptvnator\.bin"/
);
assert.match(
flatpakVerificationStep,
/set \+e[\s\S]*PROBE_OUTPUT="\$\([\s\S]*xvfb-run -a dbus-run-session -- flatpak run[\s\S]*--embedded-mpv-runtime-probe 2>&1[\s\S]*\)"[\s\S]*PROBE_STATUS=\$\?[\s\S]*set -e/
);
assert.match(
flatpakVerificationStep,
/PROBE_OUTPUT_LIMIT=16384[\s\S]*"\$\{PROBE_OUTPUT:0:PROBE_OUTPUT_LIMIT\}"/
);
assert.match(flatpakVerificationStep, /not an ELF file/);
assert.match(flatpakVerificationStep, /Zypak needs to be called directly/);
assert.match(
flatpakVerificationStep,
/if \[ "\$\{PROBE_STATUS\}" -ne 0 \]; then[\s\S]*exit "\$\{PROBE_STATUS\}"/
);
});
test('foreign DEB CI explicitly selects both marker-only ARM architectures', () => {
const foreignDebStep = workflowStep(
'Make marker-only foreign-architecture DEB packages'
@@ -24,6 +24,7 @@ const {
LINUX_SYSTEM_PACKAGE_DEPENDENCIES,
resolveLinuxFrameCopyProfile,
} = require('./linux-frame-copy-profile.cjs');
const { resolveLinuxLauncherLayout } = require('./linux-launcher-layout.cjs');
const {
RUNTIME_PROBE_MAX_BUFFER_BYTES,
RUNTIME_PROBE_TIMEOUT_MS,
@@ -1350,9 +1351,14 @@ function dependencyFileName(dependencyName) {
return String(dependencyName).replaceAll('\\', '/').split('/').at(-1) ?? '';
}
function resolveElectronBinaryPath(resourceDir, artifactFormat) {
const layout = resolveLinuxLauncherLayout([artifactFormat]);
return path.join(path.dirname(resourceDir), layout.electronBinaryName);
}
function validateElectronIsolation(resourceDir, artifactFormat, elfInspector) {
const errors = [];
const electronPath = path.join(path.dirname(resourceDir), 'iptvnator.bin');
const electronPath = resolveElectronBinaryPath(resourceDir, artifactFormat);
const binaries = [
{ label: 'Electron binary', binaryPath: electronPath },
...listElectronShippedLinuxLibraries(resourceDir, {
@@ -1557,7 +1563,7 @@ export function verifyExtractedLinuxFrameCopyRuntime({
)
);
const electronPath = path.join(path.dirname(resourceDir), 'iptvnator.bin');
const electronPath = resolveElectronBinaryPath(resourceDir, artifactFormat);
let packageArch;
try {
packageArch = readElfArchitecture(electronPath);
@@ -1709,10 +1715,7 @@ export function verifyLinuxFrameCopyArtifact({
].join('\n')
);
}
const electronPath = path.join(
path.dirname(resourceDir),
'iptvnator.bin'
);
const electronPath = resolveElectronBinaryPath(resourceDir, format);
return {
artifactPath: resolvedArtifactPath,
format,
@@ -219,7 +219,10 @@ function elfHeader(architecture) {
return image;
}
function createSystemPayload({ architecture = 'x64' } = {}) {
function createSystemPayload({
architecture = 'x64',
electronBinaryName = 'iptvnator.bin',
} = {}) {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-verifier-layout-')
);
@@ -233,7 +236,7 @@ function createSystemPayload({ architecture = 'x64' } = {}) {
);
fs.mkdirSync(nativeDir, { recursive: true });
fs.writeFileSync(
path.join(appDir, 'iptvnator.bin'),
path.join(appDir, electronBinaryName),
elfHeader(architecture)
);
@@ -1574,6 +1577,64 @@ test('artifact verification enforces Snap metadata for x64 and ARM payloads', ()
}
});
test('verifies an outer Flatpak artifact with an unwrapped Electron ELF', () => {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-verifier-flatpak-artifact-')
);
const artifactPath = path.join(root, 'IPTVnator.flatpak');
fs.writeFileSync(artifactPath, 'flatpak fixture');
try {
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',
}
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('bundled probes remove ambient loader paths and use only packaged libraries', () => {
assert.deepEqual(
createRuntimeProbeEnvironment({
@@ -1824,6 +1885,32 @@ test('requires marker-only foreign packages, scans Electron, and never probes',
}
});
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 });
}
});
test('always removes its temporary extraction root after a verifier failure', () => {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-verifier-cleanup-parent-')