mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 09:26:14 -08:00
The "Build pinned Linux Embedded MPV runtime" job failed twice on 2026-08-11 because www.freedesktop.org answered GitHub runners with HTTP 418 for the fontconfig tarball. The Linux builder curled a single pinned URL with no fallback, so upstream rate-limiting reddened the build. Route downloadArchive() through the shared downloadPinnedSource() helper the macOS builder already uses, and pin a mirror for each single-host source: fontconfig and libdisplay-info (freedesktop-hosted) plus freetype, which the macOS builder already mirrors. Each mirror was downloaded and verified to hash to the existing pin. The curl hardening flags and assertArchiveMatchesPin are unchanged, and the helper verifies every candidate against the same SHA-256, so a mirror serving different bytes is rejected rather than used. Unlike macOS, the Linux manifest keeps sourceUrl at the canonical pinned value even when a mirror served the bytes: notice generation and the Snap publication boundary compare that field against the immutable pin. A used mirror is logged instead. build-linux-runtime.mjs now imports the downloader, so download-pinned-source.mjs joins the released source-archive tooling set (otherwise the archive would ship a build script it cannot run) and the Linux runtime cache key. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1703 lines
53 KiB
JavaScript
1703 lines
53 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('node:crypto');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { isDeepStrictEqual } = require('node:util');
|
|
|
|
const HWDATA_BUILD_INPUT = Object.freeze({
|
|
consumer: 'libdisplay-info',
|
|
relativePath: 'pnp.ids',
|
|
purpose: 'PNP vendor lookup table compiled into libdisplay-info.',
|
|
});
|
|
const EXPECTED_LIBPLACEBO_V7_360_1_SOURCE_SUBMODULES = Object.freeze([
|
|
'450bd2232225d6c7728a4108055ac2e37cef6475 3rdparty/Vulkan-Headers',
|
|
'97b54ca9e75f5303507699d27c6b4f4efe4641a1 3rdparty/fast_float',
|
|
'73db193f853e2ee079bf3ca8a64aa2eaf6459043 3rdparty/glad',
|
|
'15206881c006c79667fe5154fe80c01c65410679 3rdparty/jinja',
|
|
'297fc8e356e6836a62087949245d09a28e9f1b13 3rdparty/markupsafe',
|
|
'242f35efa067a46c595645eeda7b1771ea1f83b1 demos/3rdparty/nuklear',
|
|
]);
|
|
|
|
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',
|
|
mirrors: Object.freeze([
|
|
'https://downloads.sourceforge.net/project/freetype/freetype2/2.13.3/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',
|
|
mirrors: Object.freeze([
|
|
'https://ftp.osuosl.org/pub/blfs/conglomeration/fontconfig/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',
|
|
expectedSubmodules: EXPECTED_LIBPLACEBO_V7_360_1_SOURCE_SUBMODULES,
|
|
license: 'LGPL-2.1-or-later',
|
|
},
|
|
{
|
|
id: 'hwdata',
|
|
version: '0.409',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://github.com/vcrhonek/hwdata/archive/refs/tags/v0.409.tar.gz',
|
|
expectedSha256:
|
|
'23006accc0f931dd5187d0307a57d0744e2b8feb85e73c37bc0f5229fb31eadd',
|
|
license: 'GPL-2.0-or-later OR XFree86-1.0',
|
|
buildInput: HWDATA_BUILD_INPUT,
|
|
},
|
|
{
|
|
id: 'libdisplay-info',
|
|
version: '0.1.1',
|
|
sourceKind: 'archive',
|
|
sourceUrl:
|
|
'https://gitlab.freedesktop.org/emersion/libdisplay-info/-/releases/0.1.1/downloads/libdisplay-info-0.1.1.tar.xz',
|
|
mirrors: Object.freeze([
|
|
'https://ftp.osuosl.org/pub/blfs/conglomeration/libdisplay-info/libdisplay-info-0.1.1.tar.xz',
|
|
]),
|
|
expectedSha256:
|
|
'0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60',
|
|
license: 'MIT',
|
|
},
|
|
{
|
|
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=enabled',
|
|
'-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',
|
|
'-Dxml-backend=expat',
|
|
'-Dbaseconfig-dir=/etc/fonts',
|
|
'-Dconfig-dir=/etc/fonts/conf.d',
|
|
'-Dtemplate-dir=/usr/share/fontconfig/conf.avail',
|
|
'-Dcache-dir=/var/cache/fontconfig',
|
|
'-Dxml-dir=/usr/share/xml/fontconfig',
|
|
]),
|
|
}),
|
|
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',
|
|
'--openssldir=/etc/ssl',
|
|
]),
|
|
}),
|
|
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',
|
|
]),
|
|
}),
|
|
hwdata: Object.freeze({
|
|
buildSystem: 'data',
|
|
sharedOnly: false,
|
|
args: Object.freeze([]),
|
|
}),
|
|
'libdisplay-info': Object.freeze({
|
|
buildSystem: 'meson',
|
|
sharedOnly: true,
|
|
args: Object.freeze([]),
|
|
}),
|
|
mpv: Object.freeze({
|
|
buildSystem: 'meson',
|
|
sharedOnly: true,
|
|
args: MPV_MESON_FLAGS,
|
|
}),
|
|
});
|
|
|
|
const REQUIRED_TOOLS = Object.freeze([
|
|
'cc',
|
|
'cmake',
|
|
'curl',
|
|
'git',
|
|
'gperf',
|
|
'make',
|
|
'meson',
|
|
'nasm',
|
|
'ninja',
|
|
'patchelf',
|
|
'perl',
|
|
'pkg-config',
|
|
'python3',
|
|
'readelf',
|
|
'tar',
|
|
]);
|
|
|
|
const MINIMUM_TOOL_VERSIONS = Object.freeze({
|
|
cc: '9.0.0',
|
|
cmake: '3.16.0',
|
|
curl: '7.71.0',
|
|
git: '2.30.0',
|
|
gperf: '3.1.0',
|
|
make: '4.0.0',
|
|
meson: '1.6.0',
|
|
nasm: '2.15.05',
|
|
ninja: '1.10.0',
|
|
patchelf: '0.14.0',
|
|
perl: '5.30.0',
|
|
'pkg-config': '0.29.0',
|
|
python3: '3.8.0',
|
|
readelf: '2.35.0',
|
|
tar: '1.30.0',
|
|
});
|
|
|
|
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',
|
|
'libdrm',
|
|
'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 RUNTIME_EXTERNAL_CONFIGURATION = Object.freeze({
|
|
fontconfig: Object.freeze({
|
|
configDirectory: '/etc/fonts',
|
|
templateDirectory: '/usr/share/fontconfig',
|
|
cacheDirectory: '/var/cache/fontconfig',
|
|
ownership: 'system',
|
|
}),
|
|
openssl: Object.freeze({
|
|
configFile: '/etc/ssl/openssl.cnf',
|
|
certificateFile: '/etc/ssl/cert.pem',
|
|
certificateDirectory: '/etc/ssl/certs',
|
|
ownership: 'system',
|
|
}),
|
|
});
|
|
|
|
const OUTPUT_OWNERSHIP_MARKER = '.iptvnator-linux-runtime-owner';
|
|
const OUTPUT_OWNERSHIP_MARKER_CONTENT =
|
|
'iptvnator-embedded-mpv-linux-runtime-v1\n';
|
|
|
|
const PORTABLE_ABI_BASELINE = Object.freeze({
|
|
distribution: 'Ubuntu 22.04',
|
|
glibcMaximum: '2.35',
|
|
glibcxxMaximum: '3.4.30',
|
|
});
|
|
|
|
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 assertGitSubmodulesMatchPin(sourcePackage, actualSubmodules) {
|
|
if (
|
|
!isDeepStrictEqual(actualSubmodules, sourcePackage.expectedSubmodules)
|
|
) {
|
|
throw new Error(
|
|
`${sourcePackage.id} git submodules do not match the exact pinned recursive records.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function canonicalizeGitSubmoduleStatus(output) {
|
|
if (typeof output !== 'string') {
|
|
throw new Error('Git submodule status output must be a string.');
|
|
}
|
|
if (output.trim() === '') {
|
|
return [];
|
|
}
|
|
|
|
const records = [];
|
|
const seenRecords = new Set();
|
|
const seenPaths = new Set();
|
|
for (const rawLine of output.split(/\r?\n/)) {
|
|
const line = rawLine.trim();
|
|
if (line === '') {
|
|
continue;
|
|
}
|
|
if (/^[-+U]/.test(line)) {
|
|
throw new Error(
|
|
`Git submodule checkout is not clean: ${rawLine.trimEnd()}`
|
|
);
|
|
}
|
|
const match = line.match(
|
|
/^([a-f0-9]{40,64})\s+([A-Za-z0-9_+./-]+)(?:\s+\([^\r\n]*\))?$/
|
|
);
|
|
if (!match) {
|
|
throw new Error(
|
|
`Git submodule status contains an unsafe or malformed record: ${rawLine.trimEnd()}`
|
|
);
|
|
}
|
|
const submodulePath = match[2];
|
|
if (
|
|
path.posix.isAbsolute(submodulePath) ||
|
|
submodulePath
|
|
.split('/')
|
|
.some(
|
|
(segment) =>
|
|
segment === '' || segment === '.' || segment === '..'
|
|
)
|
|
) {
|
|
throw new Error(
|
|
`Git submodule status contains an unsafe path: ${submodulePath}`
|
|
);
|
|
}
|
|
const record = `${match[1]} ${submodulePath}`;
|
|
if (seenRecords.has(record) || seenPaths.has(submodulePath)) {
|
|
throw new Error(
|
|
`Git submodule status contains a duplicate record: ${record}`
|
|
);
|
|
}
|
|
seenRecords.add(record);
|
|
seenPaths.add(submodulePath);
|
|
records.push(record);
|
|
}
|
|
return records;
|
|
}
|
|
|
|
function parseVersion(value) {
|
|
if (typeof value !== 'string') {
|
|
return null;
|
|
}
|
|
const match = value.match(/\b(\d+\.\d+(?:\.\d+)*)\b/);
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
function compareVersions(left, right) {
|
|
const leftParts = left.split('.').map(Number);
|
|
const rightParts = right.split('.').map(Number);
|
|
const length = Math.max(leftParts.length, rightParts.length);
|
|
for (let index = 0; index < length; index += 1) {
|
|
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
if (difference !== 0) {
|
|
return Math.sign(difference);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function assertMinimumToolVersions(toolVersions) {
|
|
for (const tool of REQUIRED_TOOLS) {
|
|
const declaredVersion = toolVersions?.[tool];
|
|
if (typeof declaredVersion !== 'string' || !declaredVersion.trim()) {
|
|
throw new Error(`Missing required tool version for ${tool}.`);
|
|
}
|
|
const actualVersion = parseVersion(declaredVersion);
|
|
if (!actualVersion) {
|
|
throw new Error(
|
|
`Unable to parse required tool version for ${tool}: ${declaredVersion}.`
|
|
);
|
|
}
|
|
const minimumVersion = MINIMUM_TOOL_VERSIONS[tool];
|
|
if (compareVersions(actualVersion, minimumVersion) < 0) {
|
|
throw new Error(
|
|
`${tool} ${actualVersion} is unsupported; ${tool} requires ${minimumVersion} or newer for Linux runtime builds.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertUniqueMesonOptionAssignments(buildRecipes) {
|
|
if (!buildRecipes || typeof buildRecipes !== 'object') {
|
|
throw new TypeError('Linux runtime build recipes must be an object.');
|
|
}
|
|
for (const [packageId, recipe] of Object.entries(buildRecipes)) {
|
|
if (recipe?.buildSystem !== 'meson') {
|
|
continue;
|
|
}
|
|
if (!Array.isArray(recipe.args)) {
|
|
throw new Error(
|
|
`${packageId} Meson recipe must declare an argument array.`
|
|
);
|
|
}
|
|
|
|
const assignmentsByOption = new Map();
|
|
for (const flag of recipe.args) {
|
|
const match =
|
|
typeof flag === 'string' ? flag.match(/^(-D[^=]+)=/) : null;
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
const option = match[1];
|
|
const assignmentCount = (assignmentsByOption.get(option) ?? 0) + 1;
|
|
assignmentsByOption.set(option, assignmentCount);
|
|
if (assignmentCount > 1) {
|
|
throw new Error(
|
|
`${packageId} Meson recipe must assign ${option} exactly once.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function lstatIfExists(fileSystem, filePath) {
|
|
try {
|
|
return fileSystem.lstatSync(filePath);
|
|
} catch (error) {
|
|
if (error?.code === 'ENOENT') {
|
|
return null;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function assertOwnedOutputDestination(outputPrefix, fileSystem = fs) {
|
|
const outputStat = lstatIfExists(fileSystem, outputPrefix);
|
|
if (!outputStat) {
|
|
return;
|
|
}
|
|
if (!outputStat.isDirectory() || outputStat.isSymbolicLink()) {
|
|
throw new Error(
|
|
`Existing output ${outputPrefix} must be a non-symbolic-link directory carrying the IPTVnator ownership marker.`
|
|
);
|
|
}
|
|
|
|
const markerPath = path.join(outputPrefix, OUTPUT_OWNERSHIP_MARKER);
|
|
const markerStat = lstatIfExists(fileSystem, markerPath);
|
|
if (
|
|
!markerStat ||
|
|
!markerStat.isFile() ||
|
|
markerStat.isSymbolicLink() ||
|
|
fileSystem.readFileSync(markerPath, 'utf8') !==
|
|
OUTPUT_OWNERSHIP_MARKER_CONTENT
|
|
) {
|
|
throw new Error(
|
|
`Existing output ${outputPrefix} is missing the valid IPTVnator ownership marker.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function ownedStagingPrefixPath(outputPrefix, token) {
|
|
return path.join(
|
|
path.dirname(outputPrefix),
|
|
`.${path.basename(outputPrefix)}.iptvnator-stage-${token}`
|
|
);
|
|
}
|
|
|
|
function createOwnedStagingPrefix(
|
|
outputPrefix,
|
|
{ fileSystem = fs, token = crypto.randomBytes(8).toString('hex') } = {}
|
|
) {
|
|
assertOwnedOutputDestination(outputPrefix, fileSystem);
|
|
const outputParent = path.dirname(outputPrefix);
|
|
const stagingPrefix = ownedStagingPrefixPath(outputPrefix, token);
|
|
if (lstatIfExists(fileSystem, stagingPrefix)) {
|
|
throw new Error(
|
|
`Refusing to reuse existing Linux runtime staging path ${stagingPrefix}.`
|
|
);
|
|
}
|
|
|
|
fileSystem.mkdirSync(outputParent, { recursive: true });
|
|
fileSystem.mkdirSync(stagingPrefix);
|
|
try {
|
|
fileSystem.writeFileSync(
|
|
path.join(stagingPrefix, OUTPUT_OWNERSHIP_MARKER),
|
|
OUTPUT_OWNERSHIP_MARKER_CONTENT,
|
|
{ mode: 0o644 }
|
|
);
|
|
} catch (error) {
|
|
fileSystem.rmSync(stagingPrefix, { recursive: true, force: true });
|
|
throw error;
|
|
}
|
|
return stagingPrefix;
|
|
}
|
|
|
|
function publishOwnedOutput({
|
|
outputPrefix,
|
|
stagingPrefix,
|
|
fileSystem = fs,
|
|
token = crypto.randomBytes(8).toString('hex'),
|
|
}) {
|
|
assertOwnedOutputDestination(outputPrefix, fileSystem);
|
|
assertOwnedOutputDestination(stagingPrefix, fileSystem);
|
|
if (!lstatIfExists(fileSystem, stagingPrefix)) {
|
|
throw new Error(
|
|
`Linux runtime staging prefix does not exist: ${stagingPrefix}.`
|
|
);
|
|
}
|
|
|
|
const backupPrefix = path.join(
|
|
path.dirname(outputPrefix),
|
|
`.${path.basename(outputPrefix)}.iptvnator-backup-${token}`
|
|
);
|
|
if (lstatIfExists(fileSystem, backupPrefix)) {
|
|
throw new Error(
|
|
`Refusing to reuse existing Linux runtime backup path ${backupPrefix}.`
|
|
);
|
|
}
|
|
|
|
let movedPreviousOutput = false;
|
|
let published = false;
|
|
try {
|
|
if (lstatIfExists(fileSystem, outputPrefix)) {
|
|
fileSystem.renameSync(outputPrefix, backupPrefix);
|
|
movedPreviousOutput = true;
|
|
}
|
|
fileSystem.renameSync(stagingPrefix, outputPrefix);
|
|
published = true;
|
|
if (movedPreviousOutput) {
|
|
fileSystem.rmSync(backupPrefix, {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (
|
|
movedPreviousOutput &&
|
|
!lstatIfExists(fileSystem, outputPrefix) &&
|
|
lstatIfExists(fileSystem, backupPrefix)
|
|
) {
|
|
fileSystem.renameSync(backupPrefix, outputPrefix);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
if (lstatIfExists(fileSystem, stagingPrefix)) {
|
|
fileSystem.rmSync(stagingPrefix, {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|
|
if (published && lstatIfExists(fileSystem, backupPrefix)) {
|
|
fileSystem.rmSync(backupPrefix, {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
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 createPinnedHwdataPkgConfigEnvironment({ buildEnvironment, prefix }) {
|
|
if (!buildEnvironment || typeof buildEnvironment !== 'object') {
|
|
throw new TypeError(
|
|
'Pinned hwdata requires the Linux runtime build environment.'
|
|
);
|
|
}
|
|
const prefixPkgConfigDirs = [
|
|
path.join(prefix, 'lib', 'pkgconfig'),
|
|
path.join(prefix, 'share', 'pkgconfig'),
|
|
];
|
|
const pinnedPkgConfigPath = prefixPkgConfigDirs.join(path.delimiter);
|
|
return {
|
|
...buildEnvironment,
|
|
PKG_CONFIG_PATH: pinnedPkgConfigPath,
|
|
PKG_CONFIG_LIBDIR: pinnedPkgConfigPath,
|
|
};
|
|
}
|
|
|
|
function assertPinnedHwdataResolution({
|
|
pcFileDir,
|
|
pkgDataDir,
|
|
prefix,
|
|
version,
|
|
}) {
|
|
const expectedPcFileDir = path.join(prefix, 'share', 'pkgconfig');
|
|
const expectedPkgDataDir = path.join(prefix, 'share', 'hwdata');
|
|
if (path.resolve(pcFileDir) !== path.resolve(expectedPcFileDir)) {
|
|
throw new Error(
|
|
`Pinned hwdata pkg-config metadata resolved outside the staged prefix: ${pcFileDir}.`
|
|
);
|
|
}
|
|
if (path.resolve(pkgDataDir) !== path.resolve(expectedPkgDataDir)) {
|
|
throw new Error(
|
|
`Pinned hwdata data resolved outside the staged prefix: ${pkgDataDir}.`
|
|
);
|
|
}
|
|
const hwdataPackage = SOURCE_PACKAGES.find(({ id }) => id === 'hwdata');
|
|
if (version !== hwdataPackage.version) {
|
|
throw new Error(
|
|
`Pinned hwdata version mismatch: expected ${hwdataPackage.version}, received ${version}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
function preparePinnedHwdataBuildInput({
|
|
buildEnvironment,
|
|
fileSystem = fs,
|
|
prefix,
|
|
runCapture,
|
|
sourcePath,
|
|
}) {
|
|
if (typeof runCapture !== 'function') {
|
|
throw new TypeError(
|
|
'Pinned hwdata preparation requires a command capture function.'
|
|
);
|
|
}
|
|
const hwdataPackage = SOURCE_PACKAGES.find(({ id }) => id === 'hwdata');
|
|
const sourceRoot = fileSystem.realpathSync(sourcePath);
|
|
const sourceInputPath = path.join(
|
|
sourcePath,
|
|
hwdataPackage.buildInput.relativePath
|
|
);
|
|
const sourceInputStat = fileSystem.lstatSync(sourceInputPath);
|
|
if (!sourceInputStat.isFile() || sourceInputStat.isSymbolicLink()) {
|
|
throw new Error(
|
|
`Pinned hwdata build input must be a regular file: ${sourceInputPath}.`
|
|
);
|
|
}
|
|
const realSourceInputPath = fileSystem.realpathSync(sourceInputPath);
|
|
assertPathInside(
|
|
sourceRoot,
|
|
realSourceInputPath,
|
|
'Pinned hwdata build input'
|
|
);
|
|
const pnpIds = fileSystem.readFileSync(realSourceInputPath);
|
|
if (pnpIds.length === 0) {
|
|
throw new Error('Pinned hwdata pnp.ids build input must not be empty.');
|
|
}
|
|
|
|
const pkgDataDir = path.join(prefix, 'share', 'hwdata');
|
|
const pcFileDir = path.join(prefix, 'share', 'pkgconfig');
|
|
fileSystem.mkdirSync(pkgDataDir, { recursive: true });
|
|
fileSystem.mkdirSync(pcFileDir, { recursive: true });
|
|
fileSystem.writeFileSync(path.join(pkgDataDir, 'pnp.ids'), pnpIds, {
|
|
mode: 0o644,
|
|
});
|
|
fileSystem.writeFileSync(
|
|
path.join(pcFileDir, 'hwdata.pc'),
|
|
[
|
|
`prefix=${prefix}`,
|
|
'datadir=${prefix}/share',
|
|
`pkgdatadir=${pkgDataDir}`,
|
|
'',
|
|
'Name: hwdata',
|
|
'Description: Pinned PNP hardware identification data',
|
|
`Version: ${hwdataPackage.version}`,
|
|
'',
|
|
].join('\n'),
|
|
{ mode: 0o644 }
|
|
);
|
|
|
|
const pinnedEnvironment = createPinnedHwdataPkgConfigEnvironment({
|
|
buildEnvironment,
|
|
prefix,
|
|
});
|
|
const captureOptions = { env: pinnedEnvironment };
|
|
const resolvedPcFileDir = runCapture(
|
|
'pkg-config',
|
|
['--variable=pcfiledir', 'hwdata'],
|
|
captureOptions
|
|
);
|
|
const resolvedPkgDataDir = runCapture(
|
|
'pkg-config',
|
|
['--variable=pkgdatadir', 'hwdata'],
|
|
captureOptions
|
|
);
|
|
const resolvedVersion = runCapture(
|
|
'pkg-config',
|
|
['--modversion', 'hwdata'],
|
|
captureOptions
|
|
);
|
|
assertPinnedHwdataResolution({
|
|
pcFileDir: resolvedPcFileDir,
|
|
pkgDataDir: resolvedPkgDataDir,
|
|
prefix,
|
|
version: resolvedVersion,
|
|
});
|
|
return pinnedEnvironment;
|
|
}
|
|
|
|
function resolveLinuxPackageBuildEnvironment(packageId, context) {
|
|
if (packageId !== 'libdisplay-info') {
|
|
return context.buildEnvironment;
|
|
}
|
|
if (!context.hwdataBuildEnvironment) {
|
|
throw new Error(
|
|
'libdisplay-info requires the staged pinned hwdata build environment.'
|
|
);
|
|
}
|
|
return context.hwdataBuildEnvironment;
|
|
}
|
|
|
|
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, selectedNames = null) {
|
|
const realLibDir = fs.realpathSync(libDir);
|
|
for (const name of runtimeLibraryNames(libDir)) {
|
|
if (selectedNames && !selectedNames.has(name)) {
|
|
continue;
|
|
}
|
|
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 selectReachableRuntimeLibraryNames(entries) {
|
|
if (!Array.isArray(entries)) {
|
|
throw new TypeError('Runtime dynamic entries must be an array.');
|
|
}
|
|
|
|
const entriesByName = new Map();
|
|
for (const entry of entries) {
|
|
if (
|
|
!entry ||
|
|
typeof entry.name !== 'string' ||
|
|
!SHARED_LIBRARY_PATTERN.test(entry.name)
|
|
) {
|
|
throw new Error(
|
|
'Runtime dynamic entry has an invalid library name.'
|
|
);
|
|
}
|
|
if (entriesByName.has(entry.name)) {
|
|
throw new Error(
|
|
`Runtime dynamic entries contain duplicate library ${entry.name}.`
|
|
);
|
|
}
|
|
entriesByName.set(entry.name, entry);
|
|
}
|
|
|
|
const linkerAlias = entriesByName.get('libmpv.so');
|
|
if (!linkerAlias) {
|
|
throw new Error(
|
|
'Linux runtime must contain the libmpv.so linker alias.'
|
|
);
|
|
}
|
|
if (
|
|
typeof linkerAlias.soname !== 'string' ||
|
|
!SHARED_LIBRARY_PATTERN.test(linkerAlias.soname) ||
|
|
!entriesByName.has(linkerAlias.soname)
|
|
) {
|
|
throw new Error(
|
|
'libmpv.so must declare a bundled SONAME before runtime pruning.'
|
|
);
|
|
}
|
|
|
|
const reachableNames = new Set(['libmpv.so']);
|
|
const pendingNames = [linkerAlias.soname];
|
|
while (pendingNames.length > 0) {
|
|
const libraryName = pendingNames.shift();
|
|
if (reachableNames.has(libraryName)) {
|
|
continue;
|
|
}
|
|
reachableNames.add(libraryName);
|
|
const entry = entriesByName.get(libraryName);
|
|
if (!entry) {
|
|
throw new Error(
|
|
`Reachable runtime library ${libraryName} is missing its dynamic entry.`
|
|
);
|
|
}
|
|
for (const neededName of entry.needed ?? []) {
|
|
if (
|
|
entriesByName.has(neededName) &&
|
|
!reachableNames.has(neededName)
|
|
) {
|
|
pendingNames.push(neededName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...reachableNames].sort();
|
|
}
|
|
|
|
function retainRuntimeLibraries(libDir, retainedNames) {
|
|
if (!Array.isArray(retainedNames) || retainedNames.length === 0) {
|
|
throw new Error('Runtime retention list must be a non-empty array.');
|
|
}
|
|
const retainedNameSet = new Set(retainedNames);
|
|
if (retainedNameSet.size !== retainedNames.length) {
|
|
throw new Error('Runtime retention list contains duplicate libraries.');
|
|
}
|
|
|
|
const availableNames = runtimeLibraryNames(libDir);
|
|
for (const retainedName of retainedNameSet) {
|
|
if (!availableNames.includes(retainedName)) {
|
|
throw new Error(
|
|
`Retained runtime library does not exist: ${retainedName}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
materializeLibrarySymlinks(libDir, retainedNameSet);
|
|
for (const libraryName of availableNames) {
|
|
if (!retainedNameSet.has(libraryName)) {
|
|
fs.rmSync(path.join(libDir, libraryName));
|
|
}
|
|
}
|
|
|
|
for (const retainedName of retainedNameSet) {
|
|
const retainedPath = path.join(libDir, retainedName);
|
|
const stat = fs.lstatSync(retainedPath);
|
|
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
throw new Error(
|
|
`Retained runtime library ${retainedName} must be a materialized regular file.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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: [],
|
|
soname: null,
|
|
};
|
|
const dynamicEntryPattern =
|
|
/\((NEEDED|RPATH|RUNPATH|SONAME)\)[^[]*\[([^\]]*)\]/g;
|
|
for (const match of output.matchAll(dynamicEntryPattern)) {
|
|
const [, tag, value] = match;
|
|
if (tag === 'SONAME') {
|
|
dynamic.soname = value;
|
|
continue;
|
|
}
|
|
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 ['needed', 'rpath', 'runpath']) {
|
|
dynamic[field] = [...new Set(dynamic[field])].sort();
|
|
}
|
|
return dynamic;
|
|
}
|
|
|
|
function parseReadelfVersionInfo(output, name) {
|
|
if (typeof output !== 'string' || typeof name !== 'string' || !name) {
|
|
throw new TypeError(
|
|
'readelf version output and runtime library name are required.'
|
|
);
|
|
}
|
|
|
|
let requiredGlibc = null;
|
|
let requiredGlibcxx = null;
|
|
const versionPattern = /\b(GLIBCXX|GLIBC)_(\d+(?:\.\d+)+)\b/g;
|
|
for (const [, namespace, version] of output.matchAll(versionPattern)) {
|
|
if (
|
|
namespace === 'GLIBC' &&
|
|
(!requiredGlibc || compareVersions(version, requiredGlibc) > 0)
|
|
) {
|
|
requiredGlibc = version;
|
|
}
|
|
if (
|
|
namespace === 'GLIBCXX' &&
|
|
(!requiredGlibcxx || compareVersions(version, requiredGlibcxx) > 0)
|
|
) {
|
|
requiredGlibcxx = version;
|
|
}
|
|
}
|
|
|
|
return { name, requiredGlibc, requiredGlibcxx };
|
|
}
|
|
|
|
function assertPortableAbiRecords(records) {
|
|
if (!Array.isArray(records)) {
|
|
throw new TypeError('Runtime ABI records must be an array.');
|
|
}
|
|
for (const record of records) {
|
|
for (const [field, maximum] of [
|
|
['requiredGlibc', PORTABLE_ABI_BASELINE.glibcMaximum],
|
|
['requiredGlibcxx', PORTABLE_ABI_BASELINE.glibcxxMaximum],
|
|
]) {
|
|
const version = record?.[field];
|
|
if (version && compareVersions(version, maximum) > 0) {
|
|
throw new Error(
|
|
`Portable ABI baseline ${PORTABLE_ABI_BASELINE.distribution} rejects newer symbol ${version} required by ${record.name}; maximum ${field} is ${maximum}.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertPortableBuildHostGlibc(glibcVersion) {
|
|
if (
|
|
typeof glibcVersion !== 'string' ||
|
|
!/^\d+(?:\.\d+)+$/.test(glibcVersion)
|
|
) {
|
|
throw new Error(
|
|
'Unable to determine the Linux build host glibc version.'
|
|
);
|
|
}
|
|
if (compareVersions(glibcVersion, PORTABLE_ABI_BASELINE.glibcMaximum) > 0) {
|
|
throw new Error(
|
|
`Build host glibc ${glibcVersion} exceeds the portable ABI baseline ${PORTABLE_ABI_BASELINE.distribution} maximum ${PORTABLE_ABI_BASELINE.glibcMaximum}.`
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
soname: entry.soname ?? null,
|
|
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.soname !== null &&
|
|
(typeof entry.soname !== 'string' ||
|
|
!SHARED_LIBRARY_PATTERN.test(entry.soname) ||
|
|
path.basename(entry.soname) !== entry.soname)
|
|
) {
|
|
throw new Error(
|
|
`${entry.name} SONAME must be null or a safe shared-library basename.`
|
|
);
|
|
}
|
|
if (
|
|
entry.name === 'libmpv.so' &&
|
|
(typeof entry.soname !== 'string' ||
|
|
!/^libmpv\.so\.\d+(?:\.\d+)*$/.test(entry.soname) ||
|
|
!bundledNames.has(entry.soname))
|
|
) {
|
|
throw new Error(
|
|
'libmpv.so must declare a versioned SONAME present in the runtime closure.'
|
|
);
|
|
}
|
|
|
|
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] }
|
|
: {}),
|
|
...(sourceRecord.buildInput
|
|
? { buildInput: { ...sourceRecord.buildInput } }
|
|
: {}),
|
|
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,
|
|
abiRecords,
|
|
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
|
|
);
|
|
assertGitSubmodulesMatchPin(
|
|
sourcePackage,
|
|
sourceRecord.sourceSubmodules
|
|
);
|
|
}
|
|
packages[sourcePackage.id] = sourceManifestMetadata(sourceRecord);
|
|
}
|
|
assertPortableAbiRecords(abiRecords);
|
|
|
|
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,
|
|
runtimeAbi: {
|
|
baseline: { ...PORTABLE_ABI_BASELINE },
|
|
files: abiRecords.map((record) => ({ ...record })),
|
|
},
|
|
runtimeExternalConfiguration: {
|
|
fontconfig: { ...RUNTIME_EXTERNAL_CONFIGURATION.fontconfig },
|
|
openssl: { ...RUNTIME_EXTERNAL_CONFIGURATION.openssl },
|
|
},
|
|
runtimeDependencyClosure: {
|
|
entries: dependencyClosure.entries.map((entry) => ({
|
|
name: entry.name,
|
|
soname: entry.soname ?? null,
|
|
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, including the pinned dual-licensed hwdata archive whose pnp.ids is compiled into the MIT-licensed libdisplay-info source archive, 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_LIBPLACEBO_V7_360_1_SOURCE_SUBMODULES,
|
|
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
|
|
FFMPEG_CONFIGURE_FLAGS,
|
|
GLIBC_TOOLCHAIN_ALLOWLIST,
|
|
MINIMUM_TOOL_VERSIONS,
|
|
MPV_MESON_FLAGS,
|
|
OUTPUT_OWNERSHIP_MARKER,
|
|
PORTABLE_ABI_BASELINE,
|
|
REQUIRED_TOOLS,
|
|
RUNTIME_EXTERNAL_CONFIGURATION,
|
|
SOURCE_PACKAGES,
|
|
assertArchiveMatchesPin,
|
|
assertGitCommitMatchesPin,
|
|
assertGitSubmodulesMatchPin,
|
|
assertMinimumToolVersions,
|
|
assertOwnedOutputDestination,
|
|
assertPortableAbiRecords,
|
|
assertPortableBuildHostGlibc,
|
|
assertUniqueMesonOptionAssignments,
|
|
canonicalizeGitSubmoduleStatus,
|
|
compareVersions,
|
|
createBuildEnvironment,
|
|
createLinuxRuntimeManifest,
|
|
createOwnedStagingPrefix,
|
|
createRuntimeFileRecords,
|
|
materializeLibrarySymlinks,
|
|
ownedStagingPrefixPath,
|
|
parseCliInvocation,
|
|
parseReadelfDynamic,
|
|
parseReadelfVersionInfo,
|
|
parseVersion,
|
|
preparePinnedHwdataBuildInput,
|
|
resolveSystemPkgConfigDirs,
|
|
resolveLinuxPackageBuildEnvironment,
|
|
runtimeLibraryNames,
|
|
sha256Buffer,
|
|
publishOwnedOutput,
|
|
retainRuntimeLibraries,
|
|
selectReachableRuntimeLibraryNames,
|
|
validateRuntimeDependencyClosure,
|
|
};
|