fix(packaging): validate Linux frame-copy runtimes

This commit is contained in:
4gray committed 2026-07-18 10:07:01 +02:00
1 parent 482630dd2a
commit 541d47ac96
15 files changed
+480 -145

No files matched your search

+29 -10
View File
@@ -825,16 +825,32 @@ jobs:
run: |
set -euo pipefail
cp "${RUNNER_TEMP}/electron-builder.base.json" electron-builder.json
node tools/packaging/configure-linux-frame-copy-build.mjs --foreign-deb
for foreign_arch in armv7l arm64; do
rm -rf dist/executables-linux-foreign
cp "${RUNNER_TEMP}/electron-builder.base.json" electron-builder.json
node tools/packaging/configure-linux-frame-copy-build.mjs \
--foreign-deb \
--foreign-arch "${foreign_arch}"
pnpm nx run electron-backend:make \
--arch="${foreign_arch}" \
--outputPath=dist/executables-linux-foreign \
--publishPolicy=never
mapfile -t foreign_debs < <(
find dist/executables-linux-foreign -maxdepth 1 -type f -name '*.deb' -print
)
test "${#foreign_debs[@]}" -eq 1
case "${foreign_arch}" in
armv7l) expected_deb_arch=armhf ;;
arm64) expected_deb_arch=arm64 ;;
*)
echo "::error::Unexpected foreign DEB build architecture ${foreign_arch}"
exit 1
;;
esac
actual_deb_arch="$(dpkg-deb --field "${foreign_debs[0]}" Architecture)"
test "${actual_deb_arch}" = "${expected_deb_arch}"
mv "${foreign_debs[0]}" dist/executables/
done
find dist/executables-linux-foreign -maxdepth 1 -type f -name '*.deb' \
-exec mv '{}' dist/executables/ ';'
- name: Verify DEB payloads and x64 system runtime
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
@@ -856,7 +872,8 @@ jobs:
bash -euo pipefail -c '
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
binutils libegl1 libgl1-mesa-dri libmpv2 nodejs squashfs-tools xauth xvfb
binutils libegl1 libgbm1 libgl1-mesa-dri libmpv2 libopengl0 \
nodejs squashfs-tools xauth xvfb
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
--artifact /artifact.deb --profile system
@@ -889,8 +906,9 @@ jobs:
fedora:latest \
bash -euo pipefail -c '
dnf install -y \
binutils bsdtar mesa-dri-drivers mpv-libs nodejs rpm \
xorg-x11-server-Xvfb xorg-x11-xauth
binutils bsdtar libglvnd-egl libglvnd-opengl mesa-dri-drivers \
mesa-libgbm mpv-libs nodejs rpm xorg-x11-server-Xvfb \
xorg-x11-xauth
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
--artifact /artifact.rpm --profile system
@@ -911,7 +929,8 @@ jobs:
archlinux:latest \
bash -euo pipefail -c '
pacman -Syu --noconfirm \
binutils libarchive mesa mpv nodejs xorg-server-xvfb xorg-xauth
binutils libarchive libglvnd mesa mpv nodejs xorg-server-xvfb \
xorg-xauth
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
--artifact /artifact.pacman --profile system
@@ -947,9 +966,9 @@ jobs:
verification="$(
xvfb-run -a env LIBGL_ALWAYS_SOFTWARE=1 \
node tools/packaging/verify-linux-frame-copy-runtime.mjs \
--artifact "${artifact}" --profile portable
--artifact "${artifact}" --profile portable \
2>&1 | tee /dev/stderr
)"
printf '%s\n' "${verification}"
if printf '%s\n' "${verification}" | grep -Fq 'Verified snap x64 Linux'; then
sudo snap install --dangerous "${artifact}"
installed_x64=true
+3 -1
View File
@@ -224,7 +224,9 @@ Key files:
overrides.
- Packaging runs three isolated profiles:
- `system`: DEB/RPM/Pacman, no private `native/lib`, with package
dependencies `libmpv2`/`mpv-libs`/`mpv`
dependencies DEB=`libmpv2,libegl1,libopengl0,libgbm1`,
RPM=`mpv-libs,libglvnd-egl,libglvnd-opengl,mesa-libgbm`, and
Pacman=`mpv,libglvnd,mesa`
- `portable`: AppImage/Snap with the pinned LGPL-compatible closure
- `flatpak`: Flatpak with the same pinned closure
- The DEB system-runtime contract is Ubuntu 24.04+ (`libmpv2`). Ubuntu 22.04
+12 -9
View File
@@ -628,15 +628,18 @@ engine` (restart required) or
UI; native-view retains the legacy dock. On Linux, only
`iptvnator_mpv_helper` may link libmpv; Electron, its shipped libraries, the
addon, and frame reader must not. Official x64 packages use three separate
profiles: DEB/RPM/Pacman depend on system libmpv, AppImage/Snap bundle the
pinned LGPL closure, and Flatpak bundles the same closure. The DEB contract
requires `libmpv2` and is verified on Ubuntu 24.04+; Ubuntu 22.04 users need
the x64 AppImage because Jammy provides `libmpv1`. ARM packages are
marker-only. Stored or explicit opt-ins cannot bypass the fail-closed
packaged manifest/file/hash gate and bounded `--runtime-probe`; any failure
keeps the sandbox enabled, records a stable reason, and falls back to
native-view without crashing. Snap uses an exact private `shared-memory`
plug; probe and playback share a sanitized loader environment in which
profiles: DEB/RPM/Pacman depend on system libmpv plus the helper's direct
EGL/OpenGL/GBM interfaces, AppImage/Snap bundle the pinned LGPL closure, and
Flatpak bundles the same closure. Exact system dependencies are
DEB=`libmpv2,libegl1,libopengl0,libgbm1`,
RPM=`mpv-libs,libglvnd-egl,libglvnd-opengl,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`.
ARM packages are marker-only. Stored or explicit opt-ins cannot bypass the
fail-closed packaged manifest/file/hash gate and bounded `--runtime-probe`;
any failure keeps the sandbox enabled, records a stable reason, and falls
back to native-view without crashing. Snap uses an exact private
`shared-memory` plug; probe and playback share a sanitized loader environment in which
ambient audit, preload, and library paths are removed and the validated
private closure and trusted Snap GL roots have explicit precedence. Bundled
Linux packages carry hash-validated
@@ -257,11 +257,16 @@ export const DEVELOPMENT_MANIFEST_FIELDS = [
'sourceRuntimeValidated',
] as const;
export const SYSTEM_PACKAGE_DEPENDENCIES = {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
};
export const SYSTEM_PACKAGE_DEPENDENCIES = Object.freeze({
deb: Object.freeze(['libmpv2', 'libegl1', 'libopengl0', 'libgbm1']),
rpm: Object.freeze([
'mpv-libs',
'libglvnd-egl',
'libglvnd-opengl',
'mesa-libgbm',
]),
pacman: Object.freeze(['mpv', 'libglvnd', 'mesa']),
});
export const PROFILE_CONTRACTS = {
system: {
@@ -160,9 +160,14 @@ export function createFixture(
packageDependencies: bundled
? {}
: {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
deb: ['libmpv2', 'libegl1', 'libopengl0', 'libgbm1'],
rpm: [
'mpv-libs',
'libglvnd-egl',
'libglvnd-opengl',
'mesa-libgbm',
],
pacman: ['mpv', 'libglvnd', 'mesa'],
},
runtimeFiles,
runtimeTotalBytes: runtimeFiles.reduce(
+25 -16
View File
@@ -77,16 +77,24 @@ or `ELECTRON_OZONE_PLATFORM_HINT` is never overridden.
Linux packages are built in separate passes because Electron Builder reuses
one unpacked application layout per pass:
| Profile | Formats | Frame-copy libmpv strategy |
| ---------- | ---------------- | --------------------------------------------------------------------------------------------- |
| `system` | DEB, RPM, Pacman | System `libmpv.so.2`; package dependencies are `libmpv2`, `mpv-libs`, and `mpv`, respectively |
| `portable` | AppImage, Snap | Bundled pinned LGPL-compatible runtime under `native/lib` |
| `flatpak` | Flatpak | The same bundled pinned LGPL-compatible runtime under `native/lib` |
| Profile | Formats | Frame-copy runtime strategy |
| ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------ |
| `system` | DEB, RPM, Pacman | System `libmpv.so.2` plus the helper's direct EGL/OpenGL/GBM interfaces; exact dependencies are listed below |
| `portable` | AppImage, Snap | Bundled pinned LGPL-compatible runtime under `native/lib` |
| `flatpak` | Flatpak | The same bundled pinned LGPL-compatible runtime under `native/lib` |
System package dependencies are fail-closed and format-specific:
- DEB: `libmpv2`, `libegl1`, `libopengl0`, `libgbm1`
- RPM: `mpv-libs`, `libglvnd-egl`, `libglvnd-opengl`, `mesa-libgbm`
- Pacman: `mpv`, `libglvnd`, `mesa`
The DEB contract deliberately names `libmpv2`, not a loose `libmpv`
alternative. Release CI verifies that contract on Ubuntu 24.04 (Noble).
Ubuntu 22.04 (Jammy) provides `libmpv1`, so its DEB cannot enable this
system-runtime frame-copy path; use the x64 AppImage there instead.
alternative, and explicitly names the GLVND OpenGL interface because
`libmpv2` does not pull it in for the helper. Release CI verifies that contract
on Ubuntu 24.04 (Noble). Ubuntu 22.04 (Jammy) provides `libmpv1`, so its DEB
cannot enable this system-runtime frame-copy path; use the x64 AppImage there
instead.
Every x64 layout contains the addon, frame reader, helper, and a normalized
`embedded-mpv-runtime.json`. The Electron executable, Electron libraries,
@@ -580,10 +588,10 @@ Linux release profiles:
- `IPTVNATOR_LINUX_FRAME_COPY_PROFILE=system` builds DEB, RPM, and Pacman.
`afterPack` removes the private `lib` directory, writes a
`system-libmpv-frame-copy` manifest, and package metadata requires
`libmpv2`, `mpv-libs`, or `mpv`. The DEB path is verified on Ubuntu 24.04+;
Ubuntu 22.04 users need the x64 AppImage because Jammy only provides
`libmpv1`.
`system-libmpv-frame-copy` manifest, and package metadata requires the exact
libmpv plus EGL/OpenGL/GBM package set listed above. The DEB path is verified
on Ubuntu 24.04+; Ubuntu 22.04 users need the x64 AppImage because Jammy only
provides `libmpv1`.
- `IPTVNATOR_LINUX_FRAME_COPY_PROFILE=portable` builds AppImage and Snap with
the pinned source-built closure and a `bundled-lgpl-frame-copy` manifest.
- `IPTVNATOR_LINUX_FRAME_COPY_PROFILE=flatpak` builds Flatpak with the same
@@ -618,10 +626,11 @@ Release packaging must:
Users on macOS and Windows do not need the MPV GUI application for this
architecture. Linux native-view still requires an `mpv` executable. Linux
frame-copy system packages need the declared libmpv package, while AppImage,
Snap, and Flatpak carry their own runtime closure. If frame-copy prerequisites
are missing, x64 falls back to native-view; if all Embedded MPV prerequisites
are unavailable, the existing inline/external players remain available.
frame-copy system packages need their declared libmpv and EGL/OpenGL/GBM
packages, while AppImage, Snap, and Flatpak carry their own runtime closure.
If frame-copy prerequisites are missing, x64 falls back to native-view; if all
Embedded MPV prerequisites are unavailable, the existing inline/external
players remain available.
## Runtime Staging
@@ -57,14 +57,15 @@ profile.
System package dependencies are:
- DEB: `libmpv2`
- RPM: `mpv-libs`
- Pacman: `mpv`
- DEB: `libmpv2`, `libegl1`, `libopengl0`, `libgbm1`
- RPM: `mpv-libs`, `libglvnd-egl`, `libglvnd-opengl`, `mesa-libgbm`
- Pacman: `mpv`, `libglvnd`, `mesa`
These names match the current Debian, Fedora, and Arch package databases.
System packages do not copy libmpv into IPTVnator. The helper keeps an
`$ORIGIN/lib` RPATH first for a consistent binary, but naturally resolves the
system SONAME when the private directory is absent.
These names match the current Debian, Fedora, and Arch package databases and
cover every direct helper interface: libmpv, EGL, OpenGL, and GBM. System
packages do not copy libmpv into IPTVnator. The helper keeps an `$ORIGIN/lib`
RUNPATH first for a consistent binary, but naturally resolves the system SONAME
when the private directory is absent.
Portable and sandboxed packages use a source-built runtime rather than copying
the Ubuntu runner's mpv package. The runtime build is checksum/version pinned,
+18 -8
View File
@@ -110,15 +110,25 @@ Before publication, the Linux builder verifies:
Set one exact `IPTVNATOR_LINUX_FRAME_COPY_PROFILE` per packaging pass:
| Profile | Formats | Runtime handling |
| ---------- | ---------------- | ------------------------------------------------------------ |
| `system` | DEB, RPM, Pacman | Remove `native/lib`; require `libmpv2`, `mpv-libs`, or `mpv` |
| `portable` | AppImage, Snap | Retain the pinned LGPL closure under `native/lib` |
| `flatpak` | Flatpak | Retain the same pinned LGPL closure under `native/lib` |
| Profile | Formats | Runtime handling |
| ---------- | ---------------- | ---------------------------------------------------------------------------- |
| `system` | DEB, RPM, Pacman | Remove `native/lib`; require the format-specific system runtime listed below |
| `portable` | AppImage, Snap | Retain the pinned LGPL closure under `native/lib` |
| `flatpak` | Flatpak | Retain the same pinned LGPL closure under `native/lib` |
The DEB metadata requires `libmpv2` and is release-tested on Ubuntu 24.04
(Noble). Ubuntu 22.04 (Jammy) only provides `libmpv1`; use the x64 AppImage on
that distribution rather than relaxing the runtime contract.
The system helper directly links libmpv, EGL, OpenGL, and GBM. Package metadata
therefore declares the full interface set:
- DEB: `libmpv2`, `libegl1`, `libopengl0`, `libgbm1`
- RPM: `mpv-libs`, `libglvnd-egl`, `libglvnd-opengl`, `mesa-libgbm`
- Pacman: `mpv`, `libglvnd`, `mesa`
The DEB metadata is release-tested on Ubuntu 24.04 (Noble). Ubuntu 22.04
(Jammy) only provides `libmpv1`; use the x64 AppImage on that distribution
rather than relaxing the runtime contract. CI explicitly installs the distro
Mesa software renderer for headless smoke. IPTVnator does not add DRI-driver
packages as direct dependencies; any transitive graphics-driver stack remains
under the distro's dependency policy.
The strict Snap retains Electron Builder's default plugs and adds an
auto-connected private `shared-memory` plug. This supplies a snap-specific
@@ -42,26 +42,29 @@ function fpmDependencyName(option) {
}
function configureSystemDependencies(config) {
const frameCopyDependencies = new Set(
Object.values(LINUX_SYSTEM_PACKAGE_DEPENDENCIES)
);
for (const [format, dependency] of Object.entries(
for (const [format, dependencies] of Object.entries(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES
)) {
const frameCopyDependencies = new Set(dependencies);
const formatConfig = { ...(config[format] ?? {}) };
const otherFpmOptions = (formatConfig.fpm ?? []).filter(
(option) => !frameCopyDependencies.has(fpmDependencyName(option))
);
formatConfig.fpm = [...otherFpmOptions, `--depends=${dependency}`];
formatConfig.fpm = [
...otherFpmOptions,
...dependencies.map((dependency) => `--depends=${dependency}`),
];
config[format] = formatConfig;
}
}
function removeForeignFrameCopyDependency(config, format) {
const dependency = LINUX_SYSTEM_PACKAGE_DEPENDENCIES[format];
const dependencies = new Set(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES[format] ?? []
);
const formatConfig = { ...(config[format] ?? {}) };
const retainedFpmOptions = (formatConfig.fpm ?? []).filter(
(option) => fpmDependencyName(option) !== dependency
(option) => !dependencies.has(fpmDependencyName(option))
);
if (retainedFpmOptions.length > 0) {
formatConfig.fpm = retainedFpmOptions;
@@ -73,8 +76,9 @@ function removeForeignFrameCopyDependency(config, format) {
export function configureLinuxFrameCopyBuild(
electronBuilderConfig,
{ profileName, foreignDeb = false } = {}
{ profileName, foreignDeb = false, foreignArch } = {}
) {
const hasForeignArch = foreignArch !== undefined;
if (
!electronBuilderConfig ||
typeof electronBuilderConfig !== 'object' ||
@@ -89,6 +93,11 @@ export function configureLinuxFrameCopyBuild(
'The marker-only foreign DEB pass must not select a frame-copy profile.'
);
}
if (hasForeignArch && !foreignDeb) {
throw new Error(
'A marker-only foreign architecture requires --foreign-deb.'
);
}
const configured = cloneJson(electronBuilderConfig);
const targets = configured.linux?.target;
if (!Array.isArray(targets)) {
@@ -116,7 +125,20 @@ export function configureLinuxFrameCopyBuild(
'Electron Builder DEB target has no foreign architectures.'
);
}
configuredDebTarget.arch = foreignArches;
if (
hasForeignArch &&
(typeof foreignArch !== 'string' ||
!foreignArches.includes(foreignArch))
) {
throw new Error(
`Unsupported marker-only foreign DEB architecture "${foreignArch}". Expected one of: ${foreignArches.join(
', '
)}.`
);
}
configuredDebTarget.arch = hasForeignArch
? [foreignArch]
: foreignArches;
configured.linux.target = [configuredDebTarget];
configured.directories = {
...(configured.directories ?? {}),
@@ -179,14 +201,31 @@ function parseArguments(argv) {
let configPath = 'electron-builder.json';
let profileName;
let foreignDeb = false;
let foreignArch;
const nextValue = (flag, index) => {
const value = normalized[index + 1];
if (
typeof value !== 'string' ||
value.trim() === '' ||
value.startsWith('--')
) {
throw new Error(`${flag} requires a value.`);
}
return value;
};
for (let index = 0; index < normalized.length; index += 1) {
const argument = normalized[index];
if (argument === '--config') {
configPath = normalized[++index];
configPath = nextValue(argument, index);
index += 1;
} else if (argument === '--profile') {
profileName = normalized[++index];
profileName = nextValue(argument, index);
index += 1;
} else if (argument === '--foreign-deb') {
foreignDeb = true;
} else if (argument === '--foreign-arch') {
foreignArch = nextValue(argument, index);
index += 1;
} else {
throw new Error(`Unsupported configurator argument: ${argument}`);
}
@@ -198,6 +237,7 @@ function parseArguments(argv) {
configPath: path.resolve(configPath),
profileName,
foreignDeb,
foreignArch,
};
}
@@ -1,5 +1,7 @@
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { parse as parseYaml } from 'yaml';
@@ -48,9 +50,23 @@ test('configures an x64-only system pass with exact package dependencies', () =>
{ target: 'rpm', arch: ['x64'] },
{ target: 'pacman', arch: ['x64'] },
]);
assert.deepEqual(configured.deb.fpm, ['--depends=libmpv2']);
assert.deepEqual(configured.rpm.fpm, ['--depends=mpv-libs']);
assert.deepEqual(configured.pacman.fpm, ['--depends=mpv']);
assert.deepEqual(configured.deb.fpm, [
'--depends=libmpv2',
'--depends=libegl1',
'--depends=libopengl0',
'--depends=libgbm1',
]);
assert.deepEqual(configured.rpm.fpm, [
'--depends=mpv-libs',
'--depends=libglvnd-egl',
'--depends=libglvnd-opengl',
'--depends=mesa-libgbm',
]);
assert.deepEqual(configured.pacman.fpm, [
'--depends=mpv',
'--depends=libglvnd',
'--depends=mesa',
]);
});
test('configures portable and flatpak passes without mixing targets', () => {
@@ -99,6 +115,76 @@ test('configures a separate marker-only foreign DEB pass without libmpv metadata
);
});
test('configures exactly one requested marker-only foreign DEB architecture', () => {
for (const foreignArch of ['armv7l', 'arm64']) {
const configured = configureLinuxFrameCopyBuild(electronBuilderConfig, {
foreignDeb: true,
foreignArch,
});
assert.deepEqual(configuredTargets(configured), [
{ target: 'deb', arch: [foreignArch] },
]);
}
});
test('CLI writes an exact marker-only foreign DEB architecture', (t) => {
const temporaryDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-linux-build-config-')
);
t.after(() =>
fs.rmSync(temporaryDirectory, { recursive: true, force: true })
);
const configPath = path.join(temporaryDirectory, 'electron-builder.json');
fs.writeFileSync(
configPath,
`${JSON.stringify(electronBuilderConfig, null, 4)}\n`
);
const result = spawnSync(
process.execPath,
[
path.join(
workspaceRoot,
'tools',
'packaging',
'configure-linux-frame-copy-build.mjs'
),
'--config',
configPath,
'--foreign-deb',
'--foreign-arch',
'arm64',
],
{ encoding: 'utf8' }
);
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(
configuredTargets(JSON.parse(fs.readFileSync(configPath, 'utf8'))),
[{ target: 'deb', arch: ['arm64'] }]
);
const missingValue = spawnSync(
process.execPath,
[
path.join(
workspaceRoot,
'tools',
'packaging',
'configure-linux-frame-copy-build.mjs'
),
'--config',
configPath,
'--foreign-deb',
'--foreign-arch',
],
{ encoding: 'utf8' }
);
assert.notEqual(missingValue.status, 0);
assert.match(missingValue.stderr, /--foreign-arch requires a value/);
});
test('does not mutate the shared electron-builder configuration', () => {
const before = JSON.stringify(electronBuilderConfig);
configureLinuxFrameCopyBuild(electronBuilderConfig, {
@@ -123,6 +209,31 @@ test('rejects an unknown profile and conflicting foreign/profile modes', () => {
}),
/must not select a frame-copy profile/
);
assert.throws(
() =>
configureLinuxFrameCopyBuild(electronBuilderConfig, {
foreignArch: 'arm64',
}),
/foreign architecture requires --foreign-deb/
);
assert.throws(
() =>
configureLinuxFrameCopyBuild(electronBuilderConfig, {
foreignDeb: true,
foreignArch: 'x64',
}),
/Unsupported marker-only foreign DEB architecture/
);
for (const foreignArch of ['', 64]) {
assert.throws(
() =>
configureLinuxFrameCopyBuild(electronBuilderConfig, {
foreignDeb: true,
foreignArch,
}),
/Unsupported marker-only foreign DEB architecture/
);
}
});
test('rejects duplicate profile targets even when target count looks complete', () => {
@@ -142,20 +253,32 @@ test('rejects duplicate profile targets even when target count looks complete',
test('preserves unrelated fpm dependencies and normalizes only frame-copy dependencies', () => {
const customized = structuredClone(electronBuilderConfig);
customized.deb = {
fpm: ['--depends=unrelated-runtime', '--depends=libmpv2 >= 2'],
fpm: [
'--depends=unrelated-runtime',
'--depends=mesa',
'--depends=libmpv2 >= 2',
'--depends=libopengl0',
],
};
const system = configureLinuxFrameCopyBuild(customized, {
profileName: 'system',
});
assert.deepEqual(system.deb.fpm, [
'--depends=unrelated-runtime',
'--depends=mesa',
'--depends=libmpv2',
'--depends=libegl1',
'--depends=libopengl0',
'--depends=libgbm1',
]);
const foreign = configureLinuxFrameCopyBuild(customized, {
foreignDeb: true,
});
assert.deepEqual(foreign.deb.fpm, ['--depends=unrelated-runtime']);
assert.deepEqual(foreign.deb.fpm, [
'--depends=unrelated-runtime',
'--depends=mesa',
]);
});
test('Linux CI builds one cached source runtime and packages three isolated profiles', () => {
@@ -173,7 +296,7 @@ test('Linux CI builds one cached source runtime and packages three isolated prof
);
assert.match(
buildWorkflow,
/configure-linux-frame-copy-build\.mjs --foreign-deb/
/configure-linux-frame-copy-build\.mjs[\s\S]*--foreign-deb/
);
assert.match(
buildWorkflow,
@@ -368,12 +491,85 @@ test('foreign DEB CI explicitly selects both marker-only ARM architectures', ()
);
assert.match(foreignDebStep, /for foreign_arch in armv7l arm64; do/);
assert.match(
foreignDebStep,
/--foreign-deb\s+\\\s+--foreign-arch "\$\{foreign_arch\}"/
);
assert.match(foreignDebStep, /--arch="\$\{foreign_arch\}"/);
assert.match(
foreignDebStep,
/for foreign_arch[\s\S]*cp "\$\{RUNNER_TEMP\}\/electron-builder\.base\.json" electron-builder\.json[\s\S]*configure-linux-frame-copy-build\.mjs/
);
assert.match(foreignDebStep, /rm -rf dist\/executables-linux-foreign/);
assert.match(
foreignDebStep,
/mapfile -t foreign_debs < <\(\s*find dist\/executables-linux-foreign[\s\S]*-name '\*\.deb' -print\s*\)/
);
assert.match(foreignDebStep, /test "\$\{#foreign_debs\[@\]\}" -eq 1/);
assert.match(foreignDebStep, /armv7l\) expected_deb_arch=armhf/);
assert.match(foreignDebStep, /arm64\) expected_deb_arch=arm64/);
assert.match(
foreignDebStep,
/actual_deb_arch="\$\(dpkg-deb --field "\$\{foreign_debs\[0\]\}" Architecture\)"[\s\S]*test "\$\{actual_deb_arch\}" = "\$\{expected_deb_arch\}"/
);
assert.match(
foreignDebStep,
/mv "\$\{foreign_debs\[0\]\}" dist\/executables\//
);
assert.match(foreignDebStep, /IPTVNATOR_LINUX_FRAME_COPY_PROFILE: ''/);
assert.match(foreignDebStep, /IPTVNATOR_REQUIRE_EMBEDDED_MPV: '1'/);
assert.doesNotMatch(foreignDebStep, /IPTVNATOR_REQUIRE_EMBEDDED_MPV: '0'/);
});
test('system package smoke environments install every direct helper runtime dependency', () => {
const debStep = workflowStep('Verify DEB payloads and x64 system runtime');
for (const dependency of ['libmpv2', 'libegl1', 'libopengl0', 'libgbm1']) {
assert.match(debStep, new RegExp(`\\b${dependency}\\b`));
}
const rpmStep = workflowStep('Verify RPM payload and x64 system runtime');
for (const dependency of [
'mpv-libs',
'libglvnd-egl',
'libglvnd-opengl',
'mesa-libgbm',
]) {
assert.match(rpmStep, new RegExp(`\\b${dependency}\\b`));
}
const pacmanStep = workflowStep(
'Verify Pacman payload and x64 system runtime'
);
for (const dependency of ['mpv', 'libglvnd', 'mesa']) {
assert.match(pacmanStep, new RegExp(`\\b${dependency}\\b`));
}
});
test('Snap verifier preserves fail-closed status while exposing captured diagnostics', () => {
const snapStep = workflowStep(
'Verify Snap payloads and strict-confinement runtime'
);
assert.match(snapStep, /set -euo pipefail/);
assert.match(snapStep, /2>&1 \| tee \/dev\/stderr/);
assert.doesNotMatch(
snapStep,
/^\s+printf '%s\\n' "\$\{verification\}"\s*$/m
);
assert.ok(
snapStep.indexOf('verification="$(') <
snapStep.indexOf("grep -Fq 'Verified snap x64 Linux'")
);
assert.ok(
snapStep.indexOf("grep -Fq 'Verified snap x64 Linux'") <
snapStep.indexOf('sudo snap install --dangerous')
);
assert.ok(
snapStep.indexOf('sudo snap install --dangerous') <
snapStep.indexOf('installed_x64=true')
);
});
test('dedicated packaged x64 smoke cannot silently skip', () => {
const linuxDependencies = workflowStep('Install Linux system dependencies');
assert.match(linuxDependencies, /--no-install-recommends/);
+3 -3
View File
@@ -36,9 +36,9 @@ const {
const X64_ADDON_ENV = { IPTVNATOR_EMBEDDED_MPV_ARCH: 'x64' };
const SYSTEM_PACKAGE_DEPENDENCIES = {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
deb: ['libmpv2', 'libegl1', 'libopengl0', 'libgbm1'],
rpm: ['mpv-libs', 'libglvnd-egl', 'libglvnd-opengl', 'mesa-libgbm'],
pacman: ['mpv', 'libglvnd', 'mesa'],
};
const FRAME_COPY_ARTIFACTS = {
addon: 'embedded_mpv.node',
+8 -3
View File
@@ -34,9 +34,14 @@ const SUPPORTED_PROFILE_NAMES = Object.freeze(
);
const LINUX_SYSTEM_PACKAGE_DEPENDENCIES = Object.freeze({
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
deb: Object.freeze(['libmpv2', 'libegl1', 'libopengl0', 'libgbm1']),
rpm: Object.freeze([
'mpv-libs',
'libglvnd-egl',
'libglvnd-opengl',
'mesa-libgbm',
]),
pacman: Object.freeze(['mpv', 'libglvnd', 'mesa']),
});
function expectedProfileNames() {
@@ -58,13 +58,18 @@ test('defines the exact immutable Linux frame-copy profile matrix', () => {
}
});
test('defines immutable system-package libmpv dependencies', () => {
test('defines immutable system-package helper runtime dependencies', () => {
assert.deepEqual(LINUX_SYSTEM_PACKAGE_DEPENDENCIES, {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
deb: ['libmpv2', 'libegl1', 'libopengl0', 'libgbm1'],
rpm: ['mpv-libs', 'libglvnd-egl', 'libglvnd-opengl', 'mesa-libgbm'],
pacman: ['mpv', 'libglvnd', 'mesa'],
});
assert.equal(Object.isFrozen(LINUX_SYSTEM_PACKAGE_DEPENDENCIES), true);
for (const dependencies of Object.values(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES
)) {
assert.equal(Object.isFrozen(dependencies), true);
}
});
test('resolves each supported profile as an immutable defensive value', () => {
@@ -158,7 +163,7 @@ test('rejects a non-array profile target list', () => {
});
test('keeps frame-copy package dependencies out of the base Electron Builder config', () => {
for (const [target, dependency] of Object.entries(
for (const [target, dependencies] of Object.entries(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES
)) {
assert.equal(
@@ -166,14 +171,16 @@ test('keeps frame-copy package dependencies out of the base Electron Builder con
undefined,
`${target}.depends must remain unset so electron-builder keeps its defaults`
);
assert.equal(
hasSystemFrameCopyDependency(
electronBuilderConfig,
target,
dependency
),
false
);
for (const dependency of dependencies) {
assert.equal(
hasSystemFrameCopyDependency(
electronBuilderConfig,
target,
dependency
),
false
);
}
}
});
@@ -191,13 +198,15 @@ test('keeps frame-copy package dependencies out of non-system passes', () => {
];
for (const config of configs) {
for (const [format, dependency] of Object.entries(
for (const [format, dependencies] of Object.entries(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES
)) {
assert.equal(
hasSystemFrameCopyDependency(config, format, dependency),
false
);
for (const dependency of dependencies) {
assert.equal(
hasSystemFrameCopyDependency(config, format, dependency),
false
);
}
}
}
});
@@ -932,8 +932,8 @@ function dependencyMatches(dependency, expected) {
}
export function validateSystemPackageDependencies(format, dependencies) {
const expectedDependency = LINUX_SYSTEM_PACKAGE_DEPENDENCIES[format];
if (!expectedDependency) {
const expectedDependencies = LINUX_SYSTEM_PACKAGE_DEPENDENCIES[format];
if (!expectedDependencies) {
return [];
}
if (!Array.isArray(dependencies)) {
@@ -941,31 +941,29 @@ export function validateSystemPackageDependencies(format, dependencies) {
`Linux ${format} package dependency metadata must be an array.`,
];
}
if (
!dependencies.some((dependency) =>
return expectedDependencies.flatMap((expectedDependency) =>
dependencies.some((dependency) =>
dependencyMatches(String(dependency), expectedDependency)
)
) {
return [
`Linux x64 ${format} package must declare ${expectedDependency}.`,
];
}
return [];
? []
: [
`Linux x64 ${format} package must declare ${expectedDependency}.`,
]
);
}
function validateForeignPackageDependencies(format, dependencies) {
const forbiddenDependency = LINUX_SYSTEM_PACKAGE_DEPENDENCIES[format];
if (
forbiddenDependency &&
const forbiddenDependencies =
LINUX_SYSTEM_PACKAGE_DEPENDENCIES[format] ?? [];
return forbiddenDependencies.flatMap((forbiddenDependency) =>
dependencies.some((dependency) =>
dependencyMatches(String(dependency), forbiddenDependency)
)
) {
return [
`Linux foreign-architecture ${format} package must not declare frame-copy dependency ${forbiddenDependency}.`,
];
}
return [];
? [
`Linux foreign-architecture ${format} package must not declare frame-copy dependency ${forbiddenDependency}.`,
]
: []
);
}
function dependencyFileName(dependencyName) {
@@ -77,13 +77,19 @@ const SYSTEM_MANIFEST = {
nativeViewFallback: 'process-isolated mpv --wid',
libmpvSoname: 'libmpv.so.2',
packageDependencies: {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
deb: ['libmpv2', 'libegl1', 'libopengl0', 'libgbm1'],
rpm: ['mpv-libs', 'libglvnd-egl', 'libglvnd-opengl', 'mesa-libgbm'],
pacman: ['mpv', 'libglvnd', 'mesa'],
},
runtimeFiles: [],
runtimeTotalBytes: 0,
};
const DEB_SYSTEM_PACKAGE_DEPENDENCIES = [
'libmpv2',
'libegl1',
'libopengl0',
'libgbm1',
];
test('uses the shared frozen 3000 ms runtime probe timeout contract', async () => {
assert.equal(
@@ -613,25 +619,43 @@ test('reads x64, arm64, and armv7 ELF architectures without host execution', ()
}
});
test('requires the exact system package dependency for DEB, RPM, and Pacman', () => {
test('requires every direct helper runtime dependency for DEB, RPM, and Pacman', () => {
assert.deepEqual(
validateSystemPackageDependencies('deb', [
'libmpv2 (>= 0.35)',
'libegl1',
'libopengl0',
'libgbm1',
'libc6',
]),
[]
);
assert.deepEqual(
validateSystemPackageDependencies('rpm', ['mpv-libs', 'glibc']),
validateSystemPackageDependencies('rpm', [
'mpv-libs',
'libglvnd-egl',
'libglvnd-opengl',
'mesa-libgbm',
'glibc',
]),
[]
);
assert.deepEqual(
validateSystemPackageDependencies('pacman', ['mpv>=0.35', 'glibc']),
validateSystemPackageDependencies('pacman', [
'mpv>=0.35',
'libglvnd',
'mesa',
'glibc',
]),
[]
);
assert.match(
validateSystemPackageDependencies('deb', ['libmpv1'])[0],
/libmpv2/
validateSystemPackageDependencies('deb', [
'libmpv2',
'libegl1',
'libgbm1',
])[0],
/libopengl0/
);
});
@@ -731,7 +755,12 @@ test('validates an x64 system payload and executes one bounded helper probe', ()
resourceDir: fixture.resourceDir,
artifactFormat: 'deb',
profileName: 'system',
packageDependencies: ['libmpv2 (>= 0.35)'],
packageDependencies: [
'libmpv2 (>= 0.35)',
'libegl1',
'libopengl0',
'libgbm1',
],
elfInspector: validElfInspector,
probeRunner(command, args, options) {
probeCalls.push({ command, args, options });
@@ -787,7 +816,7 @@ test('rejects helper probes terminated by a signal or hard timeout', () => {
resourceDir: fixture.resourceDir,
artifactFormat: 'deb',
profileName: 'system',
packageDependencies: ['libmpv2'],
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
elfInspector: validElfInspector,
probeRunner() {
return probeResult;
@@ -1142,7 +1171,7 @@ test('rejects helper probe framing and fields that runtime capability rejects',
resourceDir: fixture.resourceDir,
artifactFormat: 'deb',
profileName: 'system',
packageDependencies: ['libmpv2'],
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
elfInspector: validElfInspector,
probeRunner() {
return {
@@ -1219,7 +1248,7 @@ test('reports missing helper, wrong mode, wrong profile, isolation, and loader f
resourceDir: fixture.resourceDir,
artifactFormat: 'deb',
profileName: testCase.profileName ?? 'system',
packageDependencies: ['libmpv2'],
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
elfInspector: testCase.elfInspector ?? validElfInspector,
probeRunner: testCase.probeRunner ?? successfulProbeRunner,
});
@@ -1245,7 +1274,7 @@ test('rejects an x64 package manifest produced from a target subset', () => {
resourceDir: fixture.resourceDir,
artifactFormat: 'deb',
profileName: 'system',
packageDependencies: ['libmpv2'],
packageDependencies: DEB_SYSTEM_PACKAGE_DEPENDENCIES,
elfInspector: validElfInspector,
probeRunner: successfulProbeRunner,
});
@@ -1305,19 +1334,23 @@ test('requires marker-only foreign packages, scans Electron, and never probes',
);
assert.equal(probeCalls, 0);
const dependencyErrors = verifyExtractedLinuxFrameCopyRuntime({
resourceDir: fixture.resourceDir,
artifactFormat: 'deb',
profileName: 'system',
packageDependencies: ['libmpv2', 'libc6'],
declaredArch: 'arm64',
elfInspector: validElfInspector,
probeRunner: successfulProbeRunner,
});
assert.match(
dependencyErrors.join('\n'),
/must not declare frame-copy dependency libmpv2/
);
for (const forbiddenDependency of DEB_SYSTEM_PACKAGE_DEPENDENCIES) {
const dependencyErrors = verifyExtractedLinuxFrameCopyRuntime({
resourceDir: fixture.resourceDir,
artifactFormat: 'deb',
profileName: 'system',
packageDependencies: [forbiddenDependency, 'libc6'],
declaredArch: 'arm64',
elfInspector: validElfInspector,
probeRunner: successfulProbeRunner,
});
assert.match(
dependencyErrors.join('\n'),
new RegExp(
`must not declare frame-copy dependency ${forbiddenDependency}`
)
);
}
} finally {
fs.rmSync(fixture.root, { recursive: true, force: true });
}