Files
iptvnator/tools/packaging/embedded-mpv-packaging.cjs
4gray c266eaa680 fix(packaging): preserve Flatpak Electron ELF for Zypak (#1205)
Fixes the launcher/Zypak regression reported in #1203. The additional GPU/video.js behavior remains tracked separately in that issue.
2026-07-18 22:42:45 +02:00

1989 lines
62 KiB
JavaScript

const crypto = require('node:crypto');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const { isDeepStrictEqual } = require('node:util');
const {
EXTERNAL_SYSTEM_LIBRARIES,
GLIBC_TOOLCHAIN_ALLOWLIST,
parseReadelfDynamic,
validateRuntimeDependencyClosure,
} = require('../embedded-mpv/build-linux-runtime.cjs');
const {
validateLinuxRuntimeManifest,
} = require('../embedded-mpv/linux-runtime-manifest.cjs');
const {
validateLinuxSourceArchiveBinding,
} = require('../embedded-mpv/linux-source-archive-contract.cjs');
const {
NOTICE_MANIFEST,
THIRD_PARTY_NOTICES,
validateLinuxRuntimeNotices,
} = require('../embedded-mpv/generate-linux-runtime-notices.cjs');
const {
LINUX_SYSTEM_PACKAGE_DEPENDENCIES,
resolveLinuxFrameCopyProfile,
validateLinuxProfileTargets,
} = require('./linux-frame-copy-profile.cjs');
const { resolveLinuxLauncherLayout } = require('./linux-launcher-layout.cjs');
const forbiddenRuntimePathPrefixes = ['/opt/homebrew/', '/usr/local/'];
const systemRuntimePathPrefixes = ['/System/Library/', '/usr/lib/'];
const windowsMpvRuntimeNames = [
'mpv-2.dll',
'libmpv-2.dll',
'mpv.dll',
'libmpv.dll',
];
const linuxFrameCopyArtifacts = Object.freeze({
addon: Object.freeze({
name: 'embedded_mpv.node',
regularFile: true,
readable: true,
}),
frameReader: Object.freeze({
name: 'embedded_mpv_frame_reader.node',
regularFile: true,
readable: true,
}),
helper: Object.freeze({
name: 'iptvnator_mpv_helper',
regularFile: true,
readable: true,
executable: true,
}),
});
const linuxFrameCopyProcessIsolation = Object.freeze({
addonLoadsLibmpv: false,
readerLoadsLibmpv: false,
electronLoadsLibmpv: false,
helperLinksLibmpv: true,
helperRunpath: Object.freeze(['$ORIGIN/lib']),
});
const linuxNativeViewFallback = 'process-isolated mpv --wid';
const versionedLinuxLibmpvPattern = /^libmpv\.so\.\d+(?:\.\d+)*$/;
const anyLinuxLibmpvPattern = /^libmpv\.so(?:\.|$)/;
const linuxRuntimeLegalPaths = Object.freeze([
NOTICE_MANIFEST,
THIRD_PARTY_NOTICES,
'licenses',
]);
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 assertPeRange(image, offset, size, label) {
if (
!Number.isSafeInteger(offset) ||
!Number.isSafeInteger(size) ||
offset < 0 ||
size < 0 ||
offset + size > image.length
) {
throw new Error(`Invalid PE image: ${label} is out of range.`);
}
}
function readPeImportedDllNames(binaryPath) {
const image = fs.readFileSync(binaryPath);
assertPeRange(image, 0, 0x40, 'DOS header');
if (image.toString('ascii', 0, 2) !== 'MZ') {
throw new Error('Invalid PE image: missing DOS signature.');
}
const peOffset = image.readUInt32LE(0x3c);
assertPeRange(image, peOffset, 24, 'PE header');
if (image.toString('ascii', peOffset, peOffset + 4) !== 'PE\0\0') {
throw new Error('Invalid PE image: missing PE signature.');
}
const sectionCount = image.readUInt16LE(peOffset + 6);
const optionalHeaderSize = image.readUInt16LE(peOffset + 20);
const optionalHeaderOffset = peOffset + 24;
assertPeRange(
image,
optionalHeaderOffset,
optionalHeaderSize,
'optional header'
);
const optionalMagic = image.readUInt16LE(optionalHeaderOffset);
const dataDirectoryOffset =
optionalMagic === 0x20b
? optionalHeaderOffset + 112
: optionalMagic === 0x10b
? optionalHeaderOffset + 96
: null;
const directoryCountOffset =
optionalMagic === 0x20b
? optionalHeaderOffset + 108
: optionalMagic === 0x10b
? optionalHeaderOffset + 92
: null;
if (dataDirectoryOffset === null || directoryCountOffset === null) {
throw new Error(
`Invalid PE image: unsupported optional-header magic 0x${optionalMagic.toString(16)}.`
);
}
const optionalHeaderEnd = optionalHeaderOffset + optionalHeaderSize;
if (
directoryCountOffset + 4 > optionalHeaderEnd ||
dataDirectoryOffset + 16 > optionalHeaderEnd
) {
throw new Error(
'Invalid PE image: data directories exceed the optional header.'
);
}
assertPeRange(image, directoryCountOffset, 4, 'data-directory count');
if (image.readUInt32LE(directoryCountOffset) < 2) {
return [];
}
assertPeRange(image, dataDirectoryOffset + 8, 8, 'import directory');
const importRva = image.readUInt32LE(dataDirectoryOffset + 8);
const importSize = image.readUInt32LE(dataDirectoryOffset + 12);
if (importRva === 0 && importSize === 0) {
return [];
}
if (importRva === 0 || importSize < 20) {
throw new Error('Invalid PE image: malformed import directory.');
}
const sectionTableOffset = optionalHeaderOffset + optionalHeaderSize;
assertPeRange(
image,
sectionTableOffset,
sectionCount * 40,
'section table'
);
const sections = Array.from({ length: sectionCount }, (_, index) => {
const offset = sectionTableOffset + index * 40;
return {
virtualSize: image.readUInt32LE(offset + 8),
virtualAddress: image.readUInt32LE(offset + 12),
rawSize: image.readUInt32LE(offset + 16),
rawOffset: image.readUInt32LE(offset + 20),
};
});
const rvaToLocation = (rva, label) => {
const section = sections.find(
(candidate) =>
rva >= candidate.virtualAddress &&
rva <
candidate.virtualAddress +
Math.max(candidate.virtualSize, candidate.rawSize)
);
if (!section) {
throw new Error(
`Invalid PE image: ${label} RVA 0x${rva.toString(16)} is not mapped.`
);
}
const sectionOffset = rva - section.virtualAddress;
if (sectionOffset >= section.rawSize) {
throw new Error(
`Invalid PE image: ${label} is outside section file data.`
);
}
const offset = section.rawOffset + sectionOffset;
assertPeRange(image, offset, 1, label);
return {
offset,
sectionEnd: section.rawOffset + section.rawSize,
};
};
const importedDllNames = [];
const descriptorCount = Math.floor(importSize / 20);
let foundTerminator = false;
for (let index = 0; index < descriptorCount; index += 1) {
const descriptorLocation = rvaToLocation(
importRva + index * 20,
'import descriptor'
);
const descriptorOffset = descriptorLocation.offset;
if (descriptorOffset + 20 > descriptorLocation.sectionEnd) {
throw new Error(
'Invalid PE image: import descriptor crosses section file data.'
);
}
assertPeRange(image, descriptorOffset, 20, 'import descriptor');
const descriptor = image.subarray(
descriptorOffset,
descriptorOffset + 20
);
if (descriptor.every((value) => value === 0)) {
foundTerminator = true;
break;
}
const nameRva = image.readUInt32LE(descriptorOffset + 12);
const nameLocation = rvaToLocation(nameRva, 'imported DLL name');
const nameOffset = nameLocation.offset;
const nameEnd = image.indexOf(0, nameOffset);
if (nameEnd < 0 || nameEnd >= nameLocation.sectionEnd) {
throw new Error(
'Invalid PE image: imported DLL name is not terminated.'
);
}
const name = image.toString('ascii', nameOffset, nameEnd);
if (!name) {
throw new Error('Invalid PE image: imported DLL name is empty.');
}
importedDllNames.push(name);
}
if (!foundTerminator) {
throw new Error(
'Invalid PE image: import descriptor table has no terminator.'
);
}
return [...new Set(importedDllNames)];
}
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 windowsMpvRuntimeNames.flatMap((name) =>
[
nativeDir ? path.join(nativeDir, name) : null,
path.join(libDir, name),
].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 ELECTRON_BUILDER_ARCH_NAMES.includes(arch) ? arch : null;
}
return ELECTRON_BUILDER_ARCH_NAMES[arch] ?? null;
}
function resolveConfiguredLinuxTargetNames(configuredTargets, targetArch) {
if (!Array.isArray(configuredTargets)) {
throw new TypeError('Electron Builder linux.target must be an array.');
}
const archName = resolveElectronBuilderArchName(targetArch);
if (!archName) {
throw new Error(
`Unknown Electron Builder architecture: ${String(targetArch)}.`
);
}
const targetNames = new Set();
for (const target of configuredTargets) {
const targetName =
typeof target === 'string'
? target
: target && typeof target === 'object'
? target.target
: null;
if (typeof targetName !== 'string' || targetName.trim() === '') {
throw new Error(
'Electron Builder Linux targets must have a non-empty target name.'
);
}
if (target && typeof target === 'object' && target.arch !== undefined) {
const configuredArches = Array.isArray(target.arch)
? target.arch
: [target.arch];
const configuredArchNames = configuredArches.map(
(configuredArch) => {
const configuredArchName =
resolveElectronBuilderArchName(configuredArch);
if (!configuredArchName) {
throw new Error(
`Unknown Electron Builder architecture: ${String(
configuredArch
)}.`
);
}
return configuredArchName;
}
);
if (!configuredArchNames.includes(archName)) {
continue;
}
}
targetNames.add(targetName.trim().toLowerCase());
}
if (targetNames.size === 0) {
throw new Error(
`Electron Builder has no Linux targets configured for ${archName}.`
);
}
return [...targetNames].sort();
}
/**
* Official Linux frame-copy support is x64-only. electron-builder also
* produces arm64/armv7l packages, which must always carry only the
* unavailable marker and native-view fallback.
*/
function isForeignLinuxEmbeddedMpvArch(platform, targetArch) {
if (normalizeEmbeddedMpvPlatform(platform) !== 'linux') {
return false;
}
const archName = resolveElectronBuilderArchName(targetArch);
return Boolean(archName) && archName !== 'x64';
}
// 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 pathExistsByLstat(filePath) {
try {
fs.lstatSync(filePath);
return true;
} catch {
return false;
}
}
function inspectRegularReadableFile(
filePath,
label,
errors,
expectedMode = null
) {
let stat;
try {
stat = fs.lstatSync(filePath);
} catch {
errors.push(`Missing ${label}: ${filePath}`);
return null;
}
if (!stat.isFile() || stat.isSymbolicLink()) {
errors.push(`${label} must be a regular file: ${filePath}`);
return null;
}
try {
fs.accessSync(filePath, fs.constants.R_OK);
} catch {
errors.push(`${label} must be readable: ${filePath}`);
}
if (expectedMode !== null && (stat.mode & 0o7777) !== expectedMode) {
errors.push(
`${label} must have mode ${expectedMode
.toString(8)
.padStart(4, '0')}: ${filePath}`
);
}
return stat;
}
function readPackagedJson(filePath, label, errors) {
let contents;
try {
contents = fs.readFileSync(filePath, 'utf8');
} catch (error) {
errors.push(
`Unable to read ${label} at ${filePath}: ${
error instanceof Error ? error.message : String(error)
}`
);
return null;
}
try {
return JSON.parse(contents);
} catch (error) {
errors.push(
`Invalid JSON in ${label} at ${filePath}: ${
error instanceof Error ? error.message : String(error)
}`
);
return null;
}
}
function validateForeignLinuxNativeDir(nativeDir) {
const errors = [];
const markerName = 'embedded-mpv-unavailable.txt';
const markerPath = path.join(nativeDir, markerName);
let nativeStat;
try {
nativeStat = fs.lstatSync(nativeDir);
} catch {
return [
`Missing embedded MPV unavailable marker for foreign-architecture package: ${markerPath}`,
];
}
if (!nativeStat.isDirectory() || nativeStat.isSymbolicLink()) {
return [
`Foreign-architecture embedded MPV native path must be a regular directory: ${nativeDir}`,
];
}
const entries = fs.readdirSync(nativeDir).sort();
const unexpectedEntries = entries.filter((name) => name !== markerName);
if (unexpectedEntries.length > 0) {
errors.push(
[
'Embedded MPV artifacts must not ship in foreign-architecture Linux packages; only the unavailable marker is allowed.',
...unexpectedEntries.map(
(name) => `- ${path.join(nativeDir, name)}`
),
].join('\n')
);
}
if (!entries.includes(markerName)) {
errors.push(
`Missing embedded MPV unavailable marker for foreign-architecture package: ${markerPath}`
);
} else {
inspectRegularReadableFile(
markerPath,
'embedded MPV unavailable marker',
errors
);
}
return errors;
}
function validateNativeViewOnlyLinuxPackage(
nativeDir,
addonPath,
manifestPath,
errors
) {
for (const legalPath of linuxRuntimeLegalPaths) {
const packagedLegalPath = path.join(nativeDir, legalPath);
if (pathExistsByLstat(packagedLegalPath)) {
errors.push(
`Linux native-view-only packages must not ship bundled runtime legal files: ${packagedLegalPath}`
);
}
}
for (const artifactName of [
'iptvnator_mpv_helper',
'iptvnator_mpv_helper.exe',
'embedded_mpv_frame_reader.node',
]) {
const artifactPath = path.join(nativeDir, artifactName);
if (pathExistsByLstat(artifactPath)) {
errors.push(
`Linux native-view-only packages must not ship frame-copy helpers or readers: ${artifactPath}`
);
}
}
const markerPath = path.join(nativeDir, 'embedded-mpv-unavailable.txt');
if (pathExistsByLstat(markerPath)) {
errors.push(
`Same-architecture Linux packages must not retain the unavailable marker: ${markerPath}`
);
}
const libDir = path.join(nativeDir, 'lib');
if (pathExistsByLstat(libDir)) {
errors.push(
`Linux native-view-only packages must not bundle libmpv or retain a private runtime directory: ${libDir}`
);
}
inspectRegularReadableFile(addonPath, 'embedded MPV native addon', errors);
if (
!inspectRegularReadableFile(
manifestPath,
'embedded MPV runtime manifest',
errors
)
) {
return;
}
const manifest = readPackagedJson(
manifestPath,
'embedded MPV runtime manifest',
errors
);
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
if (manifest !== null) {
errors.push('Embedded MPV runtime manifest must be an object.');
}
return;
}
const expectedFields = {
schemaVersion: 1,
origin: 'external-mpv-process',
platform: 'linux',
arch: 'x64',
runtimeMode: 'native-view-only',
frameCopyAvailable: false,
artifacts: { addon: 'embedded_mpv.node' },
nativeViewFallback: linuxNativeViewFallback,
};
for (const [field, expected] of Object.entries(expectedFields)) {
if (!isDeepStrictEqual(manifest[field], expected)) {
errors.push(
`Linux native-view-only manifest ${field} must equal ${JSON.stringify(
expected
)}; received ${JSON.stringify(manifest[field])}.`
);
}
}
if (Object.hasOwn(manifest, 'profile')) {
errors.push(
'Linux native-view-only manifest must not include a frame-copy profile.'
);
}
}
function normalizeLinuxTargetNames(targetNames, errors) {
if (!Array.isArray(targetNames) || targetNames.length === 0) {
errors.push(
'Linux frame-copy package validation requires at least one target name.'
);
return [];
}
const normalized = [];
for (const targetName of targetNames) {
const name = String(targetName ?? '')
.trim()
.toLowerCase();
if (!name) {
errors.push(
'Linux frame-copy target names must be non-empty strings.'
);
continue;
}
if (normalized.includes(name)) {
errors.push(`Linux frame-copy target "${name}" is duplicated.`);
continue;
}
normalized.push(name);
}
return normalized.sort();
}
function validatePackagedManifestContract(
manifest,
profile,
targetNames,
errors
) {
const expectedFields = {
schemaVersion: 1,
origin: profile.manifestOrigin,
platform: 'linux',
arch: 'x64',
profile: profile.name,
runtimeMode: profile.runtimeMode,
targets: targetNames,
artifacts: linuxFrameCopyArtifacts,
processIsolation: linuxFrameCopyProcessIsolation,
nativeViewFallback: linuxNativeViewFallback,
};
for (const [field, expected] of Object.entries(expectedFields)) {
if (!isDeepStrictEqual(manifest[field], expected)) {
errors.push(
`Linux frame-copy manifest ${field} for profile "${profile.name}" must equal ${JSON.stringify(
expected
)}; received ${JSON.stringify(manifest[field])}.`
);
}
}
if (
typeof manifest.generatedAt !== 'string' ||
manifest.generatedAt.trim() === '' ||
Number.isNaN(Date.parse(manifest.generatedAt))
) {
errors.push(
'Linux frame-copy manifest generatedAt must be a valid timestamp.'
);
}
if (
typeof manifest.libmpvSoname !== 'string' ||
!versionedLinuxLibmpvPattern.test(manifest.libmpvSoname)
) {
errors.push(
'Linux frame-copy manifest libmpvSoname must be a versioned libmpv SONAME.'
);
}
}
function validateSystemLinuxRuntime(nativeDir, manifest, errors) {
for (const legalPath of linuxRuntimeLegalPaths) {
const packagedLegalPath = path.join(nativeDir, legalPath);
if (pathExistsByLstat(packagedLegalPath)) {
errors.push(
`Linux system frame-copy packages must not ship bundled runtime legal files: ${packagedLegalPath}`
);
}
}
if (
!isDeepStrictEqual(
manifest.packageDependencies,
LINUX_SYSTEM_PACKAGE_DEPENDENCIES
)
) {
errors.push(
`Linux system frame-copy manifest packageDependencies must equal ${JSON.stringify(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES
)}.`
);
}
if (!isDeepStrictEqual(manifest.runtimeFiles, [])) {
errors.push(
'Linux system frame-copy manifest runtimeFiles must be empty.'
);
}
if (manifest.runtimeTotalBytes !== 0) {
errors.push(
'Linux system frame-copy manifest runtimeTotalBytes must equal 0.'
);
}
for (const forbiddenField of [
'runtimeDependencyClosure',
'externalSystemLibraries',
'sourceArchive',
'sourceRuntime',
]) {
if (Object.hasOwn(manifest, forbiddenField)) {
errors.push(
`Linux system frame-copy manifest must not include ${forbiddenField}.`
);
}
}
const libDir = path.join(nativeDir, 'lib');
if (pathExistsByLstat(libDir)) {
errors.push(
`Linux system frame-copy packages must not retain a private runtime directory: ${libDir}`
);
}
}
function sha256File(filePath) {
return crypto
.createHash('sha256')
.update(fs.readFileSync(filePath))
.digest('hex');
}
function validateBundledLinuxRuntime(nativeDir, manifest, errors) {
if (!isDeepStrictEqual(manifest.packageDependencies, {})) {
errors.push(
'Linux bundled frame-copy manifest packageDependencies must be empty.'
);
}
const sourceRuntimeErrors = validateLinuxRuntimeManifest(
manifest.sourceRuntime
);
errors.push(
...sourceRuntimeErrors.map(
(error) => `Invalid packaged Linux source runtime: ${error}`
)
);
errors.push(
...validateLinuxSourceArchiveBinding(manifest.sourceArchive).map(
(error) => `Invalid packaged Linux source archive binding: ${error}`
)
);
errors.push(
...validateLinuxRuntimeNotices(nativeDir, manifest.sourceRuntime, {
allowUnrelatedFiles: true,
}).map((error) => `Invalid packaged Linux runtime notices: ${error}`)
);
if (
!isDeepStrictEqual(
manifest.runtimeFiles,
manifest.sourceRuntime?.runtimeFiles
)
) {
errors.push(
'Linux bundled frame-copy manifest runtimeFiles must exactly match sourceRuntime.runtimeFiles.'
);
}
if (
!isDeepStrictEqual(
manifest.runtimeDependencyClosure,
manifest.sourceRuntime?.runtimeDependencyClosure
)
) {
errors.push(
'Linux bundled frame-copy manifest runtimeDependencyClosure must exactly match sourceRuntime.runtimeDependencyClosure.'
);
}
if (
!isDeepStrictEqual(
manifest.externalSystemLibraries,
manifest.sourceRuntime?.externalSystemLibraries
)
) {
errors.push(
'Linux bundled frame-copy manifest externalSystemLibraries must exactly match sourceRuntime.externalSystemLibraries.'
);
}
if (!Array.isArray(manifest.runtimeFiles)) {
errors.push(
'Linux bundled frame-copy manifest runtimeFiles must be an array.'
);
return;
}
const expectedTotal = manifest.runtimeFiles.reduce(
(total, runtimeFile) =>
total +
(runtimeFile &&
Number.isSafeInteger(runtimeFile.size) &&
runtimeFile.size > 0
? runtimeFile.size
: 0),
0
);
if (manifest.runtimeTotalBytes !== expectedTotal) {
errors.push(
`Linux bundled frame-copy manifest runtimeTotalBytes must equal ${expectedTotal}.`
);
}
const libDir = path.join(nativeDir, 'lib');
let libStat;
try {
libStat = fs.lstatSync(libDir);
} catch {
errors.push(`Missing bundled Linux runtime directory: ${libDir}`);
return;
}
if (!libStat.isDirectory() || libStat.isSymbolicLink()) {
errors.push(
`Bundled Linux runtime path must be a regular directory: ${libDir}`
);
return;
}
const declaredNames = new Set();
for (const runtimeFile of manifest.runtimeFiles) {
if (
!runtimeFile ||
typeof runtimeFile.name !== 'string' ||
path.basename(runtimeFile.name) !== runtimeFile.name ||
runtimeFile.name === '.' ||
runtimeFile.name === '..'
) {
errors.push(
`Bundled Linux runtime manifest contains an unsafe file name: ${JSON.stringify(
runtimeFile?.name
)}.`
);
continue;
}
if (declaredNames.has(runtimeFile.name)) {
errors.push(
`Bundled Linux runtime manifest contains duplicate file ${runtimeFile.name}.`
);
continue;
}
declaredNames.add(runtimeFile.name);
}
let packagedEntries;
try {
packagedEntries = fs.readdirSync(libDir).sort();
} catch (error) {
errors.push(
`Unable to enumerate bundled Linux runtime at ${libDir}: ${
error instanceof Error ? error.message : String(error)
}`
);
return;
}
for (const entryName of packagedEntries) {
if (!declaredNames.has(entryName)) {
errors.push(
`Found undeclared bundled Linux runtime artifact: ${path.join(
libDir,
entryName
)}`
);
}
}
for (const runtimeFile of manifest.runtimeFiles) {
if (!runtimeFile || !declaredNames.has(runtimeFile.name)) {
continue;
}
const runtimePath = path.join(libDir, runtimeFile.name);
const stat = inspectRegularReadableFile(
runtimePath,
`bundled Linux runtime file ${runtimeFile.name}`,
errors
);
if (!stat) {
continue;
}
if (stat.size !== runtimeFile.size) {
errors.push(
`Bundled Linux runtime size mismatch for ${runtimeFile.name}: expected ${runtimeFile.size}, received ${stat.size}.`
);
}
let actualSha256;
try {
actualSha256 = sha256File(runtimePath);
} catch (error) {
errors.push(
`Unable to hash bundled Linux runtime file ${runtimePath}: ${
error instanceof Error ? error.message : String(error)
}`
);
continue;
}
if (actualSha256 !== runtimeFile.sha256) {
errors.push(
`Bundled Linux runtime SHA-256 mismatch for ${runtimeFile.name}: expected ${runtimeFile.sha256}, received ${actualSha256}.`
);
}
}
}
function normalizeElfInspection(value, binaryPath) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(
`ELF inspection for ${binaryPath} must return an object.`
);
}
const result = {
soname: value.soname ?? null,
};
if (
result.soname !== null &&
(typeof result.soname !== 'string' ||
path.basename(result.soname) !== result.soname)
) {
throw new Error(
`ELF inspection for ${binaryPath} must return a safe SONAME or null.`
);
}
for (const field of ['needed', 'rpath', 'runpath']) {
if (
!Array.isArray(value[field]) ||
value[field].some((entry) => typeof entry !== 'string')
) {
throw new Error(
`ELF inspection for ${binaryPath} must return string array ${field}.`
);
}
result[field] = [...new Set(value[field])].sort();
}
return result;
}
function dependencyFileName(dependencyName) {
return dependencyName.replaceAll('\\', '/').split('/').at(-1) ?? '';
}
function listElectronShippedLinuxLibraries(resourceDir, options = {}) {
const appDir = path.dirname(resourceDir);
const normalizedResourceDir = path.resolve(resourceDir);
const excludedSnapLibraryRoots =
options.artifactFormat === 'snap'
? new Set(
[
path.join(appDir, 'lib'),
path.join(appDir, 'usr', 'lib'),
].map((directoryPath) => path.resolve(directoryPath))
)
: new Set();
const libraries = [];
function visit(directoryPath) {
for (const entry of fs.readdirSync(directoryPath, {
withFileTypes: true,
})) {
const entryPath = path.join(directoryPath, entry.name);
if (path.resolve(entryPath) === normalizedResourceDir) {
continue;
}
if (entry.isDirectory()) {
if (excludedSnapLibraryRoots.has(path.resolve(entryPath))) {
continue;
}
visit(entryPath);
continue;
}
if (
(entry.isFile() || entry.isSymbolicLink()) &&
/\.so(?:\.\d+)*$/.test(entry.name)
) {
libraries.push(entryPath);
}
}
}
// afterPack and unpacked-layout checks see the pristine Electron tree, so
// recurse to catch future nested Electron libraries. An extracted Snap
// overlays package-manager lib/ and usr/lib/ trees onto that same root;
// exclude exactly those target-provided roots while scanning everything
// else recursively.
visit(appDir);
return libraries.sort();
}
function inspectLinuxElfIsolation(
resourceDir,
nativeDir,
manifest,
targetNames,
options,
errors
) {
const executableName = options.executableName ?? 'iptvnator';
let launcherLayout;
try {
launcherLayout = resolveLinuxLauncherLayout(
targetNames,
executableName
);
} catch (error) {
errors.push(
`Unable to resolve Linux launcher layout: ${
error instanceof Error ? error.message : String(error)
}`
);
return;
}
const hostPlatform = options.hostPlatform ?? process.platform;
let inspectElf = options.elfInspector;
if (!inspectElf) {
if (hostPlatform !== 'linux') {
return;
}
if (!commandExists('readelf')) {
errors.push(
'readelf is required to validate Linux embedded MPV packaging.'
);
return;
}
inspectElf = (binaryPath) =>
parseReadelfDynamic(run('readelf', ['-d', binaryPath]));
}
if (typeof inspectElf !== 'function') {
errors.push('Linux ELF inspector must be a function.');
return;
}
const inspectedPaths = {
electron: path.join(
path.dirname(resourceDir),
launcherLayout.electronBinaryName
),
addon: path.join(nativeDir, linuxFrameCopyArtifacts.addon.name),
reader: path.join(nativeDir, linuxFrameCopyArtifacts.frameReader.name),
helper: path.join(nativeDir, linuxFrameCopyArtifacts.helper.name),
};
for (const [index, libraryPath] of listElectronShippedLinuxLibraries(
resourceDir,
{ artifactFormat: options.artifactFormat }
).entries()) {
inspectedPaths[`electronLibrary:${index}`] = libraryPath;
}
const inspections = {};
for (const [label, binaryPath] of Object.entries(inspectedPaths)) {
if (
!inspectRegularReadableFile(
binaryPath,
`Linux ${label} ELF binary`,
errors
)
) {
continue;
}
try {
inspections[label] = normalizeElfInspection(
inspectElf(binaryPath),
binaryPath
);
} catch (error) {
errors.push(
`Unable to inspect Linux ${label} ELF binary at ${binaryPath}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
for (const label of Object.keys(inspections).filter(
(name) =>
name === 'electron' ||
name === 'addon' ||
name === 'reader' ||
name.startsWith('electronLibrary:')
)) {
const dynamic = inspections[label];
if (!dynamic) {
continue;
}
const displayLabel = label.startsWith('electronLibrary:')
? 'Electron library'
: label;
for (const dependencyName of dynamic.needed) {
if (dependencyFileName(dependencyName) !== dependencyName) {
errors.push(
`Linux ${displayLabel} DT_NEEDED entry must not contain a path: ${dependencyName} in ${inspectedPaths[label]}.`
);
}
}
const libmpvDependencies = dynamic.needed.filter((dependencyName) =>
anyLinuxLibmpvPattern.test(dependencyFileName(dependencyName))
);
if (libmpvDependencies.length > 0) {
errors.push(
`Linux ${displayLabel} must not link libmpv; found ${libmpvDependencies.join(
', '
)} in ${inspectedPaths[label]}.`
);
}
}
const helper = inspections.helper;
if (helper) {
if (!helper.needed.includes(manifest.libmpvSoname)) {
errors.push(
`Linux frame-copy helper must directly need ${manifest.libmpvSoname}.`
);
}
const unexpectedLibmpvDependencies = helper.needed.filter(
(dependencyName) =>
anyLinuxLibmpvPattern.test(
dependencyFileName(dependencyName)
) && dependencyName !== manifest.libmpvSoname
);
if (unexpectedLibmpvDependencies.length > 0) {
errors.push(
`Linux frame-copy helper must not need a different libmpv SONAME: ${unexpectedLibmpvDependencies.join(
', '
)}.`
);
}
if (helper.rpath.length > 0) {
errors.push(
`Linux frame-copy helper must not contain RPATH; found ${helper.rpath.join(
':'
)}.`
);
}
if (
helper.runpath.length !== 1 ||
helper.runpath[0] !== '$ORIGIN/lib'
) {
errors.push(
`Linux frame-copy helper RUNPATH must be exactly $ORIGIN/lib; received ${
helper.runpath.length > 0
? helper.runpath.join(':')
: '<empty>'
}.`
);
}
const allowedHelperDependencies = new Set([
manifest.libmpvSoname,
...GLIBC_TOOLCHAIN_ALLOWLIST,
...EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
...(manifest.runtimeMode === 'bundled' &&
Array.isArray(manifest.runtimeFiles)
? manifest.runtimeFiles.map(({ name }) => name)
: []),
...(manifest.runtimeMode === 'bundled' &&
Array.isArray(
manifest.runtimeDependencyClosure?.externalDependencies
)
? manifest.runtimeDependencyClosure.externalDependencies
: []),
]);
for (const dependencyName of helper.needed) {
if (
dependencyFileName(dependencyName) !== dependencyName ||
!allowedHelperDependencies.has(dependencyName)
) {
errors.push(
`Linux frame-copy helper dependency is not bundled or allowlisted: ${dependencyName}.`
);
}
}
}
if (
manifest.runtimeMode !== 'bundled' ||
!Array.isArray(manifest.runtimeFiles)
) {
return;
}
const runtimeFileNames = manifest.runtimeFiles
.map((runtimeFile) => runtimeFile?.name)
.filter((name) => typeof name === 'string');
const runtimeNameSet = new Set(runtimeFileNames);
const allowedExternalNames = new Set([
...GLIBC_TOOLCHAIN_ALLOWLIST,
...EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
]);
const closureEntries = [];
let closureHasErrors = false;
for (const runtimeFileName of runtimeFileNames) {
const runtimePath = path.join(nativeDir, 'lib', runtimeFileName);
let dynamic;
try {
dynamic = normalizeElfInspection(
inspectElf(runtimePath),
runtimePath
);
} catch (error) {
closureHasErrors = true;
errors.push(
`Unable to inspect bundled Linux runtime ELF at ${runtimePath}: ${
error instanceof Error ? error.message : String(error)
}`
);
continue;
}
closureEntries.push({
name: runtimeFileName,
soname: dynamic.soname,
needed: dynamic.needed,
rpath: dynamic.rpath,
runpath: dynamic.runpath,
});
if (dynamic.rpath.length > 0) {
closureHasErrors = true;
errors.push(
`${runtimeFileName} has forbidden RPATH ${dynamic.rpath.join(
':'
)}.`
);
}
if (dynamic.runpath.length !== 1 || dynamic.runpath[0] !== '$ORIGIN') {
closureHasErrors = true;
errors.push(
`${runtimeFileName} RUNPATH must be exactly $ORIGIN; got ${
dynamic.runpath.length > 0
? dynamic.runpath.join(':')
: '<empty>'
}.`
);
}
for (const dependencyName of dynamic.needed) {
if (
!runtimeNameSet.has(dependencyName) &&
!allowedExternalNames.has(dependencyName)
) {
closureHasErrors = true;
errors.push(
`Runtime dependency is not bundled or allowlisted: ${runtimeFileName} -> ${dependencyName}.`
);
}
}
}
if (closureHasErrors) {
return;
}
try {
const actualClosure = validateRuntimeDependencyClosure({
entries: closureEntries,
runtimeFileNames,
buildPrefix: '',
});
if (
!isDeepStrictEqual(actualClosure, manifest.runtimeDependencyClosure)
) {
errors.push(
'Actual bundled Linux ELF dependency closure does not match the packaged manifest.'
);
}
} catch (error) {
errors.push(
`Invalid bundled Linux ELF dependency closure: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
function validateLinuxPackagedEmbeddedMpv(resourceDir, options) {
const nativeDir = path.join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
if (options.foreignArch) {
return validateForeignLinuxNativeDir(nativeDir);
}
const errors = [];
const addonPath = path.join(nativeDir, 'embedded_mpv.node');
const manifestPath = path.join(nativeDir, 'embedded-mpv-runtime.json');
if (!pathExistsByLstat(addonPath)) {
if (options.required) {
errors.push(`Missing embedded MPV native addon: ${addonPath}`);
}
if (pathExistsByLstat(nativeDir)) {
for (const staleName of [
'embedded_mpv_frame_reader.node',
'iptvnator_mpv_helper',
'iptvnator_mpv_helper.exe',
'embedded-mpv-runtime.json',
'embedded-mpv-unavailable.txt',
'lib',
]) {
const stalePath = path.join(nativeDir, staleName);
if (pathExistsByLstat(stalePath)) {
errors.push(
`Embedded MPV addon is missing but stale packaged artifact remains: ${stalePath}`
);
}
}
}
return errors;
}
if (!options.profile) {
if (options.required) {
errors.push(
'Linux frame-copy profile is required for a required same-architecture package.'
);
}
validateNativeViewOnlyLinuxPackage(
nativeDir,
addonPath,
manifestPath,
errors
);
return errors;
}
let profile;
try {
profile = resolveLinuxFrameCopyProfile(options.profile);
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
return errors;
}
const targetNames = normalizeLinuxTargetNames(options.targetNames, errors);
if (targetNames.length > 0) {
try {
errors.push(
...validateLinuxProfileTargets(profile.name, targetNames)
);
} catch (error) {
errors.push(error instanceof Error ? error.message : String(error));
}
}
const readerPath = path.join(
nativeDir,
linuxFrameCopyArtifacts.frameReader.name
);
const helperPath = path.join(
nativeDir,
linuxFrameCopyArtifacts.helper.name
);
inspectRegularReadableFile(addonPath, 'embedded MPV native addon', errors);
inspectRegularReadableFile(
readerPath,
'embedded MPV frame reader',
errors,
0o644
);
inspectRegularReadableFile(
helperPath,
'embedded MPV frame-copy helper',
errors,
0o755
);
const staleWindowsHelper = path.join(nativeDir, 'iptvnator_mpv_helper.exe');
if (pathExistsByLstat(staleWindowsHelper)) {
errors.push(
`Linux frame-copy package must not retain the Windows helper: ${staleWindowsHelper}`
);
}
const staleMarker = path.join(nativeDir, 'embedded-mpv-unavailable.txt');
if (pathExistsByLstat(staleMarker)) {
errors.push(
`Same-architecture Linux package must not retain the unavailable marker: ${staleMarker}`
);
}
if (
!inspectRegularReadableFile(
manifestPath,
'embedded MPV runtime manifest',
errors
)
) {
return errors;
}
const manifest = readPackagedJson(
manifestPath,
'embedded MPV runtime manifest',
errors
);
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
if (manifest !== null) {
errors.push('Embedded MPV runtime manifest must be an object.');
}
return errors;
}
validatePackagedManifestContract(manifest, profile, targetNames, errors);
if (profile.runtimeMode === 'system') {
validateSystemLinuxRuntime(nativeDir, manifest, errors);
} else {
validateBundledLinuxRuntime(nativeDir, manifest, errors);
}
inspectLinuxElfIsolation(
resourceDir,
nativeDir,
manifest,
targetNames,
options,
errors
);
return errors;
}
function validatePackagedEmbeddedMpv(resourceDir, options = {}) {
const platform = normalizeEmbeddedMpvPlatform(options.platform);
if (platform === 'linux') {
return validateLinuxPackagedEmbeddedMpv(resourceDir, options);
}
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 (!fs.existsSync(addonPath)) {
if (options.required) {
errors.push(`Missing embedded MPV native addon: ${addonPath}`);
}
return errors;
}
if (platform === 'darwin' || platform === 'win32') {
// The frame-copy engine artifacts are built by the same binding.gyp
// run as the addon; a macOS/Windows package that ships the addon
// without them would silently lose the engine (support probe hides
// it).
const missingFrameCopyArtifacts = [
platform === 'win32'
? 'iptvnator_mpv_helper.exe'
: '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 (platform === 'win32') {
// The helper is a separate executable. Windows resolves its
// imported libmpv DLL from the executable directory, so a copy
// under native/lib may satisfy addon bookkeeping but cannot
// start iptvnator_mpv_helper.exe.
const helperPath = path.join(
unpackedNativeDir,
'iptvnator_mpv_helper.exe'
);
const helperRuntimeCandidates = windowsMpvRuntimeNames.map((name) =>
path.join(unpackedNativeDir, name)
);
if (
!helperRuntimeCandidates.some((candidate) =>
fs.existsSync(candidate)
)
) {
errors.push(
[
`Missing bundled MPV DLL beside the Windows frame-copy helper in ${unpackedNativeDir}.`,
'Expected one of:',
...helperRuntimeCandidates.map(
(candidate) => `- ${candidate}`
),
].join('\n')
);
}
if (fs.existsSync(helperPath)) {
try {
const acceptedRuntimeNames = new Set(
windowsMpvRuntimeNames.map((name) => name.toLowerCase())
);
const importedRuntimeNames = readPeImportedDllNames(
helperPath
).filter((name) =>
acceptedRuntimeNames.has(name.toLowerCase())
);
if (importedRuntimeNames.length === 0) {
errors.push(
`Windows frame-copy helper does not import a supported MPV DLL: ${helperPath}`
);
}
const packagedRuntimeNames = new Set(
fs
.readdirSync(unpackedNativeDir, {
withFileTypes: true,
})
.filter((entry) => entry.isFile())
.map((entry) => entry.name.toLowerCase())
);
for (const importedRuntimeName of importedRuntimeNames) {
if (
!packagedRuntimeNames.has(
importedRuntimeName.toLowerCase()
)
) {
errors.push(
`Windows frame-copy helper imports ${importedRuntimeName}, but the matching DLL is missing beside it: ${path.join(unpackedNativeDir, importedRuntimeName)}`
);
}
}
} catch (error) {
errors.push(
`Unable to inspect Windows frame-copy helper imports at ${helperPath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
}
}
if (!fs.existsSync(manifestPath)) {
errors.push(`Missing embedded MPV runtime manifest: ${manifestPath}`);
} else {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const expectedOrigin = 'vendored-lgpl';
if (manifest.origin !== expectedOrigin) {
errors.push(
`Embedded MPV packaged runtime must be ${expectedOrigin}, received: ${manifest.origin}`
);
}
}
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,
listElectronShippedLinuxLibraries,
listRuntimeFiles,
listDylibs,
parseOtoolDependencies,
parseOtoolRpaths,
patchAddonForBundledRuntime,
validateNoForbiddenRuntimeLinks,
getPackagedRuntimeCandidates,
validatePackagedEmbeddedMpv,
isForeignLinuxEmbeddedMpvArch,
linuxUnpackedDirArch,
resolveConfiguredLinuxTargetNames,
resolveElectronBuilderArchName,
};