mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
* feat(embedded-mpv): Linux frame-copy helper via headless EGL Port the frame-copy engine's native layer to Linux (PORTING.md items 1-4): - frame_helper_gl.h: platform GlContext abstraction. macOS keeps the CGL path (moved verbatim); Linux acquires an EGL display in order surfaceless-Mesa -> default display -> GBM render node, binds a 3.2 core desktop-GL context surfaceless (1x1 pbuffer fallback), and hands mpv eglGetProcAddress. The helper's own GL calls link against glvnd libOpenGL, so no display server is required. - frame_shm.h: portable frame_shm_now_ns() (CLOCK_MONOTONIC) shared by the helper and the reader addon, replacing the macOS-only clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); producer and consumer stay on the same clock. - embedded_mpv_frame_reader.c: real implementation now also on __linux__ (the code was already POSIX apart from the clock call). - binding.gyp: OS==linux executable branch for iptvnator_mpv_helper linking system libmpv (-lmpv) + EGL/OpenGL/gbm, with rpaths for $ORIGIN/lib and the build-time library dir. The in-process addon still does not link libmpv - the ban only binds in-process, the helper is out of process. - build-embedded-mpv.js: system-dev fallback on Linux (LIBMPV_INCLUDE_DIR or /usr/include) so a distro libmpv-dev install builds without staging a vendored runtime; a pre-set LINUX_NATIVE_LIBRARY_DIR now wins over the vendored lib dir. Verified on Ubuntu 25.04 / i7-1165G7 (Iris Xe): lavfi smoke per PORTING.md (idle->loading->playing snapshots at 4 Hz, aspect-fit generation bump g1 1280x720 -> g2 960x720 for a 4:3 source), reader probe 60 fps at 1080p60 with 0 torn reads, clean quit with no leaked processes or shm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(embedded-mpv): enable the frame-copy engine gates on Linux Flip the TypeScript side of the Linux port (PORTING.md item 5). A shared dependency-free predicate, isFrameCopyPlatformSupported() (linux any-arch, darwin arm64-only), now backs all four gates so they cannot drift: - main.ts: the persisted Settings toggle promotes to the env flag on Linux too (this runs before window creation and controls the sandbox relax). - EmbeddedMpvNativeService.isFrameCopyEngineActive/isFrameCopyAvailable. - EmbeddedMpvFrameCopyAdapter.isSupported. getSupport() ordering: the frame-copy branch moves above the Linux-only native-engine prerequisites - the X11/Xwayland display-server check and the system-mpv-on-PATH probe only bind the --wid native engine, while the frame-copy helper renders offscreen (headless EGL) and links libmpv itself. createSession() also skips resolving the native window handle for frame-copy sessions, which the adapter ignores anyway, so native-Wayland sessions no longer trip the window-handle assertion. Settings copy: the i18n frame-copy description now says macOS (Apple Silicon) and Linux in all 18 languages; stale macOS-only doc comments in the settings/support interfaces updated alongside. Tests: platform-gate matrix for the adapter (darwin arm64/x64, linux x64/arm64, win32) and service specs covering Linux activation under native Wayland, macOS arm64 staying active, macOS x64 staying native, and the skipped window handle for frame-copy sessions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(packaging): CI + package guards for the Linux frame-copy helper - build-and-make.yaml: install libegl-dev/libgl-dev/libgbm-dev on the Linux runner (the helper's EGL backend needs them now that the helper target builds on Linux), and verify the built helper exists and DOES link libmpv - the inverse of the addon's no-libmpv rule, which still holds and stays validated. - electron-after-pack.cjs: strip iptvnator_mpv_helper from packaged Linux apps. It links the build host's system libmpv, which end-user systems cannot be assumed to have; the support probe treats the missing helper as frame-copy-unavailable (dev-build-only engine until the bundled-runtime staging milestone). - frame_helper_gl.h: log the chosen EGL display tier to stderr (the adapter mirrors helper stderr), so bring-up problems on exotic setups are diagnosable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(embedded-mpv): document the Linux frame-copy port - architecture doc: frame-copy section covers Linux (EGL display tiers, build deps, package strip), Linux support matrix notes the frame-copy exception to the X11 + system-mpv requirements, Linux measured baseline. - RESULTS.md: Ubuntu 25.04 / i7-1165G7 (Iris Xe) measurement rows via the production helper + reader probe; viewport-size claim reproduced. - PORTING.md: Linux marked done with pointers to what changed; Windows remains the open port and its perf gate the open decision. - CLAUDE.md + tools/embedded-mpv/README.md: platform scope, Linux dev build requirements, system-headers fallback, helper strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(embedded-mpv): commit the Linux frame-copy measurement probe linux-frame-probe.mjs reproduces the RESULTS.md Linux rows: spawns the production helper, attaches the frame-reader addon to the announced shm generation, and reports new-frame fps, copy wall time, produce->copy age, torn reads and pixel spread. Usage documented in RESULTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address multi-agent review findings on the Linux port Confirmed findings (each verified by 3 adversarial reviewers): - CI would fail to link the helper: -lOpenGL needs the unversioned glvnd libOpenGL.so, shipped only by libopengl-dev, which neither the runner images nor the previous apt line provide. Added to the workflow and to every documented Linux build-dep list. - The new 'test -x' dist guard could never pass: webpack's dist asset copy drops file modes (helper arrives as 0644). The guard is now 'test -f'; electron-after-pack.cjs restores the execute bit on packaged helpers (also fixes packaged-macOS spawns); the support probe now requires X_OK, so a mode-stripped helper reads as frame-copy-unavailable and falls back to native instead of failing spawn with EACCES. - The Settings frame-copy toggle was unreachable in exactly the Linux states the port targets: the native-Wayland and missing-system-mpv unsupported payloads omitted frameCopyAvailable, and toggle visibility derives solely from it. Both returns now advertise availability. Also from review: - build-embedded-mpv.js keeps the old graceful-skip contract when the new system-dev fallback finds libmpv-dev but the GL/EGL/gbm dev stack is missing (previously such machines skipped; a hard electron-build failure was a regression). - createSession derives the window-handle skip from the dispatched addon instead of re-evaluating the engine gate, so the two cannot disagree. - The render thread logs the GL renderer string (surfaceless Mesa can silently pick llvmpipe on non-Mesa-primary systems; now diagnosable — verified 'Mesa Intel Iris Xe' on this machine). - Specs pin the new semantics: frameCopyAvailable advertised while native is unsupported (Wayland / no mpv), frame-copy supported without a system mpv, and the handle-skip test disposes its session through the owning adapter. - Docs: PORTING.md file map reflects the frame_helper_gl.h seam for the Windows porter; helper-strip removal correctly gated on milestone 4 (bundled libmpv), not milestone 3; RESULTS.md preamble notes the RAW->MONOTONIC clock change; stale '(macOS)' scope comments updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address Greptile/Codex review comments - Sandbox gate requires a usable helper (Greptile P1, security): the main.ts env promotion now also probes for an executable iptvnator_mpv_helper before relaxing the window sandbox — a stale opt-in on packaged Linux (helper deliberately stripped) or after a cleaned native build no longer costs a sandboxless launch for an engine that cannot activate. Helper discovery (addon candidate paths + X_OK probe) moved into embedded-mpv-frame-copy-platform.util.ts, shared by main.ts and the service; the service keeps thin instance wrappers so tests can stub per scenario. New util spec pins the platform matrix, candidate resolution, and the execute-bit semantics. - Stale frame-copy artifacts on skipped builds (Codex P2): cleanOutput() now also removes iptvnator_mpv_helper and embedded_mpv_frame_reader.node, so a failed/skipped rebuild cannot leave a previous helper advertising frame-copy support against a runtime the build just declared unavailable. - Multiarch default lib dir (Greptile P1, partially refuted): -l resolution never depended on our -L (the compiler's built-in search paths include the Debian/Ubuntu multiarch dir — proven by the green CI run linking with a nonexistent -L dir), but the system-dev fallback now defaults to /usr/lib/<multiarch-triple> when present so the -L flag and the helper's baked rpath point somewhere real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): harden Linux frame-copy port --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
576 lines
18 KiB
JavaScript
576 lines
18 KiB
JavaScript
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 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 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 keeps the -L flag and the helper's baked rpath pointing somewhere
|
|
* real. */
|
|
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 vendoredLibMpv =
|
|
targetPlatform === 'darwin'
|
|
? findLibMpv(vendoredLibDir)
|
|
: targetPlatform === 'win32'
|
|
? findWindowsLibMpv(vendoredRuntimeRoot)
|
|
: targetPlatform === 'linux'
|
|
? true
|
|
: null;
|
|
const vendoredHeader = path.join(vendoredIncludeDir, 'mpv', 'client.h');
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
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 / LINUX_NATIVE_LIBRARY_DIR override the system
|
|
// paths for machines with a local (non-root) libmpv prefix.
|
|
const systemIncludeDir =
|
|
process.env.LIBMPV_INCLUDE_DIR || '/usr/include';
|
|
if (fs.existsSync(path.join(systemIncludeDir, 'mpv', 'client.h'))) {
|
|
return {
|
|
origin: 'system-dev',
|
|
includeDir: systemIncludeDir,
|
|
libDir:
|
|
process.env.LINUX_NATIVE_LIBRARY_DIR ||
|
|
defaultLinuxSystemLibDir(),
|
|
binDir: undefined,
|
|
manifest: {
|
|
warning:
|
|
'Development-only system libmpv toolchain. Release packaging keeps the external-mpv-process contract.',
|
|
},
|
|
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 writeLinuxProcessRuntimeManifest(runtime) {
|
|
fs.rmSync(outputLibDir, { recursive: true, force: true });
|
|
|
|
const manifest = {
|
|
...runtime.manifest,
|
|
origin: 'external-mpv-process',
|
|
generatedAt: new Date().toISOString(),
|
|
runtimeFiles: [],
|
|
linuxBackend:
|
|
runtime.manifest.linuxBackend ?? 'process-isolated mpv --wid',
|
|
mpvExecutable: 'mpv',
|
|
platform: targetPlatform,
|
|
targetArch,
|
|
};
|
|
|
|
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;
|
|
}
|
|
|
|
function resolveElectronNodeGypBin() {
|
|
const pnpmRoot = path.join(workspaceRoot, 'node_modules', '.pnpm');
|
|
|
|
if (!fs.existsSync(pnpmRoot)) {
|
|
throw new Error('Unable to find node_modules/.pnpm.');
|
|
}
|
|
|
|
const packageDirs = fs
|
|
.readdirSync(pnpmRoot, { withFileTypes: true })
|
|
.filter(
|
|
(entry) =>
|
|
entry.isDirectory() &&
|
|
entry.name.startsWith('@electron+node-gyp@')
|
|
)
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
|
|
for (const packageDir of packageDirs) {
|
|
const candidate = path.join(
|
|
pnpmRoot,
|
|
packageDir,
|
|
'node_modules',
|
|
'@electron',
|
|
'node-gyp',
|
|
'bin',
|
|
'node-gyp.js'
|
|
);
|
|
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
throw new Error('Unable to resolve @electron/node-gyp.');
|
|
}
|
|
|
|
function runNodeGyp(command, env) {
|
|
const nodeGypBin = resolveElectronNodeGypBin();
|
|
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 main() {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
cleanDistNativeOutput();
|
|
|
|
if (targetPlatform !== process.platform) {
|
|
cleanOutput();
|
|
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;
|
|
}
|
|
|
|
const runtimeManifest =
|
|
targetPlatform === 'darwin'
|
|
? copyRuntimeToNativeBuild({
|
|
runtimeLibDir: runtime.libDir,
|
|
outputLibDir,
|
|
runtimeOrigin: runtime.origin,
|
|
runtimeManifest: {
|
|
targetArch,
|
|
...runtime.manifest,
|
|
},
|
|
})
|
|
: targetPlatform === 'linux'
|
|
? writeLinuxProcessRuntimeManifest(runtime)
|
|
: copyGenericRuntimeToNativeBuild(runtime);
|
|
fs.rmSync(unavailableMarkerFile, { force: true });
|
|
|
|
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_NATIVE_LIBRARY_DIR:
|
|
process.env.LINUX_NATIVE_LIBRARY_DIR || runtime.libDir,
|
|
}
|
|
: { 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();
|
|
try {
|
|
runNodeGyp('configure', env);
|
|
runNodeGyp('build', env);
|
|
} 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)
|
|
}`
|
|
);
|
|
cleanOutput();
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (!fs.existsSync(outputFile)) {
|
|
throw new Error(`Build finished without producing ${outputFile}.`);
|
|
}
|
|
|
|
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'));
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|