import assert from 'node:assert/strict'; import crypto from 'node:crypto'; import fs from 'node:fs'; import { createRequire } from 'node:module'; import os from 'node:os'; import { basename, dirname, join } from 'node:path'; import test from 'node:test'; const require = createRequire(import.meta.url); const { DEFAULT_SYSTEM_PKG_CONFIG_DIRS, EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES, MINIMUM_TOOL_VERSIONS, REQUIRED_TOOLS, SOURCE_PACKAGES, createLinuxRuntimeManifest, } = require('../embedded-mpv/build-linux-runtime.cjs'); const { isForeignLinuxEmbeddedMpvArch, linuxUnpackedDirArch, resolveConfiguredLinuxTargetNames, resolveElectronBuilderArchName, validatePackagedEmbeddedMpv, } = require('./embedded-mpv-packaging.cjs'); const { preparePackagedFrameCopyArtifacts, } = require('./embedded-mpv-frame-copy-files.cjs'); const { LICENSE_PATHS_BY_PACKAGE, collectLinuxRuntimeLicenseInputs, generateLinuxRuntimeNotices, } = require('../embedded-mpv/generate-linux-runtime-notices.cjs'); const { ensureSnapGraphicsContentMount, resolveLinuxFrameCopyPackagingContext, } = require('./electron-after-pack.cjs'); const X64_ADDON_ENV = { IPTVNATOR_EMBEDDED_MPV_ARCH: 'x64' }; const SYSTEM_PACKAGE_DEPENDENCIES = { deb: ['libmpv2', 'libegl1', 'libgl1', 'libgbm1'], rpm: ['mpv-libs', 'libglvnd-egl', 'libglvnd-glx', 'mesa-libgbm'], pacman: ['mpv', 'libglvnd', 'mesa'], }; const FRAME_COPY_ARTIFACTS = { addon: 'embedded_mpv.node', frameReader: 'embedded_mpv_frame_reader.node', helper: 'iptvnator_mpv_helper', }; function sha256(contents) { return crypto.createHash('sha256').update(contents).digest('hex'); } function sourcePackageRecord(sourcePackage) { return { version: sourcePackage.version, sourceUrl: sourcePackage.sourceUrl, ...(sourcePackage.sourceTag ? { sourceTag: sourcePackage.sourceTag } : {}), ...(sourcePackage.sourceKind === 'archive' ? { sourceSha256: sourcePackage.expectedSha256 } : { sourceGitCommit: sourcePackage.expectedGitCommit, sourceSubmodules: [...sourcePackage.expectedSubmodules], }), license: sourcePackage.license, }; } function createSourceRuntime(runtimeContents) { const runtimeFiles = Object.entries(runtimeContents) .map(([name, contents]) => ({ name, size: Buffer.byteLength(contents), sha256: sha256(contents), })) .sort((left, right) => left.name.localeCompare(right.name)); const runtimeNames = new Set(runtimeFiles.map(({ name }) => name)); const entries = runtimeFiles.map(({ name }) => { const needed = name.startsWith('libmpv.so') ? [ ...(runtimeNames.has('libavcodec.so.61') ? ['libavcodec.so.61'] : []), 'libEGL.so.1', 'libc.so.6', ] : ['libm.so.6']; return { name, soname: name === 'libmpv.so' ? 'libmpv.so.2' : null, needed: needed.sort(), rpath: [], runpath: ['$ORIGIN'], }; }); const externalDependencies = [ ...new Set( entries .flatMap(({ needed }) => needed) .filter((name) => !runtimeNames.has(name)) ), ].sort(); const sourceRecords = Object.fromEntries( SOURCE_PACKAGES.map((sourcePackage) => [ sourcePackage.id, { ...sourcePackage, ...sourcePackageRecord(sourcePackage), }, ]) ); return createLinuxRuntimeManifest({ sourceRecords, runtimeFiles, abiRecords: runtimeFiles.map(({ name }) => ({ name, requiredGlibc: '2.34', requiredGlibcxx: null, })), dependencyClosure: { entries, externalDependencies, }, buildHost: { platform: 'linux', arch: 'x64', release: 'fixture-kernel', glibcVersion: '2.35', systemPkgConfigDirs: [...DEFAULT_SYSTEM_PKG_CONFIG_DIRS], systemPkgConfigPackages: Object.fromEntries( EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES.map((name) => [ name, `${name}-fixture`, ]) ), tools: Object.fromEntries( REQUIRED_TOOLS.map((name) => [ name, `${name} ${MINIMUM_TOOL_VERSIONS[name]}`, ]) ), }, generatedAt: '2026-07-17T00:00:00.000Z', }); } function createBuildManifest(runtimeContents) { const sourceRuntime = createSourceRuntime(runtimeContents); return { schemaVersion: 1, origin: 'linux-frame-copy-build', generatedAt: '2026-07-17T00:00:00.000Z', platform: 'linux', arch: 'x64', buildInputMode: 'bundled-runtime', sourceRuntimeValidated: true, allowedPackageRuntimeModes: ['system', 'bundled'], packageRuntimeAvailability: { system: true, bundled: true, }, artifacts: { ...FRAME_COPY_ARTIFACTS }, processIsolation: { addonLoadsLibmpv: false, helperLinksLibmpv: true, helperRunpath: ['$ORIGIN/lib'], }, nativeViewFallback: 'process-isolated mpv --wid', libmpvSoname: 'libmpv.so.2', runtimeFiles: sourceRuntime.runtimeFiles.map((entry) => ({ ...entry })), runtimeTotalBytes: sourceRuntime.runtimeTotalBytes, sourceArchive: { schemaVersion: 1, name: 'linux-frame-copy-runtime-sources.tar.xz', sha256: '7'.repeat(64), repositoryRevision: '8'.repeat(40), }, sourceRuntime, }; } function createNoticeFixture(fixtureRoot, sourceRuntime) { const sourceRoot = join(fixtureRoot, 'upstream-license-sources'); for (const sourcePackage of SOURCE_PACKAGES) { for (const relativePath of LICENSE_PATHS_BY_PACKAGE[sourcePackage.id]) { const sourcePath = join(sourceRoot, sourcePackage.id, relativePath); fs.mkdirSync(dirname(sourcePath), { recursive: true }); fs.writeFileSync( sourcePath, `verbatim ${sourcePackage.id} ${relativePath}\n` ); } } const licenseInputRoot = join(fixtureRoot, 'license-inputs'); const noticeSourceDir = join(fixtureRoot, 'generated-notices'); collectLinuxRuntimeLicenseInputs({ sourceRoot, outputRoot: licenseInputRoot, runtimeManifest: sourceRuntime, }); generateLinuxRuntimeNotices({ licenseInputRoot, outputRoot: noticeSourceDir, runtimeManifest: sourceRuntime, }); return noticeSourceDir; } function createNativeFixture({ runtimeContents = { 'libavcodec.so.61': 'libavcodec-runtime', 'libmpv.so': 'libmpv-runtime', 'libmpv.so.2': 'libmpv-runtime', }, buildManifest = createBuildManifest(runtimeContents), } = {}) { const fixtureRoot = fs.mkdtempSync( join(os.tmpdir(), 'iptvnator-embedded-mpv-layout-') ); const appOutDir = join(fixtureRoot, 'linux-unpacked'); const resourceDir = join(appOutDir, 'resources'); const nativeDir = join( resourceDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true }); fs.writeFileSync(join(nativeDir, FRAME_COPY_ARTIFACTS.addon), 'addon'); fs.writeFileSync( join(nativeDir, FRAME_COPY_ARTIFACTS.frameReader), 'reader' ); fs.writeFileSync(join(nativeDir, FRAME_COPY_ARTIFACTS.helper), 'helper', { mode: 0o644, }); fs.writeFileSync( join(nativeDir, 'embedded-mpv-runtime.json'), `${JSON.stringify(buildManifest, null, 2)}\n` ); for (const [name, contents] of Object.entries(runtimeContents)) { fs.writeFileSync(join(nativeDir, 'lib', name), contents); } const noticeSourceDir = createNoticeFixture( fixtureRoot, buildManifest.sourceRuntime ); fs.cpSync(noticeSourceDir, nativeDir, { recursive: true }); fs.writeFileSync(join(appOutDir, 'iptvnator.bin'), 'electron'); return { buildManifest, fixtureRoot, resourceDir, appOutDir, nativeDir, noticeSourceDir, }; } function readManifest(nativeDir) { return JSON.parse( fs.readFileSync(join(nativeDir, 'embedded-mpv-runtime.json'), 'utf8') ); } function pureValidationOptions(options = {}) { return { platform: 'linux', required: true, foreignArch: false, hostPlatform: 'darwin', ...options, }; } function validElfInspector(nativeDir, manifest, overrides = {}) { const libDir = join(nativeDir, 'lib'); const records = new Map([ ['iptvnator', { needed: ['libc.so.6'], rpath: [], runpath: [] }], ['iptvnator.bin', { needed: ['libc.so.6'], rpath: [], runpath: [] }], [ FRAME_COPY_ARTIFACTS.addon, { needed: ['libX11.so.6'], rpath: [], runpath: [] }, ], [ FRAME_COPY_ARTIFACTS.frameReader, { needed: ['libc.so.6'], rpath: [], runpath: [] }, ], [ FRAME_COPY_ARTIFACTS.helper, { needed: [manifest.libmpvSoname, 'libEGL.so.1', 'libc.so.6'], rpath: [], runpath: ['$ORIGIN/lib'], }, ], ]); for (const entry of manifest.runtimeDependencyClosure?.entries ?? []) { records.set(entry.name, { soname: entry.soname ?? null, needed: [...entry.needed], rpath: [...entry.rpath], runpath: [...entry.runpath], }); } for (const [name, value] of Object.entries(overrides)) { records.set(name, value); } return (binaryPath) => { const name = dirname(binaryPath) === libDir ? basename(binaryPath) : basename(binaryPath); const record = records.get(name); if (!record) { throw new Error(`Missing ELF fixture for ${binaryPath}`); } return record; }; } test('resolves the required Linux profile from afterPack targets before mutation', () => { assert.deepEqual( resolveLinuxFrameCopyPackagingContext( { electronPlatformName: 'linux', arch: 1, targets: [{ name: 'DEB' }, { name: 'rpm' }], }, { required: true, environment: { IPTVNATOR_LINUX_FRAME_COPY_PROFILE: 'system', IPTVNATOR_EMBEDDED_MPV_ARCH: 'x64', }, } ), { targetArch: 'x64', foreignArch: false, targetNames: ['deb', 'rpm'], profile: { name: 'system', runtimeMode: 'system', targets: ['deb', 'rpm', 'pacman'], manifestOrigin: 'system-libmpv-frame-copy', }, } ); }); test('keeps every non-x64 Linux target marker-only even when the configured addon arch matches it', () => { const context = resolveLinuxFrameCopyPackagingContext( { electronPlatformName: 'linux', arch: 3, targets: [{ name: 'deb' }], }, { required: true, environment: { IPTVNATOR_LINUX_FRAME_COPY_PROFILE: 'system', IPTVNATOR_EMBEDDED_MPV_ARCH: 'arm64', }, } ); assert.equal(context.targetArch, 'arm64'); assert.equal(context.foreignArch, true); }); test('rejects missing, mixed, and unknown required Linux packaging context', () => { const baseParams = { electronPlatformName: 'linux', arch: 1, targets: [{ name: 'deb' }], }; assert.throws( () => resolveLinuxFrameCopyPackagingContext(baseParams, { required: true, environment: { IPTVNATOR_EMBEDDED_MPV_ARCH: 'x64' }, }), /Linux frame-copy profile is required/ ); assert.throws( () => resolveLinuxFrameCopyPackagingContext( { ...baseParams, targets: [{ name: 'deb' }, { name: 'AppImage' }], }, { required: true, environment: { IPTVNATOR_LINUX_FRAME_COPY_PROFILE: 'system', IPTVNATOR_EMBEDDED_MPV_ARCH: 'x64', }, } ), /cannot build target "appimage"/ ); assert.throws( () => resolveLinuxFrameCopyPackagingContext( { ...baseParams, arch: 99 }, { required: true, environment: { IPTVNATOR_LINUX_FRAME_COPY_PROFILE: 'system', }, } ), /Unknown Electron Builder architecture/ ); assert.throws( () => resolveLinuxFrameCopyPackagingContext( { ...baseParams, arch: 'mips64' }, { required: true, environment: { IPTVNATOR_LINUX_FRAME_COPY_PROFILE: 'system', }, } ), /Unknown Electron Builder architecture/ ); assert.throws( () => resolveLinuxFrameCopyPackagingContext(baseParams, { required: true, environment: { IPTVNATOR_EMBEDDED_MPV_ARCH: 'arm64', }, }), /Linux frame-copy profile is required/ ); }); test('validates a provided Linux profile even when embedded MPV is optional', () => { assert.throws( () => resolveLinuxFrameCopyPackagingContext( { electronPlatformName: 'linux', arch: 1, targets: [{ name: 'snap' }, { name: 'deb' }], }, { required: false, environment: { IPTVNATOR_LINUX_FRAME_COPY_PROFILE: 'portable', IPTVNATOR_EMBEDDED_MPV_ARCH: 'x64', }, } ), /cannot build target "deb"/ ); }); test('creates an exact empty Snap graphics content mount directory', (t) => { const fixtureRoot = fs.mkdtempSync( join(os.tmpdir(), 'iptvnator-snap-graphics-mount-') ); t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true })); assert.equal( ensureSnapGraphicsContentMount(fixtureRoot, ['appimage']), null ); assert.equal(fs.existsSync(join(fixtureRoot, 'graphics')), false); const mountPath = ensureSnapGraphicsContentMount(fixtureRoot, [ 'appimage', 'snap', ]); assert.equal(mountPath, join(fixtureRoot, 'graphics')); const mountStat = fs.lstatSync(mountPath); assert.equal(mountStat.isDirectory(), true); assert.equal(mountStat.isSymbolicLink(), false); assert.equal(mountStat.mode & 0o777, 0o755); assert.deepEqual(fs.readdirSync(mountPath), []); fs.chmodSync(mountPath, 0o700); assert.equal( ensureSnapGraphicsContentMount(fixtureRoot, ['snap']), mountPath ); assert.equal(fs.lstatSync(mountPath).mode & 0o777, 0o755); }); test('rejects a non-empty or redirected Snap graphics content mount', (t) => { const fixtureRoot = fs.mkdtempSync( join(os.tmpdir(), 'iptvnator-snap-graphics-invalid-') ); t.after(() => fs.rmSync(fixtureRoot, { recursive: true, force: true })); const mountPath = join(fixtureRoot, 'graphics'); fs.writeFileSync(mountPath, 'not a directory'); assert.throws( () => ensureSnapGraphicsContentMount(fixtureRoot, ['snap']), /real empty directory/ ); fs.rmSync(mountPath); fs.mkdirSync(mountPath); fs.writeFileSync(join(mountPath, 'unexpected'), 'not empty'); assert.throws( () => ensureSnapGraphicsContentMount(fixtureRoot, ['snap']), /must be empty/ ); fs.rmSync(mountPath, { recursive: true }); const outsidePath = join(fixtureRoot, 'outside'); fs.mkdirSync(outsidePath); fs.symlinkSync(outsidePath, mountPath, 'dir'); assert.throws( () => ensureSnapGraphicsContentMount(fixtureRoot, ['snap']), /real empty directory/ ); }); test('prepares a normalized system profile with no private runtime', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); fs.writeFileSync( join(fixture.nativeDir, 'iptvnator_mpv_helper.exe'), 'stale' ); fs.writeFileSync( join(fixture.nativeDir, 'embedded-mpv-unavailable.txt'), 'stale' ); const manifest = preparePackagedFrameCopyArtifacts( fixture.nativeDir, 'linux', { profile: 'system', targetNames: ['deb', 'rpm', 'pacman'], } ); assert.equal(manifest.profile, 'system'); assert.equal(manifest.runtimeMode, 'system'); assert.equal(manifest.origin, 'system-libmpv-frame-copy'); assert.deepEqual(manifest.targets, ['deb', 'pacman', 'rpm']); assert.deepEqual(manifest.packageDependencies, SYSTEM_PACKAGE_DEPENDENCIES); assert.equal(manifest.libmpvSoname, 'libmpv.so.2'); assert.deepEqual(manifest.runtimeFiles, []); assert.equal(manifest.runtimeTotalBytes, 0); assert.equal(Object.hasOwn(manifest, 'sourceArchive'), false); assert.equal(fs.existsSync(join(fixture.nativeDir, 'lib')), false); assert.equal( fs.statSync(join(fixture.nativeDir, FRAME_COPY_ARTIFACTS.helper)).mode & 0o777, 0o755 ); assert.equal( fs.statSync(join(fixture.nativeDir, FRAME_COPY_ARTIFACTS.frameReader)) .mode & 0o777, 0o644 ); assert.equal( fs.existsSync(join(fixture.nativeDir, 'iptvnator_mpv_helper.exe')), false ); assert.equal( fs.existsSync(join(fixture.nativeDir, 'embedded-mpv-unavailable.txt')), false ); for (const legalPath of [ 'embedded-mpv-notices.json', 'THIRD_PARTY_NOTICES.txt', 'licenses', ]) { assert.equal(fs.existsSync(join(fixture.nativeDir, legalPath)), false); } assert.deepEqual( validatePackagedEmbeddedMpv( fixture.resourceDir, pureValidationOptions({ profile: 'system', targetNames: ['deb', 'rpm', 'pacman'], }) ), [] ); }); test('prepares portable and Flatpak manifests with the exact bundled closure', (t) => { const portable = createNativeFixture(); const flatpak = createNativeFixture(); fs.renameSync( join(flatpak.appOutDir, 'iptvnator.bin'), join(flatpak.appOutDir, 'iptvnator') ); t.after(() => { for (const fixture of [portable, flatpak]) { fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }); } }); for (const legalPath of [ 'embedded-mpv-notices.json', 'THIRD_PARTY_NOTICES.txt', 'licenses', ]) { fs.rmSync(join(portable.nativeDir, legalPath), { recursive: true, force: true, }); } const portableManifest = preparePackagedFrameCopyArtifacts( portable.nativeDir, 'linux', { profile: 'portable', targetNames: ['AppImage', 'SNAP'], noticeSourceDir: portable.noticeSourceDir, } ); const flatpakManifest = preparePackagedFrameCopyArtifacts( flatpak.nativeDir, 'linux', { profile: 'flatpak', targetNames: ['flatpak'], } ); assert.equal(portableManifest.profile, 'portable'); assert.equal(portableManifest.runtimeMode, 'bundled'); assert.equal(portableManifest.origin, 'bundled-lgpl-frame-copy'); assert.deepEqual(portableManifest.targets, ['appimage', 'snap']); assert.deepEqual(portableManifest.packageDependencies, {}); assert.deepEqual( portableManifest.runtimeFiles.map(({ name }) => name).sort(), ['libavcodec.so.61', 'libmpv.so', 'libmpv.so.2'] ); assert.equal( portableManifest.runtimeTotalBytes, portableManifest.runtimeFiles.reduce( (total, runtimeFile) => total + runtimeFile.size, 0 ) ); assert.equal( portableManifest.sourceRuntime.origin, 'vendored-lgpl-source-build' ); assert.ok(portableManifest.sourceRuntime.packages.ffmpeg.sourceSha256); assert.deepEqual( portableManifest.sourceArchive, portable.buildManifest.sourceArchive ); assert.ok( portableManifest.sourceRuntime.ffmpeg.configureFlags.includes( '--disable-gpl' ) ); const notices = JSON.parse( fs.readFileSync( join(portable.nativeDir, 'embedded-mpv-notices.json'), 'utf8' ) ); assert.equal(notices.schemaVersion, 1); assert.equal(notices.origin, 'pinned-linux-runtime-upstream-licenses'); assert.equal(notices.noticeFile.path, 'THIRD_PARTY_NOTICES.txt'); assert.deepEqual( notices.packages.map(({ id }) => id), SOURCE_PACKAGES.map(({ id }) => id).sort() ); assert.ok(notices.packages.every(({ files }) => files.length >= 1)); assert.equal(flatpakManifest.profile, 'flatpak'); assert.equal(flatpakManifest.runtimeMode, 'bundled'); assert.equal(flatpakManifest.origin, 'bundled-lgpl-frame-copy'); assert.deepEqual( flatpakManifest.sourceArchive, flatpak.buildManifest.sourceArchive ); assert.deepEqual( validatePackagedEmbeddedMpv( portable.resourceDir, pureValidationOptions({ profile: 'portable', targetNames: ['appimage', 'snap'], }) ), [] ); assert.deepEqual( validatePackagedEmbeddedMpv( flatpak.resourceDir, pureValidationOptions({ profile: 'flatpak', targetNames: ['flatpak'], hostPlatform: 'linux', elfInspector: validElfInspector( flatpak.nativeDir, flatpakManifest ), }) ), [] ); }); test('rejects missing, tampered, undeclared, and symlinked bundled legal files', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); const manifest = preparePackagedFrameCopyArtifacts( fixture.nativeDir, 'linux', { profile: 'portable', targetNames: ['appimage'], } ); const options = pureValidationOptions({ profile: 'portable', targetNames: ['appimage'], hostPlatform: 'linux', elfInspector: validElfInspector(fixture.nativeDir, manifest), }); const notices = JSON.parse( fs.readFileSync( join(fixture.nativeDir, 'embedded-mpv-notices.json'), 'utf8' ) ); const licensePath = join( fixture.nativeDir, notices.packages[0].files[0].path ); const originalContents = fs.readFileSync(licensePath); fs.appendFileSync(licensePath, 'tampered\n'); assert.match( validatePackagedEmbeddedMpv(fixture.resourceDir, options).join('\n'), /(?:Size|SHA-256) mismatch.*packaged license/i ); fs.writeFileSync(licensePath, originalContents); fs.rmSync(licensePath); assert.match( validatePackagedEmbeddedMpv(fixture.resourceDir, options).join('\n'), /Missing .*packaged license/i ); fs.writeFileSync(licensePath, originalContents); const undeclaredPath = join(fixture.nativeDir, 'licenses', 'stale.txt'); fs.writeFileSync(undeclaredPath, 'stale\n'); assert.match( validatePackagedEmbeddedMpv(fixture.resourceDir, options).join('\n'), /undeclared packaged legal file.*stale\.txt/i ); fs.rmSync(undeclaredPath); fs.rmSync(licensePath); fs.symlinkSync( join(fixture.nativeDir, 'THIRD_PARTY_NOTICES.txt'), licensePath ); assert.match( validatePackagedEmbeddedMpv(fixture.resourceDir, options).join('\n'), /packaged license.*symbolic link/i ); }); test('rejects invalid build provenance and incomplete bundled runtime input', (t) => { const extra = createNativeFixture(); const missing = createNativeFixture(); const invalidSource = createBuildManifest({ 'libavcodec.so.61': 'libavcodec-runtime', 'libmpv.so': 'libmpv-runtime', 'libmpv.so.2': 'libmpv-runtime', }); invalidSource.sourceRuntime.ffmpeg.configureFlags.push('--enable-gpl'); const invalid = createNativeFixture({ buildManifest: invalidSource }); const unexpectedModeManifest = createBuildManifest({ 'libavcodec.so.61': 'libavcodec-runtime', 'libmpv.so': 'libmpv-runtime', 'libmpv.so.2': 'libmpv-runtime', }); unexpectedModeManifest.allowedPackageRuntimeModes.push('development'); const unexpectedMode = createNativeFixture({ buildManifest: unexpectedModeManifest, }); const wrongSonameManifest = createBuildManifest({ 'libavcodec.so.61': 'libavcodec-runtime', 'libmpv.so': 'libmpv-runtime', 'libmpv.so.2': 'libmpv-runtime', }); wrongSonameManifest.libmpvSoname = 'libmpv.so.9'; const wrongSoname = createNativeFixture({ buildManifest: wrongSonameManifest, }); const wrongSourceArchiveManifest = createBuildManifest({ 'libavcodec.so.61': 'libavcodec-runtime', 'libmpv.so': 'libmpv-runtime', 'libmpv.so.2': 'libmpv-runtime', }); wrongSourceArchiveManifest.sourceArchive.sha256 = 'not-a-digest'; const wrongSourceArchive = createNativeFixture({ buildManifest: wrongSourceArchiveManifest, }); t.after(() => { for (const fixture of [ extra, missing, invalid, unexpectedMode, wrongSoname, wrongSourceArchive, ]) { fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }); } }); fs.writeFileSync(join(extra.nativeDir, 'lib', 'libstale.so.1'), 'stale'); fs.rmSync(join(missing.nativeDir, 'lib', 'libavcodec.so.61')); assert.throws( () => preparePackagedFrameCopyArtifacts(extra.nativeDir, 'linux', { profile: 'portable', targetNames: ['appimage'], }), /undeclared bundled runtime file.*libstale\.so\.1/i ); assert.throws( () => preparePackagedFrameCopyArtifacts(missing.nativeDir, 'linux', { profile: 'flatpak', targetNames: ['flatpak'], }), /Missing bundled runtime file.*libavcodec\.so\.61/ ); assert.throws( () => preparePackagedFrameCopyArtifacts(invalid.nativeDir, 'linux', { profile: 'portable', targetNames: ['snap'], }), /--enable-gpl/ ); assert.throws( () => preparePackagedFrameCopyArtifacts( unexpectedMode.nativeDir, 'linux', { profile: 'portable', targetNames: ['appimage'], } ), /allowedPackageRuntimeModes.*exactly/i ); assert.throws( () => preparePackagedFrameCopyArtifacts(wrongSoname.nativeDir, 'linux', { profile: 'portable', targetNames: ['appimage'], }), /derived from.*SONAME/i ); assert.throws( () => preparePackagedFrameCopyArtifacts( wrongSourceArchive.nativeDir, 'linux', { profile: 'portable', targetNames: ['snap'], } ), /source archive binding.*sha256/i ); }); test('rejects profiled preparation without a concrete package target', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); assert.throws( () => preparePackagedFrameCopyArtifacts(fixture.nativeDir, 'linux', { profile: 'portable', targetNames: [], }), /at least one target/ ); }); test( 'rejects symlinked frame-copy artifacts before chmod or packaging', { skip: process.platform === 'win32' }, (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); const readerPath = join( fixture.nativeDir, FRAME_COPY_ARTIFACTS.frameReader ); fs.rmSync(readerPath); fs.symlinkSync(FRAME_COPY_ARTIFACTS.addon, readerPath); assert.throws( () => preparePackagedFrameCopyArtifacts(fixture.nativeDir, 'linux', { profile: 'system', targetNames: ['deb'], }), /frame reader.*regular file/i ); assert.equal(fs.lstatSync(readerPath).isSymbolicLink(), true); } ); test('keeps optional unprofiled Linux packages explicitly native-view-only', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); const manifest = preparePackagedFrameCopyArtifacts( fixture.nativeDir, 'linux' ); assert.equal(manifest.origin, 'external-mpv-process'); assert.equal(manifest.runtimeMode, 'native-view-only'); assert.equal(manifest.frameCopyAvailable, false); assert.equal( fs.existsSync(join(fixture.nativeDir, FRAME_COPY_ARTIFACTS.addon)), true ); assert.equal( fs.existsSync(join(fixture.nativeDir, FRAME_COPY_ARTIFACTS.helper)), false ); assert.equal( fs.existsSync( join(fixture.nativeDir, FRAME_COPY_ARTIFACTS.frameReader) ), false ); assert.equal(fs.existsSync(join(fixture.nativeDir, 'lib')), false); assert.notEqual( readManifest(fixture.nativeDir).origin, 'linux-frame-copy-build' ); }); test('rejects incorrect artifact modes and profile mismatches deterministically', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); preparePackagedFrameCopyArtifacts(fixture.nativeDir, 'linux', { profile: 'system', targetNames: ['deb'], }); fs.chmodSync(join(fixture.nativeDir, FRAME_COPY_ARTIFACTS.helper), 0o644); const modeErrors = validatePackagedEmbeddedMpv( fixture.resourceDir, pureValidationOptions({ profile: 'system', targetNames: ['deb'], }) ); assert.match(modeErrors.join('\n'), /mode 0755/); fs.chmodSync(join(fixture.nativeDir, FRAME_COPY_ARTIFACTS.helper), 0o4755); assert.match( validatePackagedEmbeddedMpv( fixture.resourceDir, pureValidationOptions({ profile: 'system', targetNames: ['deb'], }) ).join('\n'), /mode 0755/ ); fs.chmodSync(join(fixture.nativeDir, FRAME_COPY_ARTIFACTS.helper), 0o755); const profileErrors = validatePackagedEmbeddedMpv( fixture.resourceDir, pureValidationOptions({ profile: 'portable', targetNames: ['appimage'], }) ); assert.match(profileErrors.join('\n'), /profile.*portable.*system/i); }); test('turns malformed packaged manifest JSON into a deterministic error', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); fs.writeFileSync( join(fixture.nativeDir, 'embedded-mpv-runtime.json'), '{not-json' ); assert.doesNotThrow(() => validatePackagedEmbeddedMpv( fixture.resourceDir, pureValidationOptions({ profile: 'system', targetNames: ['deb'], }) ) ); assert.match( validatePackagedEmbeddedMpv( fixture.resourceDir, pureValidationOptions({ profile: 'system', targetNames: ['deb'], }) ).join('\n'), /Invalid JSON in embedded MPV runtime manifest/ ); }); test('validates Linux ELF process isolation, helper linkage, and bundled closure', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); const manifest = preparePackagedFrameCopyArtifacts( fixture.nativeDir, 'linux', { profile: 'portable', targetNames: ['appimage'], } ); const options = { platform: 'linux', required: true, foreignArch: false, profile: 'portable', targetNames: ['appimage'], hostPlatform: 'linux', elfInspector: validElfInspector(fixture.nativeDir, manifest), }; assert.deepEqual( validatePackagedEmbeddedMpv(fixture.resourceDir, options), [] ); for (const artifactName of [ FRAME_COPY_ARTIFACTS.addon, FRAME_COPY_ARTIFACTS.frameReader, ]) { const isolationErrors = validatePackagedEmbeddedMpv( fixture.resourceDir, { ...options, elfInspector: validElfInspector(fixture.nativeDir, manifest, { [artifactName]: { needed: ['libmpv.so.2'], rpath: [], runpath: [], }, }), } ); assert.match( isolationErrors.join('\n'), /must not link libmpv/, `${artifactName} must remain process-isolated from libmpv` ); } const pathBearingAddonLinkErrors = validatePackagedEmbeddedMpv( fixture.resourceDir, { ...options, elfInspector: validElfInspector(fixture.nativeDir, manifest, { [FRAME_COPY_ARTIFACTS.addon]: { needed: ['/tmp/build/libmpv.so.2'], rpath: [], runpath: [], }, }), } ); assert.match( pathBearingAddonLinkErrors.join('\n'), /must not link libmpv.*\/tmp\/build\/libmpv\.so\.2/ ); const helperLinkErrors = validatePackagedEmbeddedMpv(fixture.resourceDir, { ...options, elfInspector: validElfInspector(fixture.nativeDir, manifest, { [FRAME_COPY_ARTIFACTS.helper]: { needed: ['libmpv.so.1'], rpath: [], runpath: ['$ORIGIN/lib'], }, }), }); assert.match( helperLinkErrors.join('\n'), /must directly need libmpv\.so\.2/ ); const unexpectedHelperLinkErrors = validatePackagedEmbeddedMpv( fixture.resourceDir, { ...options, elfInspector: validElfInspector(fixture.nativeDir, manifest, { [FRAME_COPY_ARTIFACTS.helper]: { needed: [ manifest.libmpvSoname, 'libEGL.so.1', 'libc.so.6', 'libsurprise.so.1', ], rpath: [], runpath: ['$ORIGIN/lib'], }, }), } ); assert.match( unexpectedHelperLinkErrors.join('\n'), /helper dependency is not bundled or allowlisted.*libsurprise\.so\.1/ ); const closureErrors = validatePackagedEmbeddedMpv(fixture.resourceDir, { ...options, elfInspector: validElfInspector(fixture.nativeDir, manifest, { 'libavcodec.so.61': { needed: ['libsurprise.so.1'], rpath: [], runpath: ['/tmp/runtime-prefix'], }, }), }); assert.match(closureErrors.join('\n'), /RUNPATH must be exactly \$ORIGIN/); assert.match( closureErrors.join('\n'), /not bundled or allowlisted.*libsurprise\.so\.1/ ); fs.writeFileSync( join(fixture.appOutDir, 'libelectron-extra.so'), 'electron library' ); const electronLibraryErrors = validatePackagedEmbeddedMpv( fixture.resourceDir, { ...options, elfInspector: validElfInspector(fixture.nativeDir, manifest, { 'libelectron-extra.so': { needed: ['libmpv.so.2'], rpath: [], runpath: [], }, }), } ); assert.match( electronLibraryErrors.join('\n'), /Linux Electron library must not link libmpv.*libelectron-extra\.so/ ); }); test('recursively validates pristine Electron libraries before target packaging', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); const manifest = preparePackagedFrameCopyArtifacts( fixture.nativeDir, 'linux', { profile: 'portable', targetNames: ['snap'], } ); const nestedElectronLibrary = join( fixture.appOutDir, 'future-electron-runtime', 'libfuture-electron.so' ); fs.mkdirSync(dirname(nestedElectronLibrary), { recursive: true }); fs.writeFileSync(nestedElectronLibrary, 'future electron library'); const validationOptions = { platform: 'linux', required: true, foreignArch: false, profile: 'portable', targetNames: ['snap'], hostPlatform: 'linux', }; const errors = validatePackagedEmbeddedMpv(fixture.resourceDir, { ...validationOptions, elfInspector: validElfInspector(fixture.nativeDir, manifest, { 'libfuture-electron.so': { needed: ['libmpv.so.2'], rpath: [], runpath: [], }, }), }); assert.match( errors.join('\n'), /Linux Electron library must not link libmpv.*libfuture-electron\.so/ ); fs.rmSync(dirname(nestedElectronLibrary), { recursive: true, force: true, }); for (const packageLibraryDir of [ join(fixture.appOutDir, 'lib', 'x86_64-linux-gnu'), join(fixture.appOutDir, 'usr', 'lib', 'x86_64-linux-gnu'), ]) { const targetPath = join(packageLibraryDir, 'libpackage.so.1.0'); fs.mkdirSync(packageLibraryDir, { recursive: true }); fs.writeFileSync(targetPath, 'Snap template library'); fs.symlinkSync( 'libpackage.so.1.0', join(packageLibraryDir, 'libpackage.so.1') ); } assert.deepEqual( validatePackagedEmbeddedMpv(fixture.resourceDir, { ...validationOptions, artifactFormat: 'snap', elfInspector: validElfInspector(fixture.nativeDir, manifest), }), [] ); fs.mkdirSync(dirname(nestedElectronLibrary), { recursive: true }); fs.writeFileSync(nestedElectronLibrary, 'future electron library'); const snapIsolationErrors = validatePackagedEmbeddedMpv( fixture.resourceDir, { ...validationOptions, artifactFormat: 'snap', elfInspector: validElfInspector(fixture.nativeDir, manifest, { 'libfuture-electron.so': { needed: ['libmpv.so.2'], rpath: [], runpath: [], }, }), } ); assert.match( snapIsolationErrors.join('\n'), /Linux Electron library must not link libmpv.*libfuture-electron\.so/ ); }); test('rejects undeclared helper dependencies in system-runtime packages', (t) => { const fixture = createNativeFixture(); t.after(() => fs.rmSync(fixture.fixtureRoot, { recursive: true, force: true }) ); const manifest = preparePackagedFrameCopyArtifacts( fixture.nativeDir, 'linux', { profile: 'system', targetNames: ['deb'], } ); const errors = validatePackagedEmbeddedMpv(fixture.resourceDir, { platform: 'linux', required: true, foreignArch: false, profile: 'system', targetNames: ['deb'], hostPlatform: 'linux', elfInspector: validElfInspector(fixture.nativeDir, manifest, { [FRAME_COPY_ARTIFACTS.helper]: { needed: [ manifest.libmpvSoname, 'libEGL.so.1', 'libc.so.6', 'libsurprise.so.1', ], rpath: [], runpath: ['$ORIGIN/lib'], }, }), }); assert.match( errors.join('\n'), /helper dependency is not bundled or allowlisted.*libsurprise\.so\.1/ ); }); test('resolveElectronBuilderArchName maps builder-util Arch enum values', () => { assert.equal(resolveElectronBuilderArchName(0), 'ia32'); assert.equal(resolveElectronBuilderArchName(1), 'x64'); assert.equal(resolveElectronBuilderArchName(2), 'armv7l'); assert.equal(resolveElectronBuilderArchName(3), 'arm64'); assert.equal(resolveElectronBuilderArchName('arm64'), 'arm64'); assert.equal(resolveElectronBuilderArchName(99), null); }); test('flags every non-x64 Linux package regardless of configured build arch', () => { assert.equal( isForeignLinuxEmbeddedMpvArch('linux', 3, X64_ADDON_ENV), true ); assert.equal( isForeignLinuxEmbeddedMpvArch('linux', 'armv7l', X64_ADDON_ENV), true ); assert.equal( isForeignLinuxEmbeddedMpvArch('linux', 'x64', X64_ADDON_ENV), false ); assert.equal( isForeignLinuxEmbeddedMpvArch('linux', 'arm64', { IPTVNATOR_EMBEDDED_MPV_ARCH: 'arm64', }), true ); // Only Linux fans out foreign arches from one dist tree. assert.equal( isForeignLinuxEmbeddedMpvArch('darwin', 'arm64', X64_ADDON_ENV), false ); assert.equal( isForeignLinuxEmbeddedMpvArch('win32', 'arm64', X64_ADDON_ENV), false ); }); test('derives package arch from electron-builder Linux output directory names', () => { assert.equal(linuxUnpackedDirArch('linux-unpacked'), 'x64'); assert.equal(linuxUnpackedDirArch('linux-arm64-unpacked'), 'arm64'); assert.equal(linuxUnpackedDirArch('linux-armv7l-unpacked'), 'armv7l'); assert.equal(linuxUnpackedDirArch('mac-arm64'), null); assert.equal(linuxUnpackedDirArch('win-unpacked'), null); }); test('derives the exact selected Linux targets for each configured architecture', () => { const configuredTargets = [ { target: 'AppImage', arch: ['x64', 'arm64'], }, { target: 'deb', arch: ['x64'], }, { target: 'Snap', arch: ['arm64'], }, 'rpm', ]; assert.deepEqual( resolveConfiguredLinuxTargetNames(configuredTargets, 'x64'), ['appimage', 'deb', 'rpm'] ); assert.deepEqual( resolveConfiguredLinuxTargetNames(configuredTargets, 'arm64'), ['appimage', 'rpm', 'snap'] ); }); function createResourceDir(files) { const resourceDir = fs.mkdtempSync( join(os.tmpdir(), 'iptvnator-embedded-mpv-arch-') ); const nativeDir = join( resourceDir, 'app.asar.unpacked', 'electron-backend', 'native' ); fs.mkdirSync(nativeDir, { recursive: true }); for (const [name, content] of Object.entries(files)) { fs.writeFileSync(join(nativeDir, name), content); } return resourceDir; } test('foreign-arch validation requires the unavailable marker and forbids the addon', (t) => { const withMarker = createResourceDir({ 'embedded-mpv-unavailable.txt': 'not bundled for arm64\n', }); const withAddon = createResourceDir({ 'embedded-mpv-unavailable.txt': 'not bundled for arm64\n', 'embedded_mpv.node': 'x64 machine code', }); const empty = createResourceDir({}); t.after(() => { for (const dir of [withMarker, withAddon, empty]) { fs.rmSync(dir, { recursive: true, force: true }); } }); assert.deepEqual( validatePackagedEmbeddedMpv(withMarker, { platform: 'linux', required: true, foreignArch: true, }), [] ); const addonErrors = validatePackagedEmbeddedMpv(withAddon, { platform: 'linux', required: true, foreignArch: true, }); assert.equal(addonErrors.length, 1); assert.match(addonErrors[0], /must not ship in foreign-architecture/); const markerErrors = validatePackagedEmbeddedMpv(empty, { platform: 'linux', required: true, foreignArch: true, }); assert.equal(markerErrors.length, 1); assert.match(markerErrors[0], /unavailable marker/); }); test('same-arch validation still requires the addon when embedded MPV is required', (t) => { const empty = createResourceDir({}); t.after(() => fs.rmSync(empty, { recursive: true, force: true })); const errors = validatePackagedEmbeddedMpv(empty, { platform: 'linux', required: true, foreignArch: false, }); assert.equal(errors.length, 1); assert.match(errors[0], /Missing embedded MPV native addon/); });