mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* 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>
662 lines
19 KiB
JavaScript
662 lines
19 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const { spawnSync } = require('child_process');
|
|
|
|
const forbiddenRuntimePathPrefixes = ['/opt/homebrew/', '/usr/local/'];
|
|
const systemRuntimePathPrefixes = ['/System/Library/', '/usr/lib/'];
|
|
|
|
function run(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
stdio: options.stdio ?? 'pipe',
|
|
encoding: 'utf8',
|
|
...options,
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
const stderr = result.stderr ? `\n${result.stderr}` : '';
|
|
throw new Error(
|
|
`${command} ${args.join(' ')} failed with status ${result.status ?? 1}.${stderr}`
|
|
);
|
|
}
|
|
|
|
return result.stdout ?? '';
|
|
}
|
|
|
|
function commandExists(command) {
|
|
const result = spawnSync('sh', ['-lc', `command -v ${command}`], {
|
|
stdio: 'ignore',
|
|
});
|
|
return result.status === 0;
|
|
}
|
|
|
|
function ensureDir(directoryPath) {
|
|
fs.mkdirSync(directoryPath, { recursive: true });
|
|
}
|
|
|
|
function removeDir(directoryPath) {
|
|
fs.rmSync(directoryPath, { recursive: true, force: true });
|
|
}
|
|
|
|
function copyFile(sourcePath, destinationPath) {
|
|
ensureDir(path.dirname(destinationPath));
|
|
fs.copyFileSync(sourcePath, destinationPath);
|
|
fs.chmodSync(destinationPath, 0o755);
|
|
}
|
|
|
|
function listDylibs(directoryPath) {
|
|
if (!fs.existsSync(directoryPath)) {
|
|
return [];
|
|
}
|
|
|
|
return fs
|
|
.readdirSync(directoryPath, { withFileTypes: true })
|
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.dylib'))
|
|
.map((entry) => path.join(directoryPath, entry.name))
|
|
.sort();
|
|
}
|
|
|
|
function listRuntimeFiles(directoryPath) {
|
|
if (!fs.existsSync(directoryPath)) {
|
|
return [];
|
|
}
|
|
|
|
return fs
|
|
.readdirSync(directoryPath, { withFileTypes: true })
|
|
.filter((entry) => entry.isFile())
|
|
.map((entry) => path.join(directoryPath, entry.name))
|
|
.sort();
|
|
}
|
|
|
|
function parseOtoolDependencies(binaryPath) {
|
|
const output = run('otool', ['-L', binaryPath]);
|
|
return output
|
|
.split(/\r?\n/)
|
|
.slice(1)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.map((line) => line.split(/\s+\(/)[0])
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function parseOtoolRpaths(binaryPath) {
|
|
const output = run('otool', ['-l', binaryPath]);
|
|
const lines = output.split(/\r?\n/);
|
|
const rpaths = [];
|
|
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
if (lines[index].trim() !== 'cmd LC_RPATH') {
|
|
continue;
|
|
}
|
|
|
|
for (let offset = index + 1; offset < lines.length; offset += 1) {
|
|
const match = lines[offset]
|
|
.trim()
|
|
.match(/^path\s+(.+?)\s+\(offset\s+\d+\)$/);
|
|
if (match) {
|
|
rpaths.push(match[1]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return rpaths;
|
|
}
|
|
|
|
function readInstallNameId(binaryPath) {
|
|
const result = spawnSync('otool', ['-D', binaryPath], {
|
|
stdio: 'pipe',
|
|
encoding: 'utf8',
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
result.stdout
|
|
.split(/\r?\n/)
|
|
.slice(1)
|
|
.map((line) => line.trim())
|
|
.find(Boolean) ?? null
|
|
);
|
|
}
|
|
|
|
function isSystemDependency(dependencyPath) {
|
|
return systemRuntimePathPrefixes.some((prefix) =>
|
|
dependencyPath.startsWith(prefix)
|
|
);
|
|
}
|
|
|
|
function isForbiddenDependency(dependencyPath) {
|
|
return forbiddenRuntimePathPrefixes.some((prefix) =>
|
|
dependencyPath.startsWith(prefix)
|
|
);
|
|
}
|
|
|
|
function resolvePathToken(tokenPath, binaryPath) {
|
|
if (tokenPath.startsWith('@loader_path/')) {
|
|
return path.resolve(
|
|
path.dirname(binaryPath),
|
|
tokenPath.replace('@loader_path/', '')
|
|
);
|
|
}
|
|
|
|
if (tokenPath.startsWith('@executable_path/')) {
|
|
return null;
|
|
}
|
|
|
|
if (tokenPath.startsWith('@rpath/')) {
|
|
return null;
|
|
}
|
|
|
|
return tokenPath;
|
|
}
|
|
|
|
function resolveRpathDependency(dependencyPath, binaryPath) {
|
|
const dependencyName = dependencyPath.replace('@rpath/', '');
|
|
|
|
for (const rpath of parseOtoolRpaths(binaryPath)) {
|
|
const resolvedRpath = resolvePathToken(rpath, binaryPath);
|
|
if (!resolvedRpath) {
|
|
continue;
|
|
}
|
|
|
|
const candidatePath = path.resolve(resolvedRpath, dependencyName);
|
|
if (fs.existsSync(candidatePath)) {
|
|
return candidatePath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resolveDependencyPath(dependencyPath, binaryPath) {
|
|
if (dependencyPath.startsWith('@loader_path/')) {
|
|
return resolvePathToken(dependencyPath, binaryPath);
|
|
}
|
|
|
|
if (dependencyPath.startsWith('@rpath/')) {
|
|
return resolveRpathDependency(dependencyPath, binaryPath);
|
|
}
|
|
|
|
if (dependencyPath.startsWith('@executable_path/')) {
|
|
return null;
|
|
}
|
|
|
|
return dependencyPath;
|
|
}
|
|
|
|
function collectExternalDylibs(entryPaths) {
|
|
const visited = new Set();
|
|
const queue = [...entryPaths];
|
|
const result = new Map();
|
|
|
|
while (queue.length > 0) {
|
|
const currentPath = queue.shift();
|
|
if (
|
|
!currentPath ||
|
|
visited.has(currentPath) ||
|
|
!fs.existsSync(currentPath)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
visited.add(currentPath);
|
|
for (const dependencyPath of parseOtoolDependencies(currentPath)) {
|
|
if (isSystemDependency(dependencyPath)) {
|
|
continue;
|
|
}
|
|
|
|
const resolvedPath = resolveDependencyPath(
|
|
dependencyPath,
|
|
currentPath
|
|
);
|
|
if (!resolvedPath || !fs.existsSync(resolvedPath)) {
|
|
continue;
|
|
}
|
|
|
|
if (!result.has(path.basename(resolvedPath))) {
|
|
result.set(path.basename(resolvedPath), resolvedPath);
|
|
queue.push(resolvedPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...result.values()];
|
|
}
|
|
|
|
function findLibMpv(runtimeLibDir) {
|
|
const candidates = ['libmpv.2.dylib', 'libmpv.dylib'];
|
|
for (const candidate of candidates) {
|
|
const candidatePath = path.join(runtimeLibDir, candidate);
|
|
if (fs.existsSync(candidatePath)) {
|
|
return candidatePath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function patchDylibIds(libDir) {
|
|
for (const dylibPath of listRuntimeFiles(libDir)) {
|
|
if (!readInstallNameId(dylibPath)) {
|
|
continue;
|
|
}
|
|
|
|
run('install_name_tool', [
|
|
'-id',
|
|
`@loader_path/${path.basename(dylibPath)}`,
|
|
dylibPath,
|
|
]);
|
|
}
|
|
}
|
|
|
|
function patchBinaryDependencies(
|
|
binaryPath,
|
|
dependencyBaseDir,
|
|
replacementPrefix
|
|
) {
|
|
const availableRuntimeFiles = new Set(
|
|
listRuntimeFiles(dependencyBaseDir).map((runtimePath) =>
|
|
path.basename(runtimePath)
|
|
)
|
|
);
|
|
|
|
for (const dependencyPath of parseOtoolDependencies(binaryPath)) {
|
|
const dependencyName = path.basename(dependencyPath);
|
|
if (!availableRuntimeFiles.has(dependencyName)) {
|
|
continue;
|
|
}
|
|
|
|
const replacementPath = `${replacementPrefix}/${dependencyName}`;
|
|
if (dependencyPath === replacementPath) {
|
|
continue;
|
|
}
|
|
|
|
run('install_name_tool', [
|
|
'-change',
|
|
dependencyPath,
|
|
replacementPath,
|
|
binaryPath,
|
|
]);
|
|
}
|
|
}
|
|
|
|
function adHocSignBinary(binaryPath) {
|
|
if (process.platform !== 'darwin' || !commandExists('codesign')) {
|
|
return;
|
|
}
|
|
|
|
run('codesign', ['--force', '--sign', '-', '--timestamp=none', binaryPath]);
|
|
}
|
|
|
|
function adHocSignBinaries(binaryPaths) {
|
|
for (const binaryPath of binaryPaths) {
|
|
adHocSignBinary(binaryPath);
|
|
}
|
|
}
|
|
|
|
function copyRuntimeToNativeBuild({
|
|
runtimeLibDir,
|
|
outputLibDir,
|
|
runtimeOrigin,
|
|
runtimeManifest,
|
|
}) {
|
|
const libMpvPath = findLibMpv(runtimeLibDir);
|
|
if (!libMpvPath) {
|
|
throw new Error(`Unable to find libmpv in ${runtimeLibDir}.`);
|
|
}
|
|
|
|
removeDir(outputLibDir);
|
|
ensureDir(outputLibDir);
|
|
|
|
const runtimeFilesByName = new Map([
|
|
[path.basename(libMpvPath), libMpvPath],
|
|
]);
|
|
for (const dylibPath of collectExternalDylibs([libMpvPath])) {
|
|
runtimeFilesByName.set(path.basename(dylibPath), dylibPath);
|
|
}
|
|
if (runtimeOrigin === 'vendored-lgpl') {
|
|
for (const runtimePath of listRuntimeFiles(runtimeLibDir)) {
|
|
runtimeFilesByName.set(path.basename(runtimePath), runtimePath);
|
|
}
|
|
}
|
|
|
|
const dylibs = [...runtimeFilesByName.values()];
|
|
for (const dylibPath of dylibs) {
|
|
copyFile(dylibPath, path.join(outputLibDir, path.basename(dylibPath)));
|
|
}
|
|
|
|
if (!fs.existsSync(path.join(outputLibDir, 'libmpv.2.dylib'))) {
|
|
const copiedLibMpvPath = path.join(
|
|
outputLibDir,
|
|
path.basename(libMpvPath)
|
|
);
|
|
if (path.basename(copiedLibMpvPath) !== 'libmpv.2.dylib') {
|
|
copyFile(
|
|
copiedLibMpvPath,
|
|
path.join(outputLibDir, 'libmpv.2.dylib')
|
|
);
|
|
}
|
|
}
|
|
|
|
patchDylibIds(outputLibDir);
|
|
for (const runtimePath of listRuntimeFiles(outputLibDir)) {
|
|
patchBinaryDependencies(runtimePath, outputLibDir, '@loader_path');
|
|
}
|
|
adHocSignBinaries(listRuntimeFiles(outputLibDir));
|
|
|
|
const manifest = {
|
|
origin: runtimeOrigin,
|
|
generatedAt: new Date().toISOString(),
|
|
libDir: 'lib',
|
|
runtimeFiles: listRuntimeFiles(outputLibDir).map((runtimePath) =>
|
|
path.basename(runtimePath)
|
|
),
|
|
dylibs: listDylibs(outputLibDir).map((dylibPath) =>
|
|
path.basename(dylibPath)
|
|
),
|
|
...runtimeManifest,
|
|
};
|
|
|
|
fs.writeFileSync(
|
|
path.join(path.dirname(outputLibDir), 'embedded-mpv-runtime.json'),
|
|
`${JSON.stringify(manifest, null, 2)}\n`
|
|
);
|
|
|
|
return manifest;
|
|
}
|
|
|
|
function patchAddonForBundledRuntime(addonPath, outputLibDir) {
|
|
if (!fs.existsSync(addonPath)) {
|
|
throw new Error(`Missing embedded MPV native addon: ${addonPath}`);
|
|
}
|
|
|
|
patchBinaryDependencies(addonPath, outputLibDir, '@loader_path/lib');
|
|
adHocSignBinary(addonPath);
|
|
}
|
|
|
|
function validateNoForbiddenRuntimeLinks(binaryPaths) {
|
|
const errors = [];
|
|
|
|
for (const binaryPath of binaryPaths) {
|
|
if (!fs.existsSync(binaryPath)) {
|
|
errors.push(`Missing binary: ${binaryPath}`);
|
|
continue;
|
|
}
|
|
|
|
for (const dependencyPath of parseOtoolDependencies(binaryPath)) {
|
|
if (isForbiddenDependency(dependencyPath)) {
|
|
errors.push(
|
|
`${binaryPath} links to forbidden runtime path: ${dependencyPath}`
|
|
);
|
|
}
|
|
|
|
if (dependencyPath.startsWith('@loader_path/')) {
|
|
const resolvedPath = path.resolve(
|
|
path.dirname(binaryPath),
|
|
dependencyPath.replace('@loader_path/', '')
|
|
);
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
errors.push(
|
|
`${binaryPath} links to missing bundled dependency: ${dependencyPath}`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (
|
|
!isSystemDependency(dependencyPath) &&
|
|
!dependencyPath.startsWith('@loader_path/')
|
|
) {
|
|
errors.push(
|
|
`${binaryPath} links to non-bundled runtime path: ${dependencyPath}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
function getPackagedRuntimeCandidates(libDir, platform, nativeDir) {
|
|
switch (platform) {
|
|
case 'darwin':
|
|
return [
|
|
path.join(libDir, 'libmpv.2.dylib'),
|
|
path.join(libDir, 'libmpv.dylib'),
|
|
];
|
|
case 'win32':
|
|
return [
|
|
nativeDir ? path.join(nativeDir, 'mpv-2.dll') : null,
|
|
nativeDir ? path.join(nativeDir, 'libmpv-2.dll') : null,
|
|
nativeDir ? path.join(nativeDir, 'mpv.dll') : null,
|
|
nativeDir ? path.join(nativeDir, 'libmpv.dll') : null,
|
|
path.join(libDir, 'mpv-2.dll'),
|
|
path.join(libDir, 'libmpv-2.dll'),
|
|
path.join(libDir, 'mpv.dll'),
|
|
path.join(libDir, 'libmpv.dll'),
|
|
].filter(Boolean);
|
|
case 'linux':
|
|
return [];
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function normalizeEmbeddedMpvPlatform(value) {
|
|
if (value === 'macos') {
|
|
return 'darwin';
|
|
}
|
|
if (value === 'windows') {
|
|
return 'win32';
|
|
}
|
|
return value ?? process.platform;
|
|
}
|
|
|
|
// electron-builder passes `arch` to afterPack as a builder-util Arch enum
|
|
// value; this table mirrors that enum's declaration order.
|
|
const ELECTRON_BUILDER_ARCH_NAMES = [
|
|
'ia32',
|
|
'x64',
|
|
'armv7l',
|
|
'arm64',
|
|
'universal',
|
|
];
|
|
|
|
function resolveElectronBuilderArchName(arch) {
|
|
if (typeof arch === 'string') {
|
|
return arch;
|
|
}
|
|
return ELECTRON_BUILDER_ARCH_NAMES[arch] ?? null;
|
|
}
|
|
|
|
function getEmbeddedMpvAddonArch(env = process.env) {
|
|
return env.IPTVNATOR_EMBEDDED_MPV_ARCH || process.arch;
|
|
}
|
|
|
|
/**
|
|
* The embedded MPV addon is compiled once per CI host (x64 on Linux), but
|
|
* electron-builder also produces arm64/armv7l Linux packages from the same
|
|
* dist output. Those packages must not ship a foreign-architecture
|
|
* `embedded_mpv.node` — it can never load and produces a cryptic error.
|
|
*/
|
|
function isForeignLinuxEmbeddedMpvArch(platform, targetArch, env = process.env) {
|
|
if (normalizeEmbeddedMpvPlatform(platform) !== 'linux') {
|
|
return false;
|
|
}
|
|
const archName = resolveElectronBuilderArchName(targetArch);
|
|
return Boolean(archName) && archName !== getEmbeddedMpvAddonArch(env);
|
|
}
|
|
|
|
// Maps electron-builder Linux output directory names (`linux-unpacked`,
|
|
// `linux-arm64-unpacked`, ...) to the package architecture. `null` when the
|
|
// name is not a Linux unpacked directory.
|
|
function linuxUnpackedDirArch(unpackedDirName) {
|
|
const match = /^linux-(?:([a-z0-9_]+)-)?unpacked$/.exec(unpackedDirName);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
return match[1] ?? 'x64';
|
|
}
|
|
|
|
function validatePackagedEmbeddedMpv(resourceDir, options = {}) {
|
|
const platform = normalizeEmbeddedMpvPlatform(options.platform);
|
|
const unpackedNativeDir = path.join(
|
|
resourceDir,
|
|
'app.asar.unpacked',
|
|
'electron-backend',
|
|
'native'
|
|
);
|
|
const addonPath = path.join(unpackedNativeDir, 'embedded_mpv.node');
|
|
const libDir = path.join(unpackedNativeDir, 'lib');
|
|
const manifestPath = path.join(
|
|
unpackedNativeDir,
|
|
'embedded-mpv-runtime.json'
|
|
);
|
|
const errors = [];
|
|
|
|
if (options.foreignArch) {
|
|
if (fs.existsSync(addonPath)) {
|
|
errors.push(
|
|
`Embedded MPV addon must not ship in foreign-architecture Linux packages: ${addonPath}`
|
|
);
|
|
}
|
|
const markerPath = path.join(
|
|
unpackedNativeDir,
|
|
'embedded-mpv-unavailable.txt'
|
|
);
|
|
if (!fs.existsSync(markerPath)) {
|
|
errors.push(
|
|
`Missing embedded MPV unavailable marker for foreign-architecture package: ${markerPath}`
|
|
);
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
if (!fs.existsSync(addonPath)) {
|
|
if (options.required) {
|
|
errors.push(`Missing embedded MPV native addon: ${addonPath}`);
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
if (platform === 'darwin') {
|
|
// The frame-copy engine artifacts are built by the same binding.gyp
|
|
// run as the addon; a macOS package that ships the addon without
|
|
// them would silently lose the engine (support probe hides it).
|
|
const missingFrameCopyArtifacts = [
|
|
'iptvnator_mpv_helper',
|
|
'embedded_mpv_frame_reader.node',
|
|
]
|
|
.map((name) => path.join(unpackedNativeDir, name))
|
|
.filter((artifactPath) => !fs.existsSync(artifactPath));
|
|
errors.push(
|
|
...missingFrameCopyArtifacts.map(
|
|
(artifactPath) =>
|
|
`Missing embedded MPV frame-copy artifact: ${artifactPath}`
|
|
)
|
|
);
|
|
}
|
|
|
|
if (!fs.existsSync(manifestPath)) {
|
|
errors.push(`Missing embedded MPV runtime manifest: ${manifestPath}`);
|
|
} else {
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
const expectedOrigin =
|
|
platform === 'linux' ? 'external-mpv-process' : 'vendored-lgpl';
|
|
if (manifest.origin !== expectedOrigin) {
|
|
errors.push(
|
|
`Embedded MPV packaged runtime must be ${expectedOrigin}, received: ${manifest.origin}`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (platform === 'linux') {
|
|
const packagedFrameCopyHelpers = [
|
|
path.join(unpackedNativeDir, 'iptvnator_mpv_helper'),
|
|
path.join(unpackedNativeDir, 'iptvnator_mpv_helper.exe'),
|
|
].filter((candidate) => fs.existsSync(candidate));
|
|
if (packagedFrameCopyHelpers.length > 0) {
|
|
errors.push(
|
|
[
|
|
'Linux packages must not ship frame-copy helpers linked against the build host system libmpv.',
|
|
'Remove:',
|
|
...packagedFrameCopyHelpers.map(
|
|
(candidate) => `- ${candidate}`
|
|
),
|
|
].join('\n')
|
|
);
|
|
}
|
|
|
|
const bundledLinuxRuntime = [
|
|
path.join(libDir, 'libmpv.so.2'),
|
|
path.join(libDir, 'libmpv.so.1'),
|
|
path.join(libDir, 'libmpv.so'),
|
|
].filter((candidate) => fs.existsSync(candidate));
|
|
|
|
if (bundledLinuxRuntime.length > 0) {
|
|
errors.push(
|
|
[
|
|
'Linux embedded MPV must use the external mpv process backend and must not bundle libmpv.',
|
|
'Remove:',
|
|
...bundledLinuxRuntime.map((candidate) => `- ${candidate}`),
|
|
].join('\n')
|
|
);
|
|
}
|
|
}
|
|
|
|
const runtimeCandidates = getPackagedRuntimeCandidates(
|
|
libDir,
|
|
platform,
|
|
unpackedNativeDir
|
|
);
|
|
if (
|
|
runtimeCandidates.length > 0 &&
|
|
!runtimeCandidates.some((candidate) => fs.existsSync(candidate))
|
|
) {
|
|
errors.push(
|
|
[
|
|
`Missing bundled embedded MPV runtime for ${platform} in ${libDir}.`,
|
|
'Expected one of:',
|
|
...runtimeCandidates.map((candidate) => `- ${candidate}`),
|
|
].join('\n')
|
|
);
|
|
}
|
|
|
|
if (platform === 'darwin') {
|
|
if (process.platform !== 'darwin') {
|
|
errors.push(
|
|
'macOS embedded MPV link validation must run on a macOS host.'
|
|
);
|
|
} else if (!commandExists('otool')) {
|
|
errors.push(
|
|
'otool is required to validate embedded MPV macOS packaging.'
|
|
);
|
|
} else {
|
|
const binaries = [addonPath, ...listRuntimeFiles(libDir)];
|
|
errors.push(...validateNoForbiddenRuntimeLinks(binaries));
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
module.exports = {
|
|
collectExternalDylibs,
|
|
commandExists,
|
|
copyRuntimeToNativeBuild,
|
|
findLibMpv,
|
|
listRuntimeFiles,
|
|
listDylibs,
|
|
parseOtoolDependencies,
|
|
parseOtoolRpaths,
|
|
patchAddonForBundledRuntime,
|
|
validateNoForbiddenRuntimeLinks,
|
|
getPackagedRuntimeCandidates,
|
|
validatePackagedEmbeddedMpv,
|
|
getEmbeddedMpvAddonArch,
|
|
isForeignLinuxEmbeddedMpvArch,
|
|
linuxUnpackedDirArch,
|
|
resolveElectronBuilderArchName,
|
|
};
|