Files
iptvnator/apps/electron-backend/build-embedded-mpv.js
T
4grayandClaude Fable 5.1 634a66b1e4 build(deps): declare node-gyp for the embedded MPV native build (#1625)
`apps/electron-backend/build-embedded-mpv.js` resolves the compiler with
`require.resolve('node-gyp/bin/node-gyp.js')` and its comment claimed the
package was "a declared devDependency" — it never was. node-gyp reached the
tree only as a transitive of `@electron/rebuild`, in pnpm's hidden hoist
(`node_modules/.pnpm/node_modules`). pnpm's `.bin` shims export that
directory on NODE_PATH, which is why `pnpm nx …`, `pnpm run build:backend`
and CI kept building the addon, while a plain
`node apps/electron-backend/build-embedded-mpv.js` on a clean install failed
with "Unable to resolve node-gyp".

Declare node-gyp 12.4.0 (the version already in the lockfile store) as a root
devDependency so the resolution no longer depends on a shim implementation
detail, correct the stale comment, and record the contract in the
embedded-MPV architecture doc plus the Agent Bootstrap notes.

No packaged-build change: the no-runtime skip and its
`embedded-mpv-unavailable.txt` marker are untouched.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 11:30:50 +02:00

911 lines
31 KiB
JavaScript

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const {
copyRuntimeToNativeBuild,
findLibMpv,
patchAddonForBundledRuntime,
validateNoForbiddenRuntimeLinks,
} = require('../../tools/packaging/embedded-mpv-packaging.cjs');
const {
removeStaleFrameCopyArtifacts,
} = require('../../tools/packaging/embedded-mpv-frame-copy-files.cjs');
const {
isLinuxSystemBuildInputManifest,
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
} = require('../../tools/embedded-mpv/linux-runtime-manifest.cjs');
const {
SOURCE_ARCHIVE_BINDING_NAME,
validateLinuxSourceArchiveBinding,
} = require('../../tools/embedded-mpv/linux-source-archive-contract.cjs');
const {
resolveLinuxFrameCopyLinkageInputs,
resolveVerifiedLinuxLibMpvSoname,
runWithCleanup,
validateLinuxFrameCopyLinkage,
} = require('./embedded-mpv-linux-linkage.cjs');
const LINUX_PACKAGE_RUNTIME_MODES = Object.freeze(['system', 'bundled']);
const LINUX_STAGED_RUNTIME_ORIGIN = 'vendored-lgpl';
const LINUX_SOURCE_RUNTIME_ORIGIN = 'vendored-lgpl-source-build';
const workspaceRoot = process.cwd();
const addonRoot = path.join(
workspaceRoot,
'apps',
'electron-backend',
'native'
);
const outputDir = path.join(addonRoot, 'build', 'Release');
const outputFile = path.join(outputDir, 'embedded_mpv.node');
const outputLibDir = path.join(outputDir, 'lib');
const distNativeDir = path.join(
workspaceRoot,
'dist',
'apps',
'electron-backend',
'native'
);
const unavailableMarkerFile = path.join(
outputDir,
'embedded-mpv-unavailable.txt'
);
const homebrewIncludeDir = '/opt/homebrew/include';
const homebrewLibDir = '/opt/homebrew/lib';
const targetPlatform =
process.env.IPTVNATOR_EMBEDDED_MPV_PLATFORM || process.platform;
const targetArch =
process.env.IPTVNATOR_EMBEDDED_MPV_ARCH ||
process.env.npm_config_arch ||
process.arch;
const vendoredRuntimeRoot = path.join(
workspaceRoot,
'vendor',
'embedded-mpv',
`${targetPlatform}-${targetArch}`
);
const vendoredIncludeDir = path.join(vendoredRuntimeRoot, 'include');
const vendoredLibDir = path.join(vendoredRuntimeRoot, 'lib');
const vendoredBinDir = path.join(vendoredRuntimeRoot, 'bin');
const homebrewFallbackEnabled =
process.env.IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW === '1';
const embeddedMpvRequired = ['1', 'true', 'yes', 'on'].includes(
(process.env.IPTVNATOR_REQUIRE_EMBEDDED_MPV ?? '').trim().toLowerCase()
);
function log(message) {
process.stdout.write(`[embedded-mpv] ${message}\n`);
}
function cleanOutput() {
fs.rmSync(outputFile, { force: true });
fs.rmSync(outputLibDir, { recursive: true, force: true });
// Do not let an optional/skipped rebuild leave frame-copy artifacts that
// could make startup treat an incomplete runtime as available.
removeStaleFrameCopyArtifacts(outputDir);
for (const windowsDllName of [
'mpv-2.dll',
'libmpv-2.dll',
'mpv.dll',
'libmpv.dll',
]) {
fs.rmSync(path.join(outputDir, windowsDllName), { force: true });
}
fs.rmSync(path.join(outputDir, '.deps'), { recursive: true, force: true });
fs.rmSync(path.join(outputDir, 'obj.target'), {
recursive: true,
force: true,
});
fs.rmSync(path.join(outputDir, 'embedded-mpv-runtime.json'), {
force: true,
});
fs.writeFileSync(
unavailableMarkerFile,
'Embedded MPV native runtime is not available for this build.\n'
);
}
function cleanNativeBuildIntermediates() {
fs.rmSync(outputFile, { force: true });
fs.rmSync(path.join(outputDir, '.deps'), { recursive: true, force: true });
fs.rmSync(path.join(outputDir, 'obj.target'), {
recursive: true,
force: true,
});
}
function cleanDistNativeOutput() {
fs.rmSync(distNativeDir, { recursive: true, force: true });
}
function readRuntimeManifest(runtimeRoot) {
const manifestPath = path.join(runtimeRoot, 'runtime-manifest.json');
if (!fs.existsSync(manifestPath)) {
return {};
}
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
}
function readLinuxSourceArchiveBinding(runtimeRoot, errors) {
const bindingPath = path.join(runtimeRoot, SOURCE_ARCHIVE_BINDING_NAME);
if (!fs.existsSync(bindingPath)) {
return null;
}
let binding;
try {
const stat = fs.lstatSync(bindingPath);
if (!stat.isFile() || stat.isSymbolicLink()) {
throw new Error('binding must be a regular file');
}
binding = JSON.parse(fs.readFileSync(bindingPath, 'utf8'));
} catch (error) {
errors.push(
`source archive binding is invalid: ${
error instanceof Error ? error.message : String(error)
}`
);
return null;
}
errors.push(
...validateLinuxSourceArchiveBinding(binding).map(
(error) => `source archive binding is invalid: ${error}`
)
);
return binding;
}
function validatedLinuxSourceRuntime(runtimeRoot) {
const stagedManifest = readRuntimeManifest(runtimeRoot);
if (
stagedManifest === null ||
typeof stagedManifest !== 'object' ||
Array.isArray(stagedManifest)
) {
throw new Error(
`Staged Linux runtime manifest must be an object: ${path.join(
runtimeRoot,
'runtime-manifest.json'
)}`
);
}
const envelopeErrors = [];
if (stagedManifest.origin !== LINUX_STAGED_RUNTIME_ORIGIN) {
envelopeErrors.push(`origin must be "${LINUX_STAGED_RUNTIME_ORIGIN}"`);
}
if (stagedManifest.platform !== 'linux') {
envelopeErrors.push('platform must be "linux"');
}
if (stagedManifest.arch !== targetArch) {
envelopeErrors.push(`arch must be "${targetArch}"`);
}
if (
typeof stagedManifest.stagedAt !== 'string' ||
stagedManifest.stagedAt.trim().length === 0
) {
envelopeErrors.push('stagedAt must be a non-empty string');
}
if (!Array.isArray(stagedManifest.runtimeFiles)) {
envelopeErrors.push('runtimeFiles must be an array');
}
const systemBuildInputs = isLinuxSystemBuildInputManifest(stagedManifest);
let buildInputMode;
let sourceArchive = null;
let sourceRuntimeManifest;
if (systemBuildInputs) {
buildInputMode = 'system-build-inputs';
sourceRuntimeManifest = {
linuxBackend: stagedManifest.linuxBackend,
buildInputs: stagedManifest.buildInputs,
sourceDistribution: stagedManifest.sourceDistribution,
};
if (
Array.isArray(stagedManifest.runtimeFiles) &&
stagedManifest.runtimeFiles.length !== 0
) {
envelopeErrors.push(
'system build inputs must not declare staged runtime files'
);
}
if (stagedManifest.sourceBuildOrigin !== undefined) {
envelopeErrors.push(
'system build inputs must not declare sourceBuildOrigin'
);
}
} else {
buildInputMode = 'bundled-runtime';
sourceArchive = readLinuxSourceArchiveBinding(
runtimeRoot,
envelopeErrors
);
const sourceBuildOrigin = stagedManifest.sourceBuildOrigin;
const sourceMetadata = { ...stagedManifest };
delete sourceMetadata.sourceBuildOrigin;
delete sourceMetadata.stagedAt;
sourceRuntimeManifest = {
...sourceMetadata,
origin: sourceBuildOrigin,
};
if (sourceBuildOrigin !== LINUX_SOURCE_RUNTIME_ORIGIN) {
envelopeErrors.push(
`sourceBuildOrigin must be "${LINUX_SOURCE_RUNTIME_ORIGIN}"`
);
}
}
const sourceManifestErrors =
buildInputMode === 'system-build-inputs'
? validateLinuxSystemBuildInputManifest(sourceRuntimeManifest)
: validateLinuxRuntimeManifest(sourceRuntimeManifest);
const declaredRuntimeFileNames = Array.isArray(
sourceRuntimeManifest.runtimeFiles
)
? sourceRuntimeManifest.runtimeFiles
.map((runtimeFile) => runtimeFile.name)
.sort()
: [];
const stagedRuntimeFileNames = listRuntimeFiles(
path.join(runtimeRoot, 'lib'),
runtimeFilePredicate
)
.map((runtimeFile) => path.basename(runtimeFile))
.sort();
if (
JSON.stringify(stagedRuntimeFileNames) !==
JSON.stringify(declaredRuntimeFileNames)
) {
envelopeErrors.push(
'staged lib directory must exactly match manifest runtimeFiles'
);
}
const errors = [...envelopeErrors, ...sourceManifestErrors];
if (errors.length > 0) {
throw new Error(
[
`Invalid staged Linux runtime at ${runtimeRoot}:`,
...errors.map((error) => `- ${error}`),
].join('\n')
);
}
return {
buildInputMode,
sourceArchive,
sourceRuntimeManifest,
sourceRuntimeValidated: buildInputMode === 'bundled-runtime',
};
}
function fileExists(filePath) {
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
}
function listRuntimeFiles(runtimeDir, predicate) {
if (!runtimeDir || !fs.existsSync(runtimeDir)) {
return [];
}
return fs
.readdirSync(runtimeDir, { withFileTypes: true })
.filter((entry) => entry.isFile() || entry.isSymbolicLink())
.map((entry) => path.join(runtimeDir, entry.name))
.filter((filePath) => {
try {
return fs.statSync(filePath).isFile();
} catch {
return false;
}
})
.filter(predicate)
.sort();
}
function runtimeFilePredicate(filePath) {
const fileName = path.basename(filePath);
switch (targetPlatform) {
case 'darwin':
return fileName.endsWith('.dylib');
case 'win32':
return (
fileName.endsWith('.dll') ||
fileName.endsWith('.lib') ||
fileName.endsWith('.dll.a')
);
case 'linux':
return /\.so(?:\.\d+)*$/.test(fileName);
default:
return false;
}
}
function findWindowsImportLib(libDir) {
for (const candidate of ['mpv.lib', 'mpv-2.lib', 'libmpv.dll.a']) {
const candidatePath = path.join(libDir, candidate);
if (fileExists(candidatePath)) {
return candidatePath;
}
}
return null;
}
function findWindowsLibMpv(runtimeRoot) {
for (const candidate of [
path.join(runtimeRoot, 'lib', 'mpv-2.dll'),
path.join(runtimeRoot, 'bin', 'mpv-2.dll'),
path.join(runtimeRoot, 'lib', 'libmpv-2.dll'),
path.join(runtimeRoot, 'bin', 'libmpv-2.dll'),
path.join(runtimeRoot, 'lib', 'mpv.dll'),
path.join(runtimeRoot, 'bin', 'mpv.dll'),
path.join(runtimeRoot, 'lib', 'libmpv.dll'),
path.join(runtimeRoot, 'bin', 'libmpv.dll'),
]) {
if (fileExists(candidate)) {
return candidate;
}
}
return null;
}
/* Debian/Ubuntu install linker targets under the multiarch triple dir. The
* compiler's built-in search paths cover it for -l resolution either way.
* This directory is a link-time input only; the helper runtime intentionally
* stays on the sanitized system loader contract. */
function defaultLinuxSystemLibDir() {
const multiarchTriples = {
arm: 'arm-linux-gnueabihf',
arm64: 'aarch64-linux-gnu',
x64: 'x86_64-linux-gnu',
};
const triple = multiarchTriples[targetArch];
if (triple && fs.existsSync(`/usr/lib/${triple}`)) {
return `/usr/lib/${triple}`;
}
return '/usr/lib';
}
function findLinuxLibMpv(libDir) {
for (const candidate of ['libmpv.so.2', 'libmpv.so.1', 'libmpv.so']) {
const candidatePath = path.join(libDir, candidate);
if (fileExists(candidatePath)) {
return candidatePath;
}
}
return null;
}
function resolveRuntime() {
const vendoredHeader = path.join(vendoredIncludeDir, 'mpv', 'client.h');
const stagedLinuxRuntime =
targetPlatform === 'linux' && fs.existsSync(vendoredHeader)
? validatedLinuxSourceRuntime(vendoredRuntimeRoot)
: null;
const vendoredLibMpv =
targetPlatform === 'darwin'
? findLibMpv(vendoredLibDir)
: targetPlatform === 'win32'
? findWindowsLibMpv(vendoredRuntimeRoot)
: targetPlatform === 'linux'
? stagedLinuxRuntime
: null;
if (vendoredLibMpv && fs.existsSync(vendoredHeader)) {
const windowsImportLib =
targetPlatform === 'win32'
? findWindowsImportLib(vendoredLibDir)
: null;
if (targetPlatform === 'win32' && !windowsImportLib) {
return null;
}
return {
origin: 'vendored-lgpl',
includeDir: vendoredIncludeDir,
libDir: vendoredLibDir,
binDir: vendoredBinDir,
manifest: readRuntimeManifest(vendoredRuntimeRoot),
windowsImportLib,
...(stagedLinuxRuntime ?? {}),
};
}
if (targetPlatform === 'linux') {
// Dev-first Linux flow (frame-copy helper links system libmpv): a
// distro libmpv-dev install is a full runtime — no staging needed.
// LIBMPV_INCLUDE_DIR overrides the header path.
// LINUX_NATIVE_LIBRARY_DIR overrides the link-time library directory,
// which must already be visible to the system dynamic loader.
const systemIncludeDir =
process.env.LIBMPV_INCLUDE_DIR || '/usr/include';
if (fs.existsSync(path.join(systemIncludeDir, 'mpv', 'client.h'))) {
const sourceRuntimeManifest = {
linuxBackend: 'process-isolated mpv --wid',
warning: 'Development-only unmanaged system libmpv toolchain.',
};
return {
origin: 'system-dev',
includeDir: systemIncludeDir,
libDir:
process.env.LINUX_NATIVE_LIBRARY_DIR ||
defaultLinuxSystemLibDir(),
binDir: undefined,
manifest: sourceRuntimeManifest,
buildInputMode: 'system-dev',
sourceRuntimeManifest,
sourceRuntimeValidated: false,
windowsImportLib: null,
};
}
}
if (targetPlatform === 'darwin' && homebrewFallbackEnabled) {
const homebrewLibMpv = findLibMpv(homebrewLibDir);
const homebrewHeader = path.join(homebrewIncludeDir, 'mpv', 'client.h');
if (homebrewLibMpv && fs.existsSync(homebrewHeader)) {
log(
'Using Homebrew libmpv as a development-only fallback. Release packaging will reject this runtime.'
);
return {
origin: 'homebrew-dev',
includeDir: homebrewIncludeDir,
libDir: homebrewLibDir,
binDir: undefined,
manifest: {
warning:
'Development-only runtime. Do not ship this in release artifacts.',
},
windowsImportLib: null,
};
}
}
return null;
}
function hasStagedLinuxLibMpvLinkerInput(runtime) {
return (
Array.isArray(runtime.sourceRuntimeManifest?.runtimeFiles) &&
runtime.sourceRuntimeManifest.runtimeFiles.some(
(runtimeFile) => runtimeFile.name === 'libmpv.so'
)
);
}
function assertRequiredLinuxFrameCopyRuntime(runtime) {
if (targetPlatform !== 'linux' || !embeddedMpvRequired) {
return;
}
if (
runtime.buildInputMode !== 'bundled-runtime' ||
runtime.sourceRuntimeValidated !== true ||
!runtime.sourceArchive ||
!hasStagedLinuxLibMpvLinkerInput(runtime)
) {
cleanOutput();
throw new Error(
'Required Linux builds must use the validated bundled source runtime containing staged libmpv.'
);
}
}
function copyLinuxRuntimeClosureToNativeBuild(runtime) {
fs.rmSync(outputLibDir, { recursive: true, force: true });
const declaredRuntimeFiles =
runtime.buildInputMode === 'bundled-runtime'
? runtime.sourceRuntimeManifest.runtimeFiles
: [];
if (declaredRuntimeFiles.length === 0) {
return [];
}
fs.mkdirSync(outputLibDir, { recursive: true });
const copiedRuntimeFiles = [];
for (const runtimeFile of declaredRuntimeFiles) {
const sourcePath = path.join(runtime.libDir, runtimeFile.name);
let descriptor;
try {
descriptor = fs.openSync(
sourcePath,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW
);
const stat = fs.fstatSync(descriptor);
if (!stat.isFile()) {
throw new Error(
`Staged Linux runtime path is not a regular file: ${sourcePath}`
);
}
const contents = fs.readFileSync(descriptor);
if (contents.byteLength !== runtimeFile.size) {
throw new Error(
`Size mismatch for staged Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.size}, received ${contents.byteLength}.`
);
}
const actualSha256 = crypto
.createHash('sha256')
.update(contents)
.digest('hex');
if (actualSha256 !== runtimeFile.sha256) {
throw new Error(
`SHA-256 mismatch for staged Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.sha256}, received ${actualSha256}.`
);
}
const destinationPath = path.join(outputLibDir, runtimeFile.name);
fs.writeFileSync(destinationPath, contents, { mode: 0o755 });
copiedRuntimeFiles.push({ ...runtimeFile });
} finally {
if (descriptor !== undefined) {
fs.closeSync(descriptor);
}
}
}
return copiedRuntimeFiles;
}
function writeLinuxFrameCopyBuildManifest(runtime) {
const copiedRuntimeFiles = copyLinuxRuntimeClosureToNativeBuild(runtime);
const libmpvSoname =
runtime.sourceRuntimeValidated === true &&
runtime.buildInputMode === 'bundled-runtime' &&
copiedRuntimeFiles.length > 0
? resolveVerifiedLinuxLibMpvSoname({
outputLibDir,
runtimeFiles: copiedRuntimeFiles,
runtimeDependencyClosure:
runtime.sourceRuntimeManifest.runtimeDependencyClosure,
readDynamicSection: readLinuxDynamicSection,
})
: null;
const packageRuntimeAvailable =
runtime.sourceRuntimeValidated === true &&
runtime.buildInputMode === 'bundled-runtime' &&
copiedRuntimeFiles.length > 0 &&
libmpvSoname !== null;
const manifest = {
schemaVersion: 1,
origin: 'linux-frame-copy-build',
generatedAt: new Date().toISOString(),
platform: targetPlatform,
arch: targetArch,
buildInputMode: runtime.buildInputMode,
sourceRuntimeValidated: runtime.sourceRuntimeValidated,
allowedPackageRuntimeModes: [...LINUX_PACKAGE_RUNTIME_MODES],
packageRuntimeAvailability: {
system: packageRuntimeAvailable,
bundled: packageRuntimeAvailable,
},
artifacts: {
addon: 'embedded_mpv.node',
frameReader: 'embedded_mpv_frame_reader.node',
helper: 'iptvnator_mpv_helper',
},
processIsolation: {
addonLoadsLibmpv: false,
helperLinksLibmpv: true,
helperRunpath: ['$ORIGIN/lib'],
},
nativeViewFallback: 'process-isolated mpv --wid',
libmpvSoname,
runtimeFiles: copiedRuntimeFiles,
runtimeTotalBytes: copiedRuntimeFiles.reduce(
(total, runtimeFile) => total + runtimeFile.size,
0
),
sourceArchive: runtime.sourceArchive ?? null,
sourceRuntime: runtime.sourceRuntimeManifest,
};
fs.writeFileSync(
path.join(outputDir, 'embedded-mpv-runtime.json'),
`${JSON.stringify(manifest, null, 2)}\n`
);
return manifest;
}
function copyFile(sourcePath, destinationPath) {
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.copyFileSync(sourcePath, destinationPath);
fs.chmodSync(destinationPath, 0o755);
}
function copyGenericRuntimeToNativeBuild(runtime) {
fs.rmSync(outputLibDir, { recursive: true, force: true });
fs.mkdirSync(outputLibDir, { recursive: true });
const runtimeFiles = [
...listRuntimeFiles(runtime.libDir, runtimeFilePredicate),
...listRuntimeFiles(runtime.binDir, runtimeFilePredicate),
];
const copiedFiles = new Set();
for (const runtimeFile of runtimeFiles) {
const fileName = path.basename(runtimeFile);
if (copiedFiles.has(fileName)) {
continue;
}
copyFile(runtimeFile, path.join(outputLibDir, fileName));
copiedFiles.add(fileName);
}
if (targetPlatform === 'win32') {
for (const fileName of copiedFiles) {
if (fileName.endsWith('.dll')) {
copyFile(
path.join(outputLibDir, fileName),
path.join(outputDir, fileName)
);
}
}
}
if (targetPlatform === 'linux') {
const libMpvPath = findLinuxLibMpv(outputLibDir);
if (libMpvPath && path.basename(libMpvPath) !== 'libmpv.so') {
copyFile(libMpvPath, path.join(outputLibDir, 'libmpv.so'));
copiedFiles.add('libmpv.so');
}
}
const manifest = {
origin: runtime.origin,
generatedAt: new Date().toISOString(),
libDir: 'lib',
runtimeFiles: [...copiedFiles].sort(),
...runtime.manifest,
platform: targetPlatform,
targetArch,
};
fs.writeFileSync(
path.join(outputDir, 'embedded-mpv-runtime.json'),
`${JSON.stringify(manifest, null, 2)}\n`
);
return manifest;
}
// Upstream node-gyp, declared as a root devDependency so this resolves from a
// plain `node apps/electron-backend/build-embedded-mpv.js`. Until it was
// declared it was only in the tree as a transitive of `@electron/rebuild`,
// reachable through pnpm's hidden hoist (node_modules/.pnpm/node_modules) —
// which pnpm's `.bin` shims put on NODE_PATH, so `pnpm nx …` and CI worked
// while a direct invocation on a clean install threw. Before that, this
// scanned node_modules/.pnpm for the `@electron/node-gyp` fork, which
// rebuild 4 dropped for upstream `node-gyp`. The Electron target is selected
// through the npm_config_* env below, not by the binary.
function resolveNodeGypBin() {
try {
return require.resolve('node-gyp/bin/node-gyp.js');
} catch (error) {
throw new Error(
`Unable to resolve node-gyp. Is it installed? (${error.message})`
);
}
}
function runNodeGyp(command, env) {
const nodeGypBin = resolveNodeGypBin();
const result = spawnSync(
process.execPath,
[nodeGypBin, command, '--directory', addonRoot],
{
cwd: workspaceRoot,
env,
stdio: 'inherit',
}
);
if (result.status !== 0) {
throw new Error(
`node-gyp ${command} failed with status ${result.status ?? 1}.`
);
}
}
function readLinuxDynamicSection(filePath) {
const result = spawnSync('readelf', ['-d', filePath], {
cwd: workspaceRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
if (result.error) {
throw new Error(
`Unable to run readelf for ${filePath}: ${result.error.message}`
);
}
if (result.status !== 0) {
throw new Error(
`readelf -d failed for ${filePath} with status ${
result.status ?? 1
}: ${(result.stderr ?? '').trim()}`
);
}
return result.stdout;
}
function main() {
fs.mkdirSync(outputDir, { recursive: true });
cleanDistNativeOutput();
cleanOutput();
if (targetPlatform !== process.platform) {
if (embeddedMpvRequired) {
throw new Error(
`Embedded MPV is required for ${targetPlatform}-${targetArch}, but this host is ${process.platform}-${process.arch}.`
);
}
log(
`Skipping build for ${targetPlatform}-${targetArch} on ${process.platform}-${process.arch}.`
);
return;
}
const runtime = resolveRuntime();
if (!runtime) {
cleanOutput();
const message = [
`Skipping build because no embedded MPV runtime was found for ${targetPlatform}-${targetArch}.`,
`Expected vendored runtime at ${vendoredRuntimeRoot}.`,
targetPlatform === 'linux'
? 'Stage Linux MPV build inputs before requiring Embedded MPV on this platform.'
: targetPlatform === 'darwin'
? 'For local development only, set IPTVNATOR_EMBEDDED_MPV_ALLOW_HOMEBREW=1 to use Homebrew libmpv.'
: 'Stage a vendored LGPL-compatible runtime before requiring Embedded MPV on this platform.',
].join('\n');
if (embeddedMpvRequired) {
throw new Error(message);
}
log(message);
return;
}
assertRequiredLinuxFrameCopyRuntime(runtime);
const buildNativeArtifacts = () => {
const runtimeManifest =
targetPlatform === 'darwin'
? copyRuntimeToNativeBuild({
runtimeLibDir: runtime.libDir,
outputLibDir,
runtimeOrigin: runtime.origin,
runtimeManifest: {
targetArch,
...runtime.manifest,
},
})
: targetPlatform === 'linux'
? writeLinuxFrameCopyBuildManifest(runtime)
: copyGenericRuntimeToNativeBuild(runtime);
const linuxLinkageInputs =
targetPlatform === 'linux'
? resolveLinuxFrameCopyLinkageInputs({
buildInputMode: runtime.buildInputMode,
outputLibDir,
packagedLibmpvSoname: runtimeManifest.libmpvSoname,
readDynamicSection: readLinuxDynamicSection,
runtimeLibDir: runtime.libDir,
})
: null;
const electronPackageJson = require(
path.join(workspaceRoot, 'node_modules', 'electron', 'package.json')
);
const electronVersion = electronPackageJson.version;
const env = {
...process.env,
npm_config_runtime: 'electron',
npm_config_target: electronVersion,
npm_config_arch: targetArch,
npm_config_disturl: 'https://electronjs.org/headers',
npm_config_build_from_source: 'true',
npm_config_update_binary: 'false',
LIBMPV_INCLUDE_DIR: runtime.includeDir,
...(targetPlatform === 'linux'
? {
LINUX_VERIFIED_RUNTIME_LIBRARY_DIR:
linuxLinkageInputs.linkerLibraryDir,
}
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
...(runtime.windowsImportLib
? {
LIBMPV_IMPORT_LIB: path.join(
outputLibDir,
path.basename(runtime.windowsImportLib)
),
}
: {}),
};
log(
`Building native addon against Electron ${electronVersion} using ${runtime.origin} runtime for ${targetPlatform}-${targetArch}...`
);
cleanNativeBuildIntermediates();
runNodeGyp('configure', env);
runNodeGyp('build', env);
if (!fs.existsSync(outputFile)) {
throw new Error(`Build finished without producing ${outputFile}.`);
}
if (targetPlatform === 'linux') {
validateLinuxFrameCopyLinkage({
expectedLibmpvSoname: linuxLinkageInputs.expectedLibmpvSoname,
outputDir,
readDynamicSection: readLinuxDynamicSection,
});
}
if (targetPlatform === 'darwin') {
patchAddonForBundledRuntime(outputFile, outputLibDir);
// The frame-copy helper executable links libmpv too and sits next
// to the same lib/ directory, so it gets the identical
// dependency-path rewrite + ad-hoc re-sign.
const frameHelperFile = path.join(
outputDir,
'iptvnator_mpv_helper'
);
if (fs.existsSync(frameHelperFile)) {
patchAddonForBundledRuntime(frameHelperFile, outputLibDir);
}
const forbiddenLinkErrors = validateNoForbiddenRuntimeLinks([
outputFile,
...(fs.existsSync(frameHelperFile) ? [frameHelperFile] : []),
...runtimeManifest.dylibs.map((dylib) =>
path.join(outputLibDir, dylib)
),
]);
if (
runtime.origin === 'vendored-lgpl' &&
forbiddenLinkErrors.length > 0
) {
throw new Error(forbiddenLinkErrors.join('\n'));
}
}
fs.rmSync(unavailableMarkerFile, { force: true });
};
try {
runWithCleanup(buildNativeArtifacts, cleanOutput);
} catch (error) {
if (!embeddedMpvRequired && runtime.origin === 'system-dev') {
// The system-dev fallback triggers on any machine with
// libmpv-dev installed; keep the old graceful-skip contract
// when the rest of the toolchain (EGL/GL/gbm dev packages) is
// missing instead of failing the whole electron build.
log(
`Embedded MPV native build failed with the system-dev toolchain; continuing without embedded MPV. ${
error instanceof Error ? error.message : String(error)
}`
);
return;
}
throw error;
}
log(`Built ${path.relative(workspaceRoot, outputFile)}.`);
}
try {
main();
} catch (error) {
process.stderr.write(
`[embedded-mpv] ${error instanceof Error ? error.message : String(error)}\n`
);
process.exit(1);
}