mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
- Introduced tooling for building and staging the macOS `libmpv` runtime for IPTVnator's embedded MPV player. - Added `build-macos-runtime.mjs` for building an LGPL-compatible runtime from source. - Created `stage-macos-runtime.mjs` for staging the built runtime artifacts. - Implemented validation for the packaged embedded MPV runtime in `electron-after-pack.cjs` and `embedded-mpv-macos.cjs`. - Updated packaging scripts to ensure the embedded MPV runtime is correctly integrated and validated during the build process. - Added README files to document the expected layout and usage for the embedded MPV runtime artifacts. Entire-Checkpoint: c6e522b4276c
476 lines
14 KiB
JavaScript
476 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
import crypto from 'crypto';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import { spawnSync } from 'child_process';
|
|
|
|
const args = process.argv.slice(2);
|
|
const [arch, rawPrefix] = args;
|
|
const validArchitectures = new Set(['arm64', 'x64']);
|
|
const macosDeploymentTarget =
|
|
process.env.MACOSX_DEPLOYMENT_TARGET ?? '11.0';
|
|
const workspaceRoot = process.cwd();
|
|
|
|
const sourcePackages = [
|
|
{
|
|
id: 'freetype',
|
|
version: '2.13.3',
|
|
url: 'https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz',
|
|
license: 'FreeType License or GPL-2.0-or-later',
|
|
},
|
|
{
|
|
id: 'fribidi',
|
|
version: '1.0.16',
|
|
url: 'https://github.com/fribidi/fribidi/releases/download/v1.0.16/fribidi-1.0.16.tar.xz',
|
|
license: 'LGPL-2.1-or-later',
|
|
},
|
|
{
|
|
id: 'harfbuzz',
|
|
version: '8.5.0',
|
|
url: 'https://github.com/harfbuzz/harfbuzz/releases/download/8.5.0/harfbuzz-8.5.0.tar.xz',
|
|
license: 'MIT',
|
|
},
|
|
{
|
|
id: 'libass',
|
|
version: '0.17.3',
|
|
url: 'https://github.com/libass/libass/releases/download/0.17.3/libass-0.17.3.tar.xz',
|
|
license: 'ISC',
|
|
},
|
|
{
|
|
id: 'ffmpeg',
|
|
version: '8.1',
|
|
url: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
|
|
license: 'LGPL-compatible configuration',
|
|
},
|
|
{
|
|
id: 'libplacebo',
|
|
version: '7.360.1',
|
|
url: 'https://github.com/haasn/libplacebo/archive/refs/tags/v7.360.1.tar.gz',
|
|
license: 'LGPL-2.1-or-later',
|
|
},
|
|
{
|
|
id: 'mpv',
|
|
version: '0.41.0',
|
|
url: 'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz',
|
|
license: 'LGPL-compatible configuration with -Dgpl=false',
|
|
},
|
|
];
|
|
|
|
if (process.platform !== 'darwin') {
|
|
console.error('Embedded MPV runtime builds are supported on macOS only.');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!validArchitectures.has(arch) || !rawPrefix) {
|
|
console.error(
|
|
[
|
|
'Usage: node tools/embedded-mpv/build-macos-runtime.mjs <arm64|x64> <output-prefix>',
|
|
'',
|
|
'Builds a pinned LGPL-compatible macOS libmpv runtime from source.',
|
|
].join('\n')
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const prefix = path.resolve(rawPrefix);
|
|
const buildRoot = path.resolve(
|
|
process.env.IPTVNATOR_EMBEDDED_MPV_BUILD_ROOT ??
|
|
path.join(os.tmpdir(), 'iptvnator-embedded-mpv-runtime', arch)
|
|
);
|
|
const archiveRoot = path.join(buildRoot, 'archives');
|
|
const sourceRoot = path.join(buildRoot, 'sources');
|
|
const packageById = new Map(sourcePackages.map((source) => [source.id, source]));
|
|
const parallelism =
|
|
process.env.MAKEFLAGS?.match(/-j\s*(\d+)/)?.[1] ??
|
|
String(os.cpus().length);
|
|
|
|
const ffmpegConfigureFlags = [
|
|
`--prefix=${prefix}`,
|
|
'--enable-shared',
|
|
'--disable-static',
|
|
'--disable-doc',
|
|
'--disable-debug',
|
|
'--disable-programs',
|
|
'--disable-autodetect',
|
|
'--disable-gpl',
|
|
'--disable-nonfree',
|
|
'--enable-pic',
|
|
'--enable-securetransport',
|
|
'--enable-audiotoolbox',
|
|
'--enable-videotoolbox',
|
|
];
|
|
|
|
const mpvMesonFlags = [
|
|
'-Dgpl=false',
|
|
'-Dlibmpv=true',
|
|
'-Dcplayer=false',
|
|
'-Dbuild-date=false',
|
|
'-Dtests=false',
|
|
'-Dlua=disabled',
|
|
'-Djavascript=disabled',
|
|
'-Dcplugins=disabled',
|
|
'-Dmanpage-build=disabled',
|
|
'-Dhtml-build=disabled',
|
|
'-Dpdf-build=disabled',
|
|
'-Dlibarchive=disabled',
|
|
'-Dlibbluray=disabled',
|
|
'-Ddvdnav=disabled',
|
|
'-Dcdda=disabled',
|
|
'-Ddvbin=disabled',
|
|
'-Djpeg=disabled',
|
|
'-Dlcms2=disabled',
|
|
'-Drubberband=disabled',
|
|
'-Duchardet=disabled',
|
|
'-Dzimg=disabled',
|
|
'-Dvulkan=disabled',
|
|
'-Dshaderc=disabled',
|
|
'-Dspirv-cross=disabled',
|
|
'-Dcocoa=disabled',
|
|
'-Dgl-cocoa=disabled',
|
|
'-Dmacos-cocoa-cb=disabled',
|
|
'-Dswift-build=disabled',
|
|
'-Dplain-gl=enabled',
|
|
];
|
|
|
|
function log(message) {
|
|
process.stdout.write(`[embedded-mpv-runtime] ${message}\n`);
|
|
}
|
|
|
|
function run(command, commandArgs, options = {}) {
|
|
log(`${command} ${commandArgs.join(' ')}`);
|
|
const result = spawnSync(command, commandArgs, {
|
|
cwd: options.cwd ?? workspaceRoot,
|
|
env: options.env ?? buildEnv(),
|
|
stdio: 'inherit',
|
|
...options,
|
|
});
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(
|
|
`${command} ${commandArgs.join(' ')} failed with status ${
|
|
result.status ?? 1
|
|
}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function commandExists(command) {
|
|
const result = spawnSync('sh', ['-lc', `command -v ${command}`], {
|
|
stdio: 'ignore',
|
|
});
|
|
return result.status === 0;
|
|
}
|
|
|
|
function buildEnv() {
|
|
const pkgConfigDirs = [
|
|
path.join(prefix, 'lib', 'pkgconfig'),
|
|
path.join(prefix, 'share', 'pkgconfig'),
|
|
].join(path.delimiter);
|
|
|
|
return {
|
|
...process.env,
|
|
PATH: [path.join(prefix, 'bin'), process.env.PATH]
|
|
.filter(Boolean)
|
|
.join(path.delimiter),
|
|
PKG_CONFIG_PATH: pkgConfigDirs,
|
|
PKG_CONFIG_LIBDIR: pkgConfigDirs,
|
|
CMAKE_PREFIX_PATH: prefix,
|
|
DYLD_LIBRARY_PATH: path.join(prefix, 'lib'),
|
|
MACOSX_DEPLOYMENT_TARGET: macosDeploymentTarget,
|
|
CFLAGS: [`-I${path.join(prefix, 'include')}`, process.env.CFLAGS]
|
|
.filter(Boolean)
|
|
.join(' '),
|
|
LDFLAGS: [`-L${path.join(prefix, 'lib')}`, process.env.LDFLAGS]
|
|
.filter(Boolean)
|
|
.join(' '),
|
|
};
|
|
}
|
|
|
|
function ensureTools() {
|
|
const requiredCommands = [
|
|
'curl',
|
|
'tar',
|
|
'make',
|
|
'meson',
|
|
'ninja',
|
|
'pkg-config',
|
|
];
|
|
const missing = requiredCommands.filter((command) => !commandExists(command));
|
|
|
|
if (missing.length > 0) {
|
|
throw new Error(`Missing required build tools: ${missing.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
function archivePathFor(sourcePackage) {
|
|
const extension = sourcePackage.url.endsWith('.tar.xz')
|
|
? '.tar.xz'
|
|
: '.tar.gz';
|
|
return path.join(
|
|
archiveRoot,
|
|
`${sourcePackage.id}-${sourcePackage.version}${extension}`
|
|
);
|
|
}
|
|
|
|
function sourcePathFor(packageId) {
|
|
return path.join(sourceRoot, packageId);
|
|
}
|
|
|
|
function sha256File(filePath) {
|
|
const hash = crypto.createHash('sha256');
|
|
hash.update(fs.readFileSync(filePath));
|
|
return hash.digest('hex');
|
|
}
|
|
|
|
function downloadSources() {
|
|
fs.mkdirSync(archiveRoot, { recursive: true });
|
|
fs.mkdirSync(sourceRoot, { recursive: true });
|
|
|
|
for (const sourcePackage of sourcePackages) {
|
|
const archivePath = archivePathFor(sourcePackage);
|
|
if (!fs.existsSync(archivePath)) {
|
|
run('curl', [
|
|
'-fL',
|
|
'--retry',
|
|
'3',
|
|
'--retry-delay',
|
|
'5',
|
|
'-o',
|
|
archivePath,
|
|
sourcePackage.url,
|
|
]);
|
|
}
|
|
|
|
const packageSourcePath = sourcePathFor(sourcePackage.id);
|
|
fs.rmSync(packageSourcePath, { recursive: true, force: true });
|
|
fs.mkdirSync(packageSourcePath, { recursive: true });
|
|
run('tar', [
|
|
'-xf',
|
|
archivePath,
|
|
'-C',
|
|
packageSourcePath,
|
|
'--strip-components',
|
|
'1',
|
|
]);
|
|
sourcePackage.sha256 = sha256File(archivePath);
|
|
}
|
|
}
|
|
|
|
function configureMakeInstall(packageId, configureArgs) {
|
|
const packageSourcePath = sourcePathFor(packageId);
|
|
run('./configure', [`--prefix=${prefix}`, ...configureArgs], {
|
|
cwd: packageSourcePath,
|
|
});
|
|
run('make', [`-j${parallelism}`], { cwd: packageSourcePath });
|
|
run('make', ['install'], { cwd: packageSourcePath });
|
|
}
|
|
|
|
function mesonInstall(packageId, mesonArgs) {
|
|
const packageSourcePath = sourcePathFor(packageId);
|
|
const buildDir = path.join(packageSourcePath, 'build-iptvnator');
|
|
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
run(
|
|
'meson',
|
|
[
|
|
'setup',
|
|
buildDir,
|
|
`--prefix=${prefix}`,
|
|
'--libdir=lib',
|
|
'--buildtype=release',
|
|
'--default-library=shared',
|
|
...mesonArgs,
|
|
],
|
|
{ cwd: packageSourcePath }
|
|
);
|
|
run('meson', ['compile', '-C', buildDir], { cwd: packageSourcePath });
|
|
run('meson', ['install', '-C', buildDir], { cwd: packageSourcePath });
|
|
}
|
|
|
|
function buildRuntime() {
|
|
fs.rmSync(prefix, { recursive: true, force: true });
|
|
fs.mkdirSync(prefix, { recursive: true });
|
|
|
|
configureMakeInstall('freetype', ['--enable-shared', '--disable-static']);
|
|
configureMakeInstall('fribidi', ['--enable-shared', '--disable-static']);
|
|
mesonInstall('harfbuzz', [
|
|
'-Dglib=disabled',
|
|
'-Dgobject=disabled',
|
|
'-Dcairo=disabled',
|
|
'-Dchafa=disabled',
|
|
'-Dicu=disabled',
|
|
'-Dfreetype=enabled',
|
|
'-Dtests=disabled',
|
|
'-Dintrospection=disabled',
|
|
'-Ddocs=disabled',
|
|
'-Dutilities=disabled',
|
|
'-Dbenchmark=disabled',
|
|
]);
|
|
configureMakeInstall('libass', [
|
|
'--enable-shared',
|
|
'--disable-static',
|
|
'--disable-fontconfig',
|
|
'--enable-coretext',
|
|
'--disable-libunibreak',
|
|
]);
|
|
|
|
const ffmpegSourcePath = sourcePathFor('ffmpeg');
|
|
run('./configure', ffmpegConfigureFlags, { cwd: ffmpegSourcePath });
|
|
run('make', [`-j${parallelism}`], { cwd: ffmpegSourcePath });
|
|
run('make', ['install'], { cwd: ffmpegSourcePath });
|
|
|
|
mesonInstall('libplacebo', [
|
|
'-Dopengl=enabled',
|
|
'-Dvulkan=disabled',
|
|
'-Dvk-proc-addr=disabled',
|
|
'-Dglslang=disabled',
|
|
'-Dshaderc=disabled',
|
|
'-Dlcms=disabled',
|
|
'-Ddovi=disabled',
|
|
'-Dlibdovi=disabled',
|
|
'-Ddemos=false',
|
|
'-Dtests=false',
|
|
'-Dbench=false',
|
|
'-Dfuzz=false',
|
|
'-Dunwind=disabled',
|
|
'-Dxxhash=disabled',
|
|
]);
|
|
mesonInstall('mpv', mpvMesonFlags);
|
|
}
|
|
|
|
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 validateRuntimeLinks() {
|
|
if (!commandExists('otool')) {
|
|
log('Skipping otool validation because otool is unavailable.');
|
|
return;
|
|
}
|
|
|
|
const libDir = path.join(prefix, 'lib');
|
|
const errors = [];
|
|
const allowedSystemPrefixes = ['/System/Library/', '/usr/lib/'];
|
|
const forbiddenPrefixes = ['/opt/homebrew/', '/usr/local/'];
|
|
|
|
for (const dylibPath of listDylibs(libDir)) {
|
|
const result = spawnSync('otool', ['-L', dylibPath], {
|
|
encoding: 'utf8',
|
|
stdio: 'pipe',
|
|
});
|
|
if (result.status !== 0) {
|
|
errors.push(`Unable to inspect ${dylibPath}: ${result.stderr}`);
|
|
continue;
|
|
}
|
|
|
|
for (const dependencyPath of result.stdout
|
|
.split(/\r?\n/)
|
|
.slice(1)
|
|
.map((line) => line.trim().split(/\s+\(/)[0])
|
|
.filter(Boolean)) {
|
|
if (
|
|
allowedSystemPrefixes.some((prefixValue) =>
|
|
dependencyPath.startsWith(prefixValue)
|
|
) ||
|
|
dependencyPath.startsWith('@loader_path/') ||
|
|
dependencyPath.startsWith('@rpath/') ||
|
|
dependencyPath.startsWith(prefix)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
forbiddenPrefixes.some((prefixValue) =>
|
|
dependencyPath.startsWith(prefixValue)
|
|
)
|
|
) {
|
|
errors.push(`${dylibPath} links to forbidden ${dependencyPath}`);
|
|
continue;
|
|
}
|
|
|
|
if (path.isAbsolute(dependencyPath)) {
|
|
errors.push(`${dylibPath} links to external ${dependencyPath}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
throw new Error(
|
|
['Embedded MPV runtime link validation failed.', ...errors].join('\n')
|
|
);
|
|
}
|
|
}
|
|
|
|
function sourceMetadata(packageId) {
|
|
const sourcePackage = packageById.get(packageId);
|
|
return {
|
|
version: sourcePackage.version,
|
|
sourceUrl: sourcePackage.url,
|
|
sourceSha256: sourcePackage.sha256,
|
|
license: sourcePackage.license,
|
|
};
|
|
}
|
|
|
|
function writeManifest() {
|
|
const manifest = {
|
|
origin: 'vendored-lgpl-source-build',
|
|
arch,
|
|
generatedAt: new Date().toISOString(),
|
|
macosDeploymentTarget,
|
|
buildHost: {
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
},
|
|
packages: Object.fromEntries(
|
|
sourcePackages.map((sourcePackage) => [
|
|
sourcePackage.id,
|
|
{
|
|
version: sourcePackage.version,
|
|
sourceUrl: sourcePackage.url,
|
|
sourceSha256: sourcePackage.sha256,
|
|
license: sourcePackage.license,
|
|
},
|
|
])
|
|
),
|
|
ffmpeg: {
|
|
...sourceMetadata('ffmpeg'),
|
|
licensePolicy: 'LGPL, built without --enable-gpl and --enable-nonfree',
|
|
configureFlags: ffmpegConfigureFlags,
|
|
},
|
|
mpv: {
|
|
...sourceMetadata('mpv'),
|
|
licensePolicy:
|
|
'LGPL-compatible libmpv, built with -Dlibmpv=true -Dgpl=false',
|
|
mesonFlags: mpvMesonFlags,
|
|
},
|
|
sourceDistribution:
|
|
'Attach the downloaded source archives, this manifest, and any local patches with the macOS binary release.',
|
|
};
|
|
|
|
fs.writeFileSync(
|
|
path.join(prefix, 'runtime-manifest.json'),
|
|
`${JSON.stringify(manifest, null, 2)}\n`
|
|
);
|
|
}
|
|
|
|
try {
|
|
ensureTools();
|
|
fs.mkdirSync(buildRoot, { recursive: true });
|
|
downloadSources();
|
|
buildRuntime();
|
|
validateRuntimeLinks();
|
|
writeManifest();
|
|
log(`Built LGPL-compatible runtime prefix at ${prefix}`);
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
}
|