mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
938 lines
28 KiB
JavaScript
938 lines
28 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const SOURCE_PACKAGES = Object.freeze(
|
|
[
|
|
{
|
|
id: 'freetype',
|
|
version: '2.13.3',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz',
|
|
expectedSha256:
|
|
'0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289',
|
|
license: 'FreeType License (FTL)',
|
|
},
|
|
{
|
|
id: 'fribidi',
|
|
version: '1.0.16',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://github.com/fribidi/fribidi/releases/download/v1.0.16/fribidi-1.0.16.tar.xz',
|
|
expectedSha256:
|
|
'1b1cde5b235d40479e91be2f0e88a309e3214c8ab470ec8a2744d82a5a9ea05c',
|
|
license: 'LGPL-2.1-or-later',
|
|
},
|
|
{
|
|
id: 'harfbuzz',
|
|
version: '8.5.0',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://github.com/harfbuzz/harfbuzz/releases/download/8.5.0/harfbuzz-8.5.0.tar.xz',
|
|
expectedSha256:
|
|
'77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27',
|
|
license: 'MIT',
|
|
},
|
|
{
|
|
id: 'expat',
|
|
version: '2.8.2',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://github.com/libexpat/libexpat/releases/download/R_2_8_2/expat-2.8.2.tar.xz',
|
|
expectedSha256:
|
|
'3ad89b8588e6644bd4e49981480d48b21289eebbcd4f0a1a4afb1c29f99b6ab4',
|
|
license: 'MIT',
|
|
},
|
|
{
|
|
id: 'fontconfig',
|
|
version: '2.16.0',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.16.0.tar.xz',
|
|
expectedSha256:
|
|
'6a33dc555cc9ba8b10caf7695878ef134eeb36d0af366041f639b1da9b6ed220',
|
|
license: 'MIT',
|
|
},
|
|
{
|
|
id: 'libass',
|
|
version: '0.17.3',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://github.com/libass/libass/releases/download/0.17.3/libass-0.17.3.tar.xz',
|
|
expectedSha256:
|
|
'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959',
|
|
license: 'ISC',
|
|
},
|
|
{
|
|
id: 'openssl',
|
|
version: '3.5.7',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://github.com/openssl/openssl/releases/download/openssl-3.5.7/openssl-3.5.7.tar.gz',
|
|
expectedSha256:
|
|
'a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8',
|
|
license: 'Apache-2.0',
|
|
},
|
|
{
|
|
id: 'ffmpeg',
|
|
version: '8.1',
|
|
sourceKind: 'archive',
|
|
sourceUrl: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
|
|
expectedSha256:
|
|
'b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a',
|
|
license: 'LGPL-2.1-or-later',
|
|
},
|
|
{
|
|
id: 'libplacebo',
|
|
version: '7.360.1',
|
|
sourceKind: 'git',
|
|
sourceUrl: 'https://github.com/haasn/libplacebo.git',
|
|
sourceTag: 'v7.360.1',
|
|
expectedGitCommit: 'cee9b076f2c63104ccfd497fa79c39a867293ec4',
|
|
license: 'LGPL-2.1-or-later',
|
|
},
|
|
{
|
|
id: 'mpv',
|
|
version: '0.41.0',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz',
|
|
expectedSha256:
|
|
'ee21092a5ee427353392360929dc64645c54479aefdb5babc5cfbb5fad626209',
|
|
license: 'LGPL-2.1-or-later with -Dgpl=false',
|
|
},
|
|
].map((sourcePackage) => Object.freeze(sourcePackage))
|
|
);
|
|
|
|
const BUILD_ORDER = Object.freeze(
|
|
SOURCE_PACKAGES.map((sourcePackage) => sourcePackage.id)
|
|
);
|
|
|
|
const FFMPEG_CONFIGURE_FLAGS = Object.freeze([
|
|
'--enable-shared',
|
|
'--disable-static',
|
|
'--disable-programs',
|
|
'--disable-doc',
|
|
'--disable-debug',
|
|
'--disable-autodetect',
|
|
'--disable-gpl',
|
|
'--disable-nonfree',
|
|
'--disable-version3',
|
|
'--enable-pic',
|
|
'--enable-pthreads',
|
|
'--enable-openssl',
|
|
'--disable-gnutls',
|
|
'--disable-mbedtls',
|
|
'--disable-libtls',
|
|
'--enable-network',
|
|
'--disable-protocols',
|
|
'--enable-protocol=file',
|
|
'--enable-protocol=http',
|
|
'--enable-protocol=https',
|
|
'--enable-protocol=httpproxy',
|
|
'--enable-protocol=tcp',
|
|
'--enable-protocol=tls',
|
|
'--enable-protocol=udp',
|
|
'--enable-protocol=crypto',
|
|
'--enable-protocol=data',
|
|
'--enable-demuxer=hls',
|
|
'--enable-vaapi',
|
|
'--disable-vdpau',
|
|
'--disable-vulkan',
|
|
'--disable-libdrm',
|
|
'--disable-cuda-llvm',
|
|
'--disable-cuvid',
|
|
'--disable-nvdec',
|
|
'--disable-nvenc',
|
|
'--disable-xlib',
|
|
'--disable-sdl2',
|
|
'--disable-openal',
|
|
]);
|
|
|
|
const MPV_MESON_FLAGS = Object.freeze([
|
|
'-Dgpl=false',
|
|
'-Dlibmpv=true',
|
|
'-Dcplayer=false',
|
|
'-Dbuild-date=false',
|
|
'-Dtests=false',
|
|
'-Dfuzzers=false',
|
|
'-Ddisable-packet-pool=false',
|
|
'-Dcdda=disabled',
|
|
'-Dcplugins=disabled',
|
|
'-Ddvbin=disabled',
|
|
'-Ddvdnav=disabled',
|
|
'-Diconv=enabled',
|
|
'-Djavascript=disabled',
|
|
'-Djpeg=disabled',
|
|
'-Dlcms2=disabled',
|
|
'-Dlibarchive=disabled',
|
|
'-Dlibavdevice=disabled',
|
|
'-Dlibbluray=disabled',
|
|
'-Dlua=disabled',
|
|
'-Dpthread-debug=disabled',
|
|
'-Drubberband=disabled',
|
|
'-Dsdl2-gamepad=disabled',
|
|
'-Duchardet=disabled',
|
|
'-Duwp=disabled',
|
|
'-Dvapoursynth=disabled',
|
|
'-Dvector=enabled',
|
|
'-Dwin32-threads=disabled',
|
|
'-Dx11-clipboard=disabled',
|
|
'-Dzimg=disabled',
|
|
'-Dzlib=disabled',
|
|
'-Dalsa=enabled',
|
|
'-Daudiounit=disabled',
|
|
'-Dcoreaudio=disabled',
|
|
'-Davfoundation=disabled',
|
|
'-Djack=disabled',
|
|
'-Dopenal=disabled',
|
|
'-Daudiotrack=disabled',
|
|
'-Daaudio=disabled',
|
|
'-Dopensles=disabled',
|
|
'-Doss-audio=disabled',
|
|
'-Dpipewire=disabled',
|
|
'-Dpulse=enabled',
|
|
'-Dsdl2-audio=disabled',
|
|
'-Dsndio=disabled',
|
|
'-Dwasapi=disabled',
|
|
'-Dcaca=disabled',
|
|
'-Dcocoa=disabled',
|
|
'-Dd3d11=disabled',
|
|
'-Ddirect3d=disabled',
|
|
'-Ddmabuf-wayland=disabled',
|
|
'-Ddrm=disabled',
|
|
'-Degl=enabled',
|
|
'-Degl-android=disabled',
|
|
'-Degl-angle=disabled',
|
|
'-Degl-angle-lib=disabled',
|
|
'-Degl-angle-win32=disabled',
|
|
'-Degl-drm=disabled',
|
|
'-Degl-wayland=disabled',
|
|
'-Degl-x11=disabled',
|
|
'-Dgbm=enabled',
|
|
'-Dgl=enabled',
|
|
'-Dgl-cocoa=disabled',
|
|
'-Dgl-dxinterop=disabled',
|
|
'-Dgl-win32=disabled',
|
|
'-Dgl-x11=disabled',
|
|
'-Dsdl2-video=disabled',
|
|
'-Dshaderc=disabled',
|
|
'-Dsixel=disabled',
|
|
'-Dspirv-cross=disabled',
|
|
'-Dplain-gl=enabled',
|
|
'-Dvdpau=disabled',
|
|
'-Dvdpau-gl-x11=disabled',
|
|
'-Dvaapi=enabled',
|
|
'-Dvaapi-drm=enabled',
|
|
'-Dvaapi-wayland=disabled',
|
|
'-Dvaapi-win32=disabled',
|
|
'-Dvaapi-x11=disabled',
|
|
'-Dvulkan=disabled',
|
|
'-Dwayland=disabled',
|
|
'-Dx11=disabled',
|
|
'-Dxv=disabled',
|
|
'-Dandroid-media-ndk=disabled',
|
|
'-Dcuda-hwaccel=disabled',
|
|
'-Dcuda-interop=disabled',
|
|
'-Dd3d-hwaccel=disabled',
|
|
'-Dd3d9-hwaccel=disabled',
|
|
'-Dgl-dxinterop-d3d9=disabled',
|
|
'-Dios-gl=disabled',
|
|
'-Dvideotoolbox-gl=disabled',
|
|
'-Dvideotoolbox-pl=disabled',
|
|
'-Dmacos-10-15-4-features=disabled',
|
|
'-Dmacos-11-features=disabled',
|
|
'-Dmacos-11-3-features=disabled',
|
|
'-Dmacos-12-features=disabled',
|
|
'-Dmacos-cocoa-cb=disabled',
|
|
'-Dmacos-media-player=disabled',
|
|
'-Dmacos-touchbar=disabled',
|
|
'-Dswift-build=disabled',
|
|
'-Dwin32-smtc=disabled',
|
|
'-Dhtml-build=disabled',
|
|
'-Dmanpage-build=disabled',
|
|
'-Dpdf-build=disabled',
|
|
]);
|
|
|
|
const BUILD_RECIPES = Object.freeze({
|
|
freetype: Object.freeze({
|
|
buildSystem: 'configure',
|
|
sharedOnly: true,
|
|
args: Object.freeze([
|
|
'--enable-shared',
|
|
'--disable-static',
|
|
'--without-brotli',
|
|
'--without-bzip2',
|
|
'--without-harfbuzz',
|
|
'--without-png',
|
|
'--without-zlib',
|
|
]),
|
|
}),
|
|
fribidi: Object.freeze({
|
|
buildSystem: 'configure',
|
|
sharedOnly: true,
|
|
args: Object.freeze([
|
|
'--enable-shared',
|
|
'--disable-static',
|
|
'--disable-docs',
|
|
'--disable-bin',
|
|
]),
|
|
}),
|
|
harfbuzz: Object.freeze({
|
|
buildSystem: 'meson',
|
|
sharedOnly: true,
|
|
args: Object.freeze([
|
|
'-Dglib=disabled',
|
|
'-Dgobject=disabled',
|
|
'-Dcairo=disabled',
|
|
'-Dchafa=disabled',
|
|
'-Dicu=disabled',
|
|
'-Dfreetype=enabled',
|
|
'-Dtests=disabled',
|
|
'-Dintrospection=disabled',
|
|
'-Ddocs=disabled',
|
|
'-Dutilities=disabled',
|
|
'-Dbenchmark=disabled',
|
|
]),
|
|
}),
|
|
expat: Object.freeze({
|
|
buildSystem: 'cmake',
|
|
sharedOnly: true,
|
|
args: Object.freeze([
|
|
'-DEXPAT_SHARED_LIBS=ON',
|
|
'-DEXPAT_BUILD_TOOLS=OFF',
|
|
'-DEXPAT_BUILD_EXAMPLES=OFF',
|
|
'-DEXPAT_BUILD_TESTS=OFF',
|
|
'-DEXPAT_BUILD_DOCS=OFF',
|
|
]),
|
|
}),
|
|
fontconfig: Object.freeze({
|
|
buildSystem: 'meson',
|
|
sharedOnly: true,
|
|
args: Object.freeze([
|
|
'-Ddoc=disabled',
|
|
'-Dtests=disabled',
|
|
'-Dtools=disabled',
|
|
'-Dcache-build=disabled',
|
|
'-Dnls=disabled',
|
|
]),
|
|
}),
|
|
libass: Object.freeze({
|
|
buildSystem: 'configure',
|
|
sharedOnly: true,
|
|
args: Object.freeze([
|
|
'--enable-shared',
|
|
'--disable-static',
|
|
'--enable-fontconfig',
|
|
'--disable-coretext',
|
|
'--disable-directwrite',
|
|
'--disable-libunibreak',
|
|
]),
|
|
}),
|
|
openssl: Object.freeze({
|
|
buildSystem: 'openssl',
|
|
sharedOnly: true,
|
|
args: Object.freeze([
|
|
'shared',
|
|
'no-apps',
|
|
'no-docs',
|
|
'no-tests',
|
|
'no-engine',
|
|
'no-legacy',
|
|
'no-module',
|
|
'no-weak-ssl-ciphers',
|
|
]),
|
|
}),
|
|
ffmpeg: Object.freeze({
|
|
buildSystem: 'ffmpeg',
|
|
sharedOnly: true,
|
|
args: FFMPEG_CONFIGURE_FLAGS,
|
|
}),
|
|
libplacebo: Object.freeze({
|
|
buildSystem: 'meson',
|
|
sharedOnly: true,
|
|
args: Object.freeze([
|
|
'-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',
|
|
]),
|
|
}),
|
|
mpv: Object.freeze({
|
|
buildSystem: 'meson',
|
|
sharedOnly: true,
|
|
args: MPV_MESON_FLAGS,
|
|
}),
|
|
});
|
|
|
|
const REQUIRED_TOOLS = Object.freeze([
|
|
'cc',
|
|
'cmake',
|
|
'curl',
|
|
'git',
|
|
'make',
|
|
'meson',
|
|
'ninja',
|
|
'patchelf',
|
|
'perl',
|
|
'pkg-config',
|
|
'python3',
|
|
'readelf',
|
|
'tar',
|
|
]);
|
|
|
|
const DEFAULT_SYSTEM_PKG_CONFIG_DIRS = Object.freeze([
|
|
'/usr/lib/x86_64-linux-gnu/pkgconfig',
|
|
'/usr/lib64/pkgconfig',
|
|
'/usr/lib/pkgconfig',
|
|
'/usr/share/pkgconfig',
|
|
]);
|
|
|
|
const EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES = Object.freeze([
|
|
'alsa',
|
|
'egl',
|
|
'gbm',
|
|
'gl',
|
|
'libpulse',
|
|
'libva',
|
|
'libva-drm',
|
|
]);
|
|
|
|
const GLIBC_TOOLCHAIN_ALLOWLIST = Object.freeze([
|
|
'ld-linux-x86-64.so.2',
|
|
'libc.so.6',
|
|
'libdl.so.2',
|
|
'libgcc_s.so.1',
|
|
'libm.so.6',
|
|
'libpthread.so.0',
|
|
'librt.so.1',
|
|
'libstdc++.so.6',
|
|
]);
|
|
|
|
const EXTERNAL_SYSTEM_LIBRARIES = Object.freeze(
|
|
[
|
|
{
|
|
name: 'libEGL.so.1',
|
|
interface: 'EGL',
|
|
reason: 'System graphics-driver interface used by the frame-copy helper.',
|
|
},
|
|
{
|
|
name: 'libGL.so.1',
|
|
interface: 'OpenGL',
|
|
reason: 'System OpenGL compatibility interface supplied by the graphics stack.',
|
|
},
|
|
{
|
|
name: 'libGLX.so.0',
|
|
interface: 'OpenGL',
|
|
reason: 'GLVND OpenGL dispatch interface supplied by the graphics stack.',
|
|
},
|
|
{
|
|
name: 'libOpenGL.so.0',
|
|
interface: 'OpenGL',
|
|
reason: 'GLVND OpenGL interface supplied by the graphics stack.',
|
|
},
|
|
{
|
|
name: 'libasound.so.2',
|
|
interface: 'ALSA',
|
|
reason: 'Linux system audio interface intentionally used by libmpv.',
|
|
},
|
|
{
|
|
name: 'libdrm.so.2',
|
|
interface: 'DRM',
|
|
reason: 'Kernel graphics interface used by system GBM and VA-API drivers.',
|
|
},
|
|
{
|
|
name: 'libgbm.so.1',
|
|
interface: 'GBM',
|
|
reason: 'System graphics-buffer interface used by headless EGL rendering.',
|
|
},
|
|
{
|
|
name: 'libpulse.so.0',
|
|
interface: 'PulseAudio',
|
|
reason: 'Linux desktop audio interface intentionally used by libmpv.',
|
|
},
|
|
{
|
|
name: 'libva-drm.so.2',
|
|
interface: 'VA-API DRM',
|
|
reason: 'System VA-API DRM interface used for hardware decoding.',
|
|
},
|
|
{
|
|
name: 'libva.so.2',
|
|
interface: 'VA-API',
|
|
reason: 'System video-acceleration interface used for hardware decoding.',
|
|
},
|
|
].map((externalLibrary) => Object.freeze(externalLibrary))
|
|
);
|
|
|
|
const SHARED_LIBRARY_PATTERN = /\.so(?:\.\d+)*$/;
|
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
const externalSystemLibraryNames = new Set(
|
|
EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name)
|
|
);
|
|
const allowedExternalLibraryNames = new Set([
|
|
...GLIBC_TOOLCHAIN_ALLOWLIST,
|
|
...externalSystemLibraryNames,
|
|
]);
|
|
|
|
function assertArchiveMatchesPin(sourcePackage, actualSha256) {
|
|
if (actualSha256 !== sourcePackage.expectedSha256) {
|
|
throw new Error(
|
|
`${sourcePackage.id} archive SHA-256 mismatch: expected ${sourcePackage.expectedSha256}, received ${actualSha256}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertGitCommitMatchesPin(sourcePackage, actualGitCommit) {
|
|
if (actualGitCommit !== sourcePackage.expectedGitCommit) {
|
|
throw new Error(
|
|
`${sourcePackage.id} git commit mismatch: expected ${sourcePackage.expectedGitCommit}, received ${actualGitCommit}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function joinEnvironmentParts(parts, separator = ' ') {
|
|
return parts.filter((value) => value && value.trim()).join(separator);
|
|
}
|
|
|
|
function resolveSystemPkgConfigDirs(environment = {}) {
|
|
const explicitDirectories =
|
|
environment.IPTVNATOR_EMBEDDED_MPV_SYSTEM_PKG_CONFIG_DIRS;
|
|
if (!explicitDirectories) {
|
|
return [...DEFAULT_SYSTEM_PKG_CONFIG_DIRS];
|
|
}
|
|
|
|
const directories = explicitDirectories
|
|
.split(path.delimiter)
|
|
.map((directory) => directory.trim())
|
|
.filter(Boolean);
|
|
if (
|
|
directories.length === 0 ||
|
|
directories.some((directory) => !path.isAbsolute(directory))
|
|
) {
|
|
throw new Error(
|
|
'IPTVNATOR_EMBEDDED_MPV_SYSTEM_PKG_CONFIG_DIRS must contain only absolute paths.'
|
|
);
|
|
}
|
|
return [
|
|
...new Set(directories.map((directory) => path.normalize(directory))),
|
|
];
|
|
}
|
|
|
|
function createBuildEnvironment({
|
|
prefix,
|
|
baseEnv = process.env,
|
|
systemPkgConfigDirs = [],
|
|
}) {
|
|
const prefixPkgConfigDirs = [
|
|
path.join(prefix, 'lib', 'pkgconfig'),
|
|
path.join(prefix, 'share', 'pkgconfig'),
|
|
];
|
|
const pkgConfigLibDirs = [
|
|
...new Set([
|
|
...prefixPkgConfigDirs,
|
|
...systemPkgConfigDirs.filter(Boolean),
|
|
]),
|
|
];
|
|
const prefixLibDir = path.join(prefix, 'lib');
|
|
const ignoredVariables = new Set([
|
|
'CFLAGS',
|
|
'CPPFLAGS',
|
|
'CXXFLAGS',
|
|
'LDFLAGS',
|
|
'LD_LIBRARY_PATH',
|
|
'LIBRARY_PATH',
|
|
'CPATH',
|
|
'C_INCLUDE_PATH',
|
|
'CPLUS_INCLUDE_PATH',
|
|
'CMAKE_PREFIX_PATH',
|
|
'CMAKE_LIBRARY_PATH',
|
|
'CMAKE_INCLUDE_PATH',
|
|
'FONTCONFIG_PATH',
|
|
'OPENSSL_MODULES',
|
|
]);
|
|
const inheritedEnvironment = Object.fromEntries(
|
|
Object.entries(baseEnv).filter(
|
|
([name]) =>
|
|
!ignoredVariables.has(name) && !name.startsWith('PKG_CONFIG')
|
|
)
|
|
);
|
|
|
|
return {
|
|
...inheritedEnvironment,
|
|
PATH: joinEnvironmentParts(
|
|
[path.join(prefix, 'bin'), baseEnv.PATH],
|
|
path.delimiter
|
|
),
|
|
PKG_CONFIG_PATH: prefixPkgConfigDirs.join(path.delimiter),
|
|
PKG_CONFIG_LIBDIR: pkgConfigLibDirs.join(path.delimiter),
|
|
CMAKE_PREFIX_PATH: prefix,
|
|
CPPFLAGS: `-I${path.join(prefix, 'include')}`,
|
|
CFLAGS: joinEnvironmentParts([
|
|
'-fPIC',
|
|
`-I${path.join(prefix, 'include')}`,
|
|
]),
|
|
CXXFLAGS: joinEnvironmentParts([
|
|
'-fPIC',
|
|
`-I${path.join(prefix, 'include')}`,
|
|
]),
|
|
LDFLAGS: joinEnvironmentParts([
|
|
`-L${prefixLibDir}`,
|
|
`-Wl,-rpath-link,${prefixLibDir}`,
|
|
]),
|
|
LD_LIBRARY_PATH: prefixLibDir,
|
|
FONTCONFIG_PATH: path.join(prefix, 'etc', 'fonts'),
|
|
OPENSSL_MODULES: path.join(prefixLibDir, 'ossl-modules'),
|
|
};
|
|
}
|
|
|
|
function sha256Buffer(contents) {
|
|
return crypto.createHash('sha256').update(contents).digest('hex');
|
|
}
|
|
|
|
function runtimeLibraryNames(libDir) {
|
|
return fs
|
|
.readdirSync(libDir, { withFileTypes: true })
|
|
.filter(
|
|
(entry) =>
|
|
(entry.isFile() || entry.isSymbolicLink()) &&
|
|
SHARED_LIBRARY_PATTERN.test(entry.name)
|
|
)
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
}
|
|
|
|
function assertPathInside(parentPath, candidatePath, label) {
|
|
const relativePath = path.relative(parentPath, candidatePath);
|
|
if (
|
|
relativePath === '..' ||
|
|
relativePath.startsWith(`..${path.sep}`) ||
|
|
path.isAbsolute(relativePath)
|
|
) {
|
|
throw new Error(`${label} resolves outside ${parentPath}.`);
|
|
}
|
|
}
|
|
|
|
function materializeLibrarySymlinks(libDir) {
|
|
const realLibDir = fs.realpathSync(libDir);
|
|
for (const name of runtimeLibraryNames(libDir)) {
|
|
const libraryPath = path.join(libDir, name);
|
|
const stat = fs.lstatSync(libraryPath);
|
|
if (!stat.isSymbolicLink()) {
|
|
continue;
|
|
}
|
|
|
|
const realLibraryPath = fs.realpathSync(libraryPath);
|
|
assertPathInside(
|
|
realLibDir,
|
|
realLibraryPath,
|
|
`Runtime library alias ${name}`
|
|
);
|
|
const targetStat = fs.statSync(realLibraryPath);
|
|
if (!targetStat.isFile()) {
|
|
throw new Error(
|
|
`Runtime library alias ${name} does not resolve to a regular file.`
|
|
);
|
|
}
|
|
const contents = fs.readFileSync(realLibraryPath);
|
|
fs.unlinkSync(libraryPath);
|
|
fs.writeFileSync(libraryPath, contents, {
|
|
mode: targetStat.mode & 0o777,
|
|
});
|
|
}
|
|
}
|
|
|
|
function createRuntimeFileRecords(libDir) {
|
|
return runtimeLibraryNames(libDir).map((name) => {
|
|
const libraryPath = path.join(libDir, name);
|
|
const stat = fs.lstatSync(libraryPath);
|
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
throw new Error(
|
|
`Runtime library ${name} must be a materialized regular file.`
|
|
);
|
|
}
|
|
const contents = fs.readFileSync(libraryPath);
|
|
return {
|
|
name,
|
|
size: contents.length,
|
|
sha256: sha256Buffer(contents),
|
|
};
|
|
});
|
|
}
|
|
|
|
function parseReadelfDynamic(output) {
|
|
const dynamic = {
|
|
needed: [],
|
|
rpath: [],
|
|
runpath: [],
|
|
};
|
|
const dynamicEntryPattern = /\((NEEDED|RPATH|RUNPATH)\)[^[]*\[([^\]]*)\]/g;
|
|
for (const match of output.matchAll(dynamicEntryPattern)) {
|
|
const [, tag, value] = match;
|
|
if (tag === 'NEEDED') {
|
|
dynamic.needed.push(value);
|
|
continue;
|
|
}
|
|
const field = tag.toLowerCase();
|
|
dynamic[field].push(
|
|
...value.split(':').filter((pathEntry) => pathEntry.length > 0)
|
|
);
|
|
}
|
|
|
|
for (const field of Object.keys(dynamic)) {
|
|
dynamic[field] = [...new Set(dynamic[field])].sort();
|
|
}
|
|
return dynamic;
|
|
}
|
|
|
|
function validateRuntimeDependencyClosure({
|
|
entries,
|
|
runtimeFileNames,
|
|
buildPrefix,
|
|
}) {
|
|
if (!Array.isArray(entries) || !Array.isArray(runtimeFileNames)) {
|
|
throw new TypeError(
|
|
'Runtime closure entries and runtime file names must be arrays.'
|
|
);
|
|
}
|
|
|
|
const bundledNames = new Set(runtimeFileNames);
|
|
const entryNames = new Set();
|
|
const externalDependencies = new Set();
|
|
const normalizedEntries = [...entries]
|
|
.map((entry) => ({
|
|
name: entry.name,
|
|
needed: [...new Set(entry.needed ?? [])].sort(),
|
|
rpath: [...new Set(entry.rpath ?? [])].sort(),
|
|
runpath: [...new Set(entry.runpath ?? [])].sort(),
|
|
}))
|
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
|
|
for (const entry of normalizedEntries) {
|
|
if (!bundledNames.has(entry.name)) {
|
|
throw new Error(
|
|
`Dynamic closure contains undeclared runtime file ${entry.name}.`
|
|
);
|
|
}
|
|
if (entryNames.has(entry.name)) {
|
|
throw new Error(
|
|
`Dynamic closure contains duplicate runtime file ${entry.name}.`
|
|
);
|
|
}
|
|
entryNames.add(entry.name);
|
|
|
|
if (entry.rpath.length > 0) {
|
|
throw new Error(
|
|
`${entry.name} has forbidden RPATH ${entry.rpath.join(':')}.`
|
|
);
|
|
}
|
|
if (entry.runpath.length !== 1 || entry.runpath[0] !== '$ORIGIN') {
|
|
const renderedRunpath =
|
|
entry.runpath.length > 0 ? entry.runpath.join(':') : '<empty>';
|
|
throw new Error(
|
|
`${entry.name} RUNPATH must be exactly $ORIGIN; got ${renderedRunpath}.`
|
|
);
|
|
}
|
|
if (
|
|
buildPrefix &&
|
|
[...entry.rpath, ...entry.runpath].some((value) =>
|
|
value.includes(buildPrefix)
|
|
)
|
|
) {
|
|
throw new Error(
|
|
`${entry.name} RPATH/RUNPATH contains build prefix ${buildPrefix}.`
|
|
);
|
|
}
|
|
|
|
for (const dependencyName of entry.needed) {
|
|
if (bundledNames.has(dependencyName)) {
|
|
continue;
|
|
}
|
|
if (!allowedExternalLibraryNames.has(dependencyName)) {
|
|
throw new Error(
|
|
`Runtime dependency is not bundled or allowlisted: ${entry.name} -> ${dependencyName}.`
|
|
);
|
|
}
|
|
externalDependencies.add(dependencyName);
|
|
}
|
|
}
|
|
|
|
for (const runtimeFileName of bundledNames) {
|
|
if (!entryNames.has(runtimeFileName)) {
|
|
throw new Error(
|
|
`Runtime library ${runtimeFileName} is missing from the dynamic closure.`
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
entries: normalizedEntries,
|
|
externalDependencies: [...externalDependencies].sort(),
|
|
};
|
|
}
|
|
|
|
function parseCliInvocation({ platform, arch, argv, cwd }) {
|
|
if (platform !== 'linux' || arch !== 'x64') {
|
|
throw new Error(
|
|
`Embedded MPV runtime source builds are supported on Linux x64 only; received ${platform}/${arch}.`
|
|
);
|
|
}
|
|
|
|
const args = argv[0] === '--' ? argv.slice(1) : argv;
|
|
if (args.length !== 1 || !args[0]) {
|
|
throw new Error(
|
|
[
|
|
'Usage: node tools/embedded-mpv/build-linux-runtime.mjs <output-prefix>',
|
|
'',
|
|
'Builds the pinned LGPL-compatible Linux x64 libmpv runtime from source.',
|
|
].join('\n')
|
|
);
|
|
}
|
|
|
|
return { prefix: path.resolve(cwd, args[0]) };
|
|
}
|
|
|
|
function sourceManifestMetadata(sourceRecord) {
|
|
const metadata = {
|
|
version: sourceRecord.version,
|
|
sourceUrl: sourceRecord.sourceUrl,
|
|
...(sourceRecord.sourceTag
|
|
? { sourceTag: sourceRecord.sourceTag }
|
|
: {}),
|
|
...(sourceRecord.sourceSha256
|
|
? { sourceSha256: sourceRecord.sourceSha256 }
|
|
: {}),
|
|
...(sourceRecord.sourceGitCommit
|
|
? { sourceGitCommit: sourceRecord.sourceGitCommit }
|
|
: {}),
|
|
...(sourceRecord.sourceSubmodules
|
|
? { sourceSubmodules: [...sourceRecord.sourceSubmodules] }
|
|
: {}),
|
|
license: sourceRecord.license,
|
|
};
|
|
|
|
if (
|
|
sourceRecord.sourceKind === 'archive' &&
|
|
!SHA256_PATTERN.test(sourceRecord.sourceSha256 ?? '')
|
|
) {
|
|
throw new Error(
|
|
`Archive source ${sourceRecord.id} is missing its downloaded SHA-256 digest.`
|
|
);
|
|
}
|
|
if (
|
|
sourceRecord.sourceKind === 'git' &&
|
|
!/^[a-f0-9]{40,64}$/.test(sourceRecord.sourceGitCommit ?? '')
|
|
) {
|
|
throw new Error(
|
|
`Git source ${sourceRecord.id} is missing its exact commit digest.`
|
|
);
|
|
}
|
|
return metadata;
|
|
}
|
|
|
|
function createLinuxRuntimeManifest({
|
|
sourceRecords,
|
|
runtimeFiles,
|
|
dependencyClosure,
|
|
buildHost,
|
|
generatedAt = new Date().toISOString(),
|
|
ffmpegConfigureFlags = FFMPEG_CONFIGURE_FLAGS,
|
|
mpvMesonFlags = MPV_MESON_FLAGS,
|
|
}) {
|
|
const packages = {};
|
|
for (const sourcePackage of SOURCE_PACKAGES) {
|
|
const sourceRecord = sourceRecords[sourcePackage.id];
|
|
if (!sourceRecord) {
|
|
throw new Error(`Missing source metadata for ${sourcePackage.id}.`);
|
|
}
|
|
if (sourcePackage.sourceKind === 'archive') {
|
|
assertArchiveMatchesPin(sourcePackage, sourceRecord.sourceSha256);
|
|
} else {
|
|
assertGitCommitMatchesPin(
|
|
sourcePackage,
|
|
sourceRecord.sourceGitCommit
|
|
);
|
|
}
|
|
packages[sourcePackage.id] = sourceManifestMetadata(sourceRecord);
|
|
}
|
|
|
|
const runtimeTotalBytes = runtimeFiles.reduce(
|
|
(total, runtimeFile) => total + runtimeFile.size,
|
|
0
|
|
);
|
|
return {
|
|
schemaVersion: 1,
|
|
origin: 'vendored-lgpl-source-build',
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
generatedAt,
|
|
packages,
|
|
ffmpeg: {
|
|
...packages.ffmpeg,
|
|
licensePolicy:
|
|
'LGPL, built with OpenSSL and without GPL, nonfree, or version-3-only components.',
|
|
configureFlags: [...ffmpegConfigureFlags],
|
|
},
|
|
mpv: {
|
|
...packages.mpv,
|
|
licensePolicy:
|
|
'LGPL-compatible libmpv built with -Dgpl=false and without the CLI player.',
|
|
mesonFlags: [...mpvMesonFlags],
|
|
},
|
|
runtimeFiles: runtimeFiles.map((runtimeFile) => ({ ...runtimeFile })),
|
|
runtimeTotalBytes,
|
|
runtimeDependencyClosure: {
|
|
entries: dependencyClosure.entries.map((entry) => ({
|
|
name: entry.name,
|
|
needed: [...entry.needed],
|
|
rpath: [...entry.rpath],
|
|
runpath: [...entry.runpath],
|
|
})),
|
|
externalDependencies: [...dependencyClosure.externalDependencies],
|
|
},
|
|
externalSystemLibraries: EXTERNAL_SYSTEM_LIBRARIES.map(
|
|
(externalLibrary) => ({ ...externalLibrary })
|
|
),
|
|
buildHost,
|
|
sourceDistribution:
|
|
'Attach a source archive to the corresponding Linux binary release containing the exact downloaded source archives, a checkout or git bundle of the recorded libplacebo commit and submodules, tools/embedded-mpv/build-linux-runtime.mjs, tools/embedded-mpv/build-linux-runtime.cjs, this runtime manifest, and any local patches.',
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
BUILD_RECIPES,
|
|
BUILD_ORDER,
|
|
DEFAULT_SYSTEM_PKG_CONFIG_DIRS,
|
|
EXTERNAL_SYSTEM_LIBRARIES,
|
|
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
|
|
FFMPEG_CONFIGURE_FLAGS,
|
|
GLIBC_TOOLCHAIN_ALLOWLIST,
|
|
MPV_MESON_FLAGS,
|
|
REQUIRED_TOOLS,
|
|
SOURCE_PACKAGES,
|
|
assertArchiveMatchesPin,
|
|
assertGitCommitMatchesPin,
|
|
createBuildEnvironment,
|
|
createLinuxRuntimeManifest,
|
|
createRuntimeFileRecords,
|
|
materializeLibrarySymlinks,
|
|
parseCliInvocation,
|
|
parseReadelfDynamic,
|
|
resolveSystemPkgConfigDirs,
|
|
runtimeLibraryNames,
|
|
sha256Buffer,
|
|
validateRuntimeDependencyClosure,
|
|
};
|