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 validatePackagedEmbeddedMpv(resourceDir, options = {}) { if (process.platform !== 'darwin') { return []; } if (!commandExists('otool')) { return ['otool is required to validate embedded MPV macOS packaging.']; } 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 (!fs.existsSync(manifestPath)) { errors.push(`Missing embedded MPV runtime manifest: ${manifestPath}`); } else { const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); if (manifest.origin !== 'vendored-lgpl') { errors.push( `Embedded MPV packaged runtime must be vendored-lgpl, received: ${manifest.origin}` ); } } if (!fs.existsSync(path.join(libDir, 'libmpv.2.dylib'))) { errors.push(`Missing bundled libmpv.2.dylib in ${libDir}`); } const binaries = [addonPath, ...listRuntimeFiles(libDir)]; errors.push(...validateNoForbiddenRuntimeLinks(binaries)); return errors; } module.exports = { collectExternalDylibs, commandExists, copyRuntimeToNativeBuild, findLibMpv, listRuntimeFiles, listDylibs, parseOtoolDependencies, parseOtoolRpaths, patchAddonForBundledRuntime, validateNoForbiddenRuntimeLinks, validatePackagedEmbeddedMpv, };