Files
iptvnator/tools/packaging/electron-package-identity.test.mjs
T
4grayandClaude Fable 5 7d75d989e8 feat(embedded-mpv): Linux port of the frame-copy rendering engine (headless EGL) (#1171)
* feat(embedded-mpv): Linux frame-copy helper via headless EGL

Port the frame-copy engine's native layer to Linux (PORTING.md items 1-4):

- frame_helper_gl.h: platform GlContext abstraction. macOS keeps the CGL
  path (moved verbatim); Linux acquires an EGL display in order
  surfaceless-Mesa -> default display -> GBM render node, binds a 3.2 core
  desktop-GL context surfaceless (1x1 pbuffer fallback), and hands mpv
  eglGetProcAddress. The helper's own GL calls link against glvnd
  libOpenGL, so no display server is required.
- frame_shm.h: portable frame_shm_now_ns() (CLOCK_MONOTONIC) shared by the
  helper and the reader addon, replacing the macOS-only
  clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); producer and consumer stay on
  the same clock.
- embedded_mpv_frame_reader.c: real implementation now also on __linux__
  (the code was already POSIX apart from the clock call).
- binding.gyp: OS==linux executable branch for iptvnator_mpv_helper linking
  system libmpv (-lmpv) + EGL/OpenGL/gbm, with rpaths for $ORIGIN/lib and
  the build-time library dir. The in-process addon still does not link
  libmpv - the ban only binds in-process, the helper is out of process.
- build-embedded-mpv.js: system-dev fallback on Linux (LIBMPV_INCLUDE_DIR
  or /usr/include) so a distro libmpv-dev install builds without staging a
  vendored runtime; a pre-set LINUX_NATIVE_LIBRARY_DIR now wins over the
  vendored lib dir.

Verified on Ubuntu 25.04 / i7-1165G7 (Iris Xe): lavfi smoke per PORTING.md
(idle->loading->playing snapshots at 4 Hz, aspect-fit generation bump
g1 1280x720 -> g2 960x720 for a 4:3 source), reader probe 60 fps at
1080p60 with 0 torn reads, clean quit with no leaked processes or shm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(embedded-mpv): enable the frame-copy engine gates on Linux

Flip the TypeScript side of the Linux port (PORTING.md item 5). A shared
dependency-free predicate, isFrameCopyPlatformSupported() (linux any-arch,
darwin arm64-only), now backs all four gates so they cannot drift:

- main.ts: the persisted Settings toggle promotes to the env flag on Linux
  too (this runs before window creation and controls the sandbox relax).
- EmbeddedMpvNativeService.isFrameCopyEngineActive/isFrameCopyAvailable.
- EmbeddedMpvFrameCopyAdapter.isSupported.

getSupport() ordering: the frame-copy branch moves above the Linux-only
native-engine prerequisites - the X11/Xwayland display-server check and
the system-mpv-on-PATH probe only bind the --wid native engine, while the
frame-copy helper renders offscreen (headless EGL) and links libmpv
itself. createSession() also skips resolving the native window handle for
frame-copy sessions, which the adapter ignores anyway, so native-Wayland
sessions no longer trip the window-handle assertion.

Settings copy: the i18n frame-copy description now says macOS (Apple
Silicon) and Linux in all 18 languages; stale macOS-only doc comments in
the settings/support interfaces updated alongside.

Tests: platform-gate matrix for the adapter (darwin arm64/x64, linux
x64/arm64, win32) and service specs covering Linux activation under
native Wayland, macOS arm64 staying active, macOS x64 staying native, and
the skipped window handle for frame-copy sessions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(packaging): CI + package guards for the Linux frame-copy helper

- build-and-make.yaml: install libegl-dev/libgl-dev/libgbm-dev on the
  Linux runner (the helper's EGL backend needs them now that the helper
  target builds on Linux), and verify the built helper exists and DOES
  link libmpv - the inverse of the addon's no-libmpv rule, which still
  holds and stays validated.
- electron-after-pack.cjs: strip iptvnator_mpv_helper from packaged Linux
  apps. It links the build host's system libmpv, which end-user systems
  cannot be assumed to have; the support probe treats the missing helper
  as frame-copy-unavailable (dev-build-only engine until the
  bundled-runtime staging milestone).
- frame_helper_gl.h: log the chosen EGL display tier to stderr (the
  adapter mirrors helper stderr), so bring-up problems on exotic setups
  are diagnosable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(embedded-mpv): document the Linux frame-copy port

- architecture doc: frame-copy section covers Linux (EGL display tiers,
  build deps, package strip), Linux support matrix notes the frame-copy
  exception to the X11 + system-mpv requirements, Linux measured baseline.
- RESULTS.md: Ubuntu 25.04 / i7-1165G7 (Iris Xe) measurement rows via the
  production helper + reader probe; viewport-size claim reproduced.
- PORTING.md: Linux marked done with pointers to what changed; Windows
  remains the open port and its perf gate the open decision.
- CLAUDE.md + tools/embedded-mpv/README.md: platform scope, Linux dev
  build requirements, system-headers fallback, helper strip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(embedded-mpv): commit the Linux frame-copy measurement probe

linux-frame-probe.mjs reproduces the RESULTS.md Linux rows: spawns the
production helper, attaches the frame-reader addon to the announced shm
generation, and reports new-frame fps, copy wall time, produce->copy age,
torn reads and pixel spread. Usage documented in RESULTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): address multi-agent review findings on the Linux port

Confirmed findings (each verified by 3 adversarial reviewers):

- CI would fail to link the helper: -lOpenGL needs the unversioned glvnd
  libOpenGL.so, shipped only by libopengl-dev, which neither the runner
  images nor the previous apt line provide. Added to the workflow and to
  every documented Linux build-dep list.
- The new 'test -x' dist guard could never pass: webpack's dist asset
  copy drops file modes (helper arrives as 0644). The guard is now
  'test -f'; electron-after-pack.cjs restores the execute bit on packaged
  helpers (also fixes packaged-macOS spawns); the support probe now
  requires X_OK, so a mode-stripped helper reads as frame-copy-unavailable
  and falls back to native instead of failing spawn with EACCES.
- The Settings frame-copy toggle was unreachable in exactly the Linux
  states the port targets: the native-Wayland and missing-system-mpv
  unsupported payloads omitted frameCopyAvailable, and toggle visibility
  derives solely from it. Both returns now advertise availability.

Also from review:

- build-embedded-mpv.js keeps the old graceful-skip contract when the new
  system-dev fallback finds libmpv-dev but the GL/EGL/gbm dev stack is
  missing (previously such machines skipped; a hard electron-build
  failure was a regression).
- createSession derives the window-handle skip from the dispatched addon
  instead of re-evaluating the engine gate, so the two cannot disagree.
- The render thread logs the GL renderer string (surfaceless Mesa can
  silently pick llvmpipe on non-Mesa-primary systems; now diagnosable —
  verified 'Mesa Intel Iris Xe' on this machine).
- Specs pin the new semantics: frameCopyAvailable advertised while native
  is unsupported (Wayland / no mpv), frame-copy supported without a
  system mpv, and the handle-skip test disposes its session through the
  owning adapter.
- Docs: PORTING.md file map reflects the frame_helper_gl.h seam for the
  Windows porter; helper-strip removal correctly gated on milestone 4
  (bundled libmpv), not milestone 3; RESULTS.md preamble notes the
  RAW->MONOTONIC clock change; stale '(macOS)' scope comments updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): address Greptile/Codex review comments

- Sandbox gate requires a usable helper (Greptile P1, security): the
  main.ts env promotion now also probes for an executable
  iptvnator_mpv_helper before relaxing the window sandbox — a stale
  opt-in on packaged Linux (helper deliberately stripped) or after a
  cleaned native build no longer costs a sandboxless launch for an
  engine that cannot activate. Helper discovery (addon candidate paths +
  X_OK probe) moved into embedded-mpv-frame-copy-platform.util.ts,
  shared by main.ts and the service; the service keeps thin instance
  wrappers so tests can stub per scenario. New util spec pins the
  platform matrix, candidate resolution, and the execute-bit semantics.
- Stale frame-copy artifacts on skipped builds (Codex P2): cleanOutput()
  now also removes iptvnator_mpv_helper and
  embedded_mpv_frame_reader.node, so a failed/skipped rebuild cannot
  leave a previous helper advertising frame-copy support against a
  runtime the build just declared unavailable.
- Multiarch default lib dir (Greptile P1, partially refuted): -l
  resolution never depended on our -L (the compiler's built-in search
  paths include the Debian/Ubuntu multiarch dir — proven by the green CI
  run linking with a nonexistent -L dir), but the system-dev fallback
  now defaults to /usr/lib/<multiarch-triple> when present so the -L
  flag and the helper's baked rpath point somewhere real.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(embedded-mpv): harden Linux frame-copy port

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 20:42:50 +02:00

659 lines
22 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import os from 'node:os';
import { dirname, join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const currentDir = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const packageMetadata = JSON.parse(
fs.readFileSync(join(currentDir, '..', '..', 'package.json'), 'utf8')
);
const buildAndMakeWorkflow = fs.readFileSync(
join(currentDir, '..', '..', '.github', 'workflows', 'build-and-make.yaml'),
'utf8'
);
const electronBuilderConfig = JSON.parse(
fs.readFileSync(
join(currentDir, '..', '..', 'electron-builder.json'),
'utf8'
)
);
const electronProjectConfig = JSON.parse(
fs.readFileSync(
join(
currentDir,
'..',
'..',
'apps',
'electron-backend',
'project.json'
),
'utf8'
)
);
const makerOptions = JSON.parse(
fs.readFileSync(
join(
currentDir,
'..',
'..',
'apps',
'electron-backend',
'src',
'app',
'options',
'maker.options.json'
),
'utf8'
)
);
const generatedMetadataConfigPath =
'apps/electron-backend/src/app/options/electron-builder.metadata.generated.json';
const packageLayoutVerifier = fs.readFileSync(
join(currentDir, 'verify-electron-package-layout.mjs'),
'utf8'
);
const electronAfterPackSource = fs.readFileSync(
join(currentDir, 'electron-after-pack.cjs'),
'utf8'
);
const frameCopyFilesModulePath = join(
currentDir,
'embedded-mpv-frame-copy-files.cjs'
);
const embeddedMpvPackagingSource = fs.readFileSync(
join(currentDir, 'embedded-mpv-packaging.cjs'),
'utf8'
);
const embeddedMpvBuildSource = fs.readFileSync(
join(
currentDir,
'..',
'..',
'apps',
'electron-backend',
'build-embedded-mpv.js'
),
'utf8'
);
const embeddedMpvStageRuntimeSource = fs.readFileSync(
join(currentDir, '..', 'embedded-mpv', 'stage-runtime.mjs'),
'utf8'
);
const embeddedMpvWindowsArchiveStageSource = fs.readFileSync(
join(currentDir, '..', 'embedded-mpv', 'stage-windows-runtime-archive.mjs'),
'utf8'
);
const embeddedMpvWin32Source = fs.readFileSync(
join(
currentDir,
'..',
'..',
'apps',
'electron-backend',
'native',
'src',
'embedded_mpv_win32.cc'
),
'utf8'
);
const { validatePackagedEmbeddedMpv } = require('./embedded-mpv-packaging.cjs');
test('Linux package identity does not expose the internal Electron backend project name', () => {
assert.equal(electronBuilderConfig.productName, 'IPTVnator');
assert.equal(electronBuilderConfig.extraMetadata?.name, 'iptvnator');
assert.equal(electronBuilderConfig.extraMetadata?.productName, 'IPTVnator');
assert.equal(electronBuilderConfig.linux?.executableName, 'iptvnator');
assert.equal(
electronBuilderConfig.linux?.desktop?.entry?.StartupWMClass,
'iptvnator'
);
assert.ok(
electronBuilderConfig.linux?.executableArgs?.includes(
'--ozone-platform=x11'
)
);
});
test('GitHub Releases auto-update metadata is generated and uploaded', () => {
const releaseFiles = buildAndMakeWorkflow.match(
/files: \|\n([\s\S]*?)\n\s+env:/
)?.[1];
assert.ok(releaseFiles, 'release upload files block must exist');
assert.ok(
packageMetadata.dependencies?.['electron-updater'],
'electron-updater must be a runtime dependency because the packaged app imports it'
);
assert.deepEqual(electronBuilderConfig.publish, [
{
provider: 'github',
owner: '4gray',
repo: 'iptvnator',
},
]);
assert.deepEqual(electronBuilderConfig.mac?.target, [
{
target: 'dmg',
arch: ['x64', 'arm64'],
},
{
target: 'zip',
arch: ['x64', 'arm64'],
},
]);
assert.match(buildAndMakeWorkflow, /dist\/executables\/\*\*\/latest\.yml/);
assert.match(
buildAndMakeWorkflow,
/dist\/executables\/\*\*\/latest-mac\.yml/
);
assert.match(
buildAndMakeWorkflow,
/dist\/executables\/\*\*\/latest-linux\*\.yml/
);
assert.match(buildAndMakeWorkflow, /dist\/executables\/\*\*\/\*\.blockmap/);
assert.match(buildAndMakeWorkflow, /Merge macOS updater metadata/);
assert.match(buildAndMakeWorkflow, /artifacts\/latest-mac\.yml/);
assert.doesNotMatch(
releaseFiles,
/artifacts\/macos-(?:x64|arm64)-artifacts\/latest-mac\.yml/
);
assert.match(
buildAndMakeWorkflow,
/artifacts\/linux-artifacts\/latest-linux\*\.yml/
);
assert.match(
buildAndMakeWorkflow,
/artifacts\/windows-artifacts\/latest\.yml/
);
const makeCommands = [
...buildAndMakeWorkflow.matchAll(/run: (pnpm run make:app[^\n]*)/g),
].map((match) => match[1].trim());
assert.ok(makeCommands.length >= 2, 'workflow must package Electron apps');
assert.deepEqual(
[...new Set(makeCommands)],
['pnpm run make:app -- --publishPolicy=never']
);
});
test('generated Electron package metadata mirrors the root package identity', async () => {
const { buildElectronBuilderMetadata, buildElectronPackageMetadata } =
await import('./generate-electron-builder-metadata.mjs');
const generatedElectronPackage = buildElectronPackageMetadata(
packageMetadata,
electronBuilderConfig,
{
name: 'electron-backend',
version: '0.0.1',
dependencies: {
'better-sqlite3': '12.5.0',
},
}
);
assert.deepEqual(
buildElectronBuilderMetadata(packageMetadata, electronBuilderConfig)
.extraMetadata,
{
name: packageMetadata.name,
productName: electronBuilderConfig.productName,
version: packageMetadata.version,
description: packageMetadata.description,
author: packageMetadata.author,
homepage: packageMetadata.homepage,
license: packageMetadata.license,
main: electronBuilderConfig.extraMetadata.main,
}
);
assert.deepEqual(generatedElectronPackage, {
name: packageMetadata.name,
productName: electronBuilderConfig.productName,
version: packageMetadata.version,
description: packageMetadata.description,
author: packageMetadata.author,
homepage: packageMetadata.homepage,
license: packageMetadata.license,
main: electronBuilderConfig.extraMetadata.main,
dependencies: {
'better-sqlite3': '12.5.0',
},
});
});
test('nx-electron packaging prepares metadata before make/package', () => {
assert.equal(makerOptions.extends, generatedMetadataConfigPath);
assert.deepEqual(
electronProjectConfig.targets['generate-builder-metadata'].dependsOn,
['electron-backend:build']
);
assert.deepEqual(
electronProjectConfig.targets['generate-builder-metadata'].outputs,
[
`{workspaceRoot}/${generatedMetadataConfigPath}`,
'{workspaceRoot}/dist/apps/electron-backend/package.json',
]
);
assert.ok(
electronProjectConfig.targets.make.dependsOn.includes(
'electron-backend:generate-builder-metadata'
)
);
assert.ok(
electronProjectConfig.targets.package.dependsOn.includes(
'electron-backend:generate-builder-metadata'
)
);
});
test('package layout verifier uses canonical helpers and direct dependencies', () => {
assert.ok(packageMetadata.devDependencies?.['@electron/asar']);
assert.match(packageLayoutVerifier, /require\(['"]@electron\/asar['"]\)/);
assert.doesNotMatch(packageLayoutVerifier, /electronBuilderRequire/);
assert.match(
packageLayoutVerifier,
/buildElectronBuilderMetadata\(\s*packageMetadata,\s*electronBuilderConfig\s*\)\.extraMetadata/s
);
assert.doesNotMatch(
packageLayoutVerifier,
/const packagedPackageMetadata = \{\s*name:/s
);
assert.doesNotMatch(
packageLayoutVerifier,
/builderEffectiveConfigPath && fileExists\(builderEffectiveConfigPath\)/
);
});
test('nx-electron packaging does not copy duplicate root package metadata', () => {
const nxElectronExecutorPath =
require.resolve('nx-electron/src/executors/package/executor.js');
const nxElectronExecutor = fs.readFileSync(nxElectronExecutorPath, 'utf8');
assert.doesNotMatch(nxElectronExecutor, /['"]\.\/package\.json['"]/);
assert.match(
nxElectronExecutor,
/filter:\s*\[['"]index\.js['"],\s*['"]package\.json['"]\]/
);
});
test('embedded MPV runtime binaries are unpacked on every supported desktop platform', () => {
const asarUnpack = electronBuilderConfig.asarUnpack ?? [];
for (const requiredPattern of [
'**/*.node',
'**/*.dylib',
'**/*.dll',
'**/*.so',
'**/*.so.*',
'**/embedded-mpv-runtime.json',
]) {
assert.ok(
asarUnpack.includes(requiredPattern),
`electron-builder asarUnpack must include ${requiredPattern}`
);
}
});
test('embedded MPV package validation accepts Windows runtime files and Linux process isolation', () => {
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
try {
for (const [platform, runtimeFile] of [
['windows', 'mpv-2.dll'],
['windows', 'libmpv-2.dll'],
['windows', join('lib', 'mpv.dll')],
['windows', join('lib', 'libmpv.dll')],
]) {
const resourceDir = join(tempDir, platform);
const nativeDir = join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true });
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
fs.writeFileSync(
join(nativeDir, 'embedded-mpv-runtime.json'),
JSON.stringify({ origin: 'vendored-lgpl' })
);
fs.writeFileSync(join(nativeDir, runtimeFile), '');
assert.deepEqual(
validatePackagedEmbeddedMpv(resourceDir, {
platform,
required: true,
}),
[]
);
}
const darwinResourceDir = join(tempDir, 'darwin');
const darwinNativeDir = join(
darwinResourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(join(darwinNativeDir, 'lib'), { recursive: true });
fs.writeFileSync(join(darwinNativeDir, 'embedded_mpv.node'), '');
fs.writeFileSync(
join(darwinNativeDir, 'embedded-mpv-runtime.json'),
JSON.stringify({ origin: 'vendored-lgpl' })
);
fs.writeFileSync(join(darwinNativeDir, 'lib', 'libmpv.2.dylib'), '');
// macOS packages that ship the addon must also ship the frame-copy
// engine artifacts built by the same binding.gyp run.
const missingFrameCopyErrors = validatePackagedEmbeddedMpv(
darwinResourceDir,
{ platform: 'darwin', required: true }
);
assert.ok(
missingFrameCopyErrors.some((error) =>
error.includes('iptvnator_mpv_helper')
)
);
assert.ok(
missingFrameCopyErrors.some((error) =>
error.includes('embedded_mpv_frame_reader.node')
)
);
fs.writeFileSync(join(darwinNativeDir, 'iptvnator_mpv_helper'), '');
fs.writeFileSync(
join(darwinNativeDir, 'embedded_mpv_frame_reader.node'),
''
);
// Host-agnostic assertion: on non-macOS hosts the validator also
// reports that link validation needs a macOS host, so only the
// frame-copy artifact requirement is asserted here.
const remainingErrors = validatePackagedEmbeddedMpv(
darwinResourceDir,
{ platform: 'darwin', required: true }
);
assert.ok(
!remainingErrors.some((error) =>
error.includes('frame-copy artifact')
),
`unexpected frame-copy errors: ${remainingErrors.join('; ')}`
);
const linuxResourceDir = join(tempDir, 'linux');
const linuxNativeDir = join(
linuxResourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(linuxNativeDir, { recursive: true });
fs.writeFileSync(join(linuxNativeDir, 'embedded_mpv.node'), '');
fs.writeFileSync(
join(linuxNativeDir, 'embedded-mpv-runtime.json'),
JSON.stringify({ origin: 'external-mpv-process' })
);
assert.deepEqual(
validatePackagedEmbeddedMpv(linuxResourceDir, {
platform: 'linux',
required: true,
}),
[]
);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test('Windows embedded MPV staging preserves import-library DLL basenames', () => {
assert.match(
embeddedMpvStageRuntimeSource,
/win32:\s*\[\s*'mpv-2\.dll',\s*'libmpv-2\.dll',\s*'mpv\.dll',\s*'libmpv\.dll'\s*\]/
);
assert.match(
embeddedMpvWindowsArchiveStageSource,
/path\.join\(normalizedPrefix,\s*'bin',\s*path\.basename\(runtimeDll\)\)/
);
assert.doesNotMatch(
embeddedMpvWindowsArchiveStageSource,
/normalizedWindowsDllName/
);
assert.match(embeddedMpvBuildSource, /'libmpv-2\.dll'/);
assert.match(embeddedMpvPackagingSource, /libmpv-2\.dll/);
});
test('Windows embedded MPV archive staging keeps CI downloads bounded and quiet', () => {
assert.match(
embeddedMpvWindowsArchiveStageSource,
/parsedUrl\.protocol === 'https:'/
);
assert.doesNotMatch(
embeddedMpvWindowsArchiveStageSource,
/parsedUrl\.protocol === 'http:'/
);
assert.match(
embeddedMpvWindowsArchiveStageSource,
/fs\.createReadStream\(filePath\)/
);
assert.doesNotMatch(
embeddedMpvWindowsArchiveStageSource,
/hash\.update\(fs\.readFileSync\(filePath\)\)/
);
assert.match(
embeddedMpvWindowsArchiveStageSource,
/runResult\(\s*'tar',\s*\['-xf', archivePath, '-C', extractRoot\],\s*\{\s*stdio: 'pipe',\s*\}\s*\)/s
);
});
test('embedded MPV packaging helpers use a cross-platform module name', () => {
assert.match(
electronAfterPackSource,
/require\(['"]\.\/embedded-mpv-packaging\.cjs['"]\)/
);
assert.match(
packageLayoutVerifier,
/require\(['"]\.\/embedded-mpv-packaging\.cjs['"]\)/
);
});
test('frame-copy packaging file operations enforce modes and remove stale artifacts', () => {
assert.ok(
fs.existsSync(frameCopyFilesModulePath),
'shared frame-copy packaging file helper must exist'
);
const {
preparePackagedFrameCopyArtifacts,
removeStaleFrameCopyArtifacts,
} = require(frameCopyFilesModulePath);
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'impv-fc-files-'));
try {
const helperPath = join(tempDir, 'iptvnator_mpv_helper');
const windowsHelperPath = join(
tempDir,
'iptvnator_mpv_helper.exe'
);
const readerPath = join(tempDir, 'embedded_mpv_frame_reader.node');
fs.writeFileSync(helperPath, '#!/bin/sh\n');
fs.chmodSync(helperPath, 0o644);
fs.writeFileSync(windowsHelperPath, 'exe');
fs.writeFileSync(readerPath, 'reader');
if (process.platform !== 'win32') {
preparePackagedFrameCopyArtifacts(tempDir, 'darwin');
assert.notEqual(
fs.statSync(helperPath).mode & 0o111,
0,
'macOS helper must be executable after packaging'
);
}
preparePackagedFrameCopyArtifacts(tempDir, 'linux');
assert.equal(
fs.existsSync(helperPath),
false,
'Linux packages must omit the unsupported frame-copy helper'
);
assert.equal(
fs.existsSync(windowsHelperPath),
false,
'Linux packages must omit stale Windows frame-copy helpers too'
);
fs.writeFileSync(helperPath, '#!/bin/sh\n');
removeStaleFrameCopyArtifacts(tempDir);
assert.equal(fs.existsSync(helperPath), false);
assert.equal(fs.existsSync(windowsHelperPath), false);
assert.equal(fs.existsSync(readerPath), false);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test('Windows CI packages embedded MPV from a staged x64 runtime', () => {
const requireEmbeddedMpvLines = buildAndMakeWorkflow
.split(/\r?\n/)
.filter((line) => line.includes('IPTVNATOR_REQUIRE_EMBEDDED_MPV:'));
assert.equal(
packageMetadata.scripts?.['embedded-mpv:stage-runtime:windows-archive'],
'node tools/embedded-mpv/stage-windows-runtime-archive.mjs'
);
assert.match(
buildAndMakeWorkflow,
/name:\s+Stage Windows embedded MPV runtime archive/
);
assert.match(buildAndMakeWorkflow, /runner:\s+windows-2022/);
assert.match(
buildAndMakeWorkflow,
/IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_URL/
);
assert.match(
buildAndMakeWorkflow,
/IPTVNATOR_WINDOWS_EMBEDDED_MPV_RUNTIME_SHA256/
);
assert.match(
buildAndMakeWorkflow,
/IPTVNATOR_DEFAULT_WINDOWS_EMBEDDED_MPV_RUNTIME_URL: https:\/\/github\.com\/zhongfly\/mpv-winbuild\/releases\/download\//
);
assert.match(buildAndMakeWorkflow, /refs\/tags\/v\*/);
assert.match(
buildAndMakeWorkflow,
/name:\s+Override Windows arch in electron-builder\.json/
);
assert.match(
buildAndMakeWorkflow,
/IPTVNATOR_REQUIRE_EMBEDDED_MPV:\s+\$\{\{\s*\(matrix\.os == 'linux' \|\| matrix\.os == 'windows'/
);
assert.match(embeddedMpvStageRuntimeSource, /\.dll\.a/);
assert.match(embeddedMpvBuildSource, /\.dll\.a/);
assert.ok(requireEmbeddedMpvLines.length > 0);
for (const line of requireEmbeddedMpvLines) {
assert.doesNotMatch(line, /cache-hit/);
}
});
test('Windows embedded MPV native build uses wide Win32 cursor resources', () => {
assert.match(
embeddedMpvWin32Source,
/LoadCursorW\(nullptr,\s*MAKEINTRESOURCEW\(32512\)\)/
);
assert.doesNotMatch(
embeddedMpvWin32Source,
/LoadCursorW\(nullptr,\s*IDC_ARROW\)/
);
});
test('embedded MPV package validation rejects missing required Windows runtime', () => {
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
try {
const nativeDir = join(
tempDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true });
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
fs.writeFileSync(
join(nativeDir, 'embedded-mpv-runtime.json'),
JSON.stringify({ origin: 'vendored-lgpl' })
);
const errors = validatePackagedEmbeddedMpv(tempDir, {
platform: 'windows',
required: true,
});
assert.match(errors.join('\n'), /Missing bundled embedded MPV runtime/);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test('embedded MPV package validation rejects bundled Linux libmpv', () => {
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
try {
const nativeDir = join(
tempDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(join(nativeDir, 'lib'), { recursive: true });
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
fs.writeFileSync(
join(nativeDir, 'embedded-mpv-runtime.json'),
JSON.stringify({ origin: 'external-mpv-process' })
);
fs.writeFileSync(join(nativeDir, 'lib', 'libmpv.so'), '');
const errors = validatePackagedEmbeddedMpv(tempDir, {
platform: 'linux',
required: true,
});
assert.match(errors.join('\n'), /must not bundle libmpv/);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test('embedded MPV package validation rejects frame-copy helpers in Linux packages', () => {
const tempDir = fs.mkdtempSync(join(os.tmpdir(), 'iptvnator-mpv-package-'));
try {
const nativeDir = join(
tempDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.mkdirSync(nativeDir, { recursive: true });
fs.writeFileSync(join(nativeDir, 'embedded_mpv.node'), '');
fs.writeFileSync(
join(nativeDir, 'embedded-mpv-runtime.json'),
JSON.stringify({ origin: 'external-mpv-process' })
);
fs.writeFileSync(join(nativeDir, 'iptvnator_mpv_helper'), '');
fs.writeFileSync(join(nativeDir, 'iptvnator_mpv_helper.exe'), '');
const errors = validatePackagedEmbeddedMpv(tempDir, {
platform: 'linux',
required: true,
});
const message = errors.join('\n');
assert.match(message, /must not ship frame-copy helpers/);
assert.match(message, /iptvnator_mpv_helper\n/);
assert.match(message, /iptvnator_mpv_helper\.exe/);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});