Files
iptvnator/tools/packaging/embedded-mpv-packaging.cjs
T
4gray 59e08fd2d6 feat(embedded-mpv): add Windows frame-copy support (#1175)
Port the embedded mpv frame-copy pipeline to Windows with WGL rendering and named shared memory. Includes packaging validation, platform gates, tests, and architecture documentation.
2026-07-15 21:27:56 +02:00

914 lines
28 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/'];
const windowsMpvRuntimeNames = [
'mpv-2.dll',
'libmpv-2.dll',
'mpv.dll',
'libmpv.dll',
];
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 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' || 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). Linux packages intentionally strip the helper until the
// bundled-libmpv runtime lands (see electron-after-pack.cjs).
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 =
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,
};