mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
* docs: design Linux frame-copy packaging * docs: plan Linux frame-copy packaging * feat(packaging): define Linux frame-copy profiles * fix(packaging): reject inherited profile names * feat(embedded-mpv): validate staged Linux runtime * fix(embedded-mpv): require Linux source packages * fix(embedded-mpv): harden Linux runtime staging * feat(embedded-mpv): build LGPL Linux runtime * fix(embedded-mpv): pin Linux runtime inputs * feat(embedded-mpv): build relocatable Linux helper * fix(embedded-mpv): require bundled Linux runtime * fix(embedded-mpv): make Linux runtime portable * feat(packaging): ship Linux frame-copy artifacts * fix(embedded-mpv): verify Linux helper linkage * fix(packaging): enforce Linux frame-copy isolation * fix(embedded-mpv): pin Linux display data * docs(embedded-mpv): document Linux frame-copy packaging * feat(embedded-mpv): probe Linux frame-copy runtime * test(embedded-mpv): smoke packaged Linux frame-copy * docs(embedded-mpv): clarify Linux system runtime baseline * fix(embedded-mpv): harden Linux runtime capability gate * ci: verify Linux frame-copy packages * test(embedded-mpv): harden packaged Linux smoke * test(embedded-mpv): preserve packaged GL mode * test(packaging): harden Linux package probes * fix(embedded-mpv): enable private Snap shared memory * fix(embedded-mpv): sanitize Linux helper environment * fix(packaging): enforce private Snap memory semantics * fix(packaging): reject ambiguous Snap memory metadata * fix(embedded-mpv): prioritize trusted Snap GL * fix(packaging): reject advanced Snap YAML semantics * fix(packaging): reject arbitrary Snap YAML aliases * feat(packaging): ship Linux runtime license notices * docs(embedded-mpv): document Linux runtime distribution * fix(packaging): parse Snap trailing comments safely * fix(release): gate Snap publish on public source release * fix(packaging): strip VCS metadata from source bundle * docs(packaging): clarify Linux source release gate * test(embedded-mpv): smoke missing bundled libmpv * style(embedded-mpv): format final validation inputs * fix(e2e): satisfy fixture index signature typing * fix(ci): declare fontconfig gperf generator * fix(embedded-mpv): hash runtime cache identities * fix(packaging): harden Linux frame-copy delivery * fix(packaging): tighten runtime delivery gates * fix(ci): decouple Linux runtime matrix * fix(packaging): harden Linux frame-copy delivery * fix(packaging): validate Linux frame-copy runtimes * fix(packaging): scope Snap Electron library checks * feat(packaging): ship Linux frame-copy runtimes * fix(packaging): improve Linux runtime smoke diagnostics * fix(packaging): expose bounded helper probe details * test(packaging): trace Snap EGL probe failures * fix(packaging): prefer core22 ABI in Snap helper * fix(packaging): bound helper probe capture * fix(packaging): harden Linux frame-copy releases * fix(packaging): canonicalize libplacebo submodule identity * fix(packaging): make source archive inspection portable * fix(packaging): harden Snap release verification
1694 lines
53 KiB
JavaScript
1694 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',
|
|
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',
|
|
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',
|
|
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,
|
|
};
|