mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06: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
1639 lines
53 KiB
JavaScript
1639 lines
53 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { createRequire } from 'node:module';
|
|
import test from 'node:test';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const builderScript = path.join(currentDir, 'build-linux-runtime.mjs');
|
|
const builderHelpers = path.join(currentDir, 'build-linux-runtime.cjs');
|
|
const workspaceRoot = path.resolve(currentDir, '..', '..');
|
|
const require = createRequire(import.meta.url);
|
|
const {
|
|
BUILD_RECIPES,
|
|
BUILD_ORDER,
|
|
DEFAULT_SYSTEM_PKG_CONFIG_DIRS,
|
|
EXTERNAL_SYSTEM_LIBRARIES,
|
|
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,
|
|
assertMinimumToolVersions,
|
|
assertOwnedOutputDestination,
|
|
assertPortableAbiRecords,
|
|
assertPortableBuildHostGlibc,
|
|
assertUniqueMesonOptionAssignments,
|
|
canonicalizeGitSubmoduleStatus,
|
|
createBuildEnvironment,
|
|
createLinuxRuntimeManifest,
|
|
createOwnedStagingPrefix,
|
|
createRuntimeFileRecords,
|
|
materializeLibrarySymlinks,
|
|
parseCliInvocation,
|
|
parseReadelfDynamic,
|
|
parseReadelfVersionInfo,
|
|
preparePinnedHwdataBuildInput,
|
|
publishOwnedOutput,
|
|
retainRuntimeLibraries,
|
|
resolveLinuxPackageBuildEnvironment,
|
|
resolveSystemPkgConfigDirs,
|
|
selectReachableRuntimeLibraryNames,
|
|
validateRuntimeDependencyClosure,
|
|
} = require('./build-linux-runtime.cjs');
|
|
const {
|
|
validateLinuxRuntimeManifest,
|
|
} = require('./linux-runtime-manifest.cjs');
|
|
|
|
function createPinnedSourceRecords() {
|
|
return Object.fromEntries(
|
|
SOURCE_PACKAGES.map((sourcePackage) => [
|
|
sourcePackage.id,
|
|
{
|
|
...sourcePackage,
|
|
...(sourcePackage.sourceKind === 'git'
|
|
? {
|
|
sourceGitCommit: sourcePackage.expectedGitCommit,
|
|
sourceSubmodules: [
|
|
...sourcePackage.expectedSubmodules,
|
|
],
|
|
}
|
|
: {
|
|
sourceSha256: sourcePackage.expectedSha256,
|
|
}),
|
|
},
|
|
])
|
|
);
|
|
}
|
|
|
|
test('provides the Linux source runtime builder entrypoint and helpers', () => {
|
|
assert.equal(fs.existsSync(builderScript), true);
|
|
assert.equal(fs.existsSync(builderHelpers), true);
|
|
});
|
|
|
|
test('canonicalizes exact submodule identities independently of clone depth', () => {
|
|
const commit = '450bd2232225d6c7728a4108055ac2e37cef6475';
|
|
const path = '3rdparty/Vulkan-Headers';
|
|
assert.deepEqual(
|
|
canonicalizeGitSubmoduleStatus(` ${commit} ${path} (v1.4.337)\n`),
|
|
[`${commit} ${path}`]
|
|
);
|
|
assert.deepEqual(
|
|
canonicalizeGitSubmoduleStatus(`${commit} ${path} (450bd22)`),
|
|
[`${commit} ${path}`]
|
|
);
|
|
});
|
|
|
|
test('rejects non-clean, unsafe, and duplicate submodule status records', () => {
|
|
const commit = 'a'.repeat(40);
|
|
for (const output of [
|
|
`-${commit} 3rdparty/missing`,
|
|
`+${commit} 3rdparty/moved`,
|
|
`U${commit} 3rdparty/conflicted`,
|
|
` ${commit} ../outside`,
|
|
` ${commit} 3rdparty/example\n ${commit} 3rdparty/example`,
|
|
]) {
|
|
assert.throws(
|
|
() => canonicalizeGitSubmoduleStatus(output),
|
|
/not clean|unsafe|duplicate/i
|
|
);
|
|
}
|
|
});
|
|
|
|
test('pins the complete source stack and preserves dependency build order', () => {
|
|
assert.deepEqual(
|
|
SOURCE_PACKAGES.map(({ id, version }) => ({ id, version })),
|
|
[
|
|
{ id: 'freetype', version: '2.13.3' },
|
|
{ id: 'fribidi', version: '1.0.16' },
|
|
{ id: 'harfbuzz', version: '8.5.0' },
|
|
{ id: 'expat', version: '2.8.2' },
|
|
{ id: 'fontconfig', version: '2.16.0' },
|
|
{ id: 'libass', version: '0.17.3' },
|
|
{ id: 'openssl', version: '3.5.7' },
|
|
{ id: 'ffmpeg', version: '8.1' },
|
|
{ id: 'libplacebo', version: '7.360.1' },
|
|
{ id: 'hwdata', version: '0.409' },
|
|
{ id: 'libdisplay-info', version: '0.1.1' },
|
|
{ id: 'mpv', version: '0.41.0' },
|
|
]
|
|
);
|
|
assert.deepEqual(
|
|
BUILD_ORDER,
|
|
SOURCE_PACKAGES.map(({ id }) => id)
|
|
);
|
|
|
|
for (const sourcePackage of SOURCE_PACKAGES) {
|
|
assert.match(sourcePackage.sourceUrl, /^https:\/\//);
|
|
assert.ok(sourcePackage.license);
|
|
if (sourcePackage.id === 'libplacebo') {
|
|
assert.equal(sourcePackage.sourceTag, 'v7.360.1');
|
|
assert.equal(sourcePackage.sourceKind, 'git');
|
|
} else {
|
|
assert.equal(sourcePackage.sourceKind, 'archive');
|
|
}
|
|
}
|
|
|
|
const byId = new Map(
|
|
SOURCE_PACKAGES.map((sourcePackage) => [
|
|
sourcePackage.id,
|
|
sourcePackage,
|
|
])
|
|
);
|
|
assert.equal(
|
|
byId.get('expat').sourceUrl,
|
|
'https://github.com/libexpat/libexpat/releases/download/R_2_8_2/expat-2.8.2.tar.xz'
|
|
);
|
|
assert.equal(
|
|
byId.get('fontconfig').sourceUrl,
|
|
'https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.16.0.tar.xz'
|
|
);
|
|
assert.equal(
|
|
byId.get('openssl').sourceUrl,
|
|
'https://github.com/openssl/openssl/releases/download/openssl-3.5.7/openssl-3.5.7.tar.gz'
|
|
);
|
|
assert.deepEqual(byId.get('hwdata'), {
|
|
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: {
|
|
consumer: 'libdisplay-info',
|
|
relativePath: 'pnp.ids',
|
|
purpose: 'PNP vendor lookup table compiled into libdisplay-info.',
|
|
},
|
|
});
|
|
assert.deepEqual(byId.get('libdisplay-info'), {
|
|
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',
|
|
});
|
|
assert.ok(
|
|
BUILD_ORDER.indexOf('hwdata') < BUILD_ORDER.indexOf('libdisplay-info')
|
|
);
|
|
assert.ok(
|
|
BUILD_ORDER.indexOf('libdisplay-info') < BUILD_ORDER.indexOf('mpv')
|
|
);
|
|
});
|
|
|
|
test('hardcodes the verified official archive digests and libplacebo commit', () => {
|
|
const expectedArchivePins = {
|
|
expat: '3ad89b8588e6644bd4e49981480d48b21289eebbcd4f0a1a4afb1c29f99b6ab4',
|
|
ffmpeg: 'b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a',
|
|
fontconfig:
|
|
'6a33dc555cc9ba8b10caf7695878ef134eeb36d0af366041f639b1da9b6ed220',
|
|
freetype:
|
|
'0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289',
|
|
fribidi:
|
|
'1b1cde5b235d40479e91be2f0e88a309e3214c8ab470ec8a2744d82a5a9ea05c',
|
|
harfbuzz:
|
|
'77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27',
|
|
hwdata: '23006accc0f931dd5187d0307a57d0744e2b8feb85e73c37bc0f5229fb31eadd',
|
|
libass: 'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959',
|
|
'libdisplay-info':
|
|
'0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60',
|
|
mpv: 'ee21092a5ee427353392360929dc64645c54479aefdb5babc5cfbb5fad626209',
|
|
openssl:
|
|
'a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8',
|
|
};
|
|
assert.deepEqual(
|
|
Object.fromEntries(
|
|
SOURCE_PACKAGES.filter(({ sourceKind }) => sourceKind === 'archive')
|
|
.map(({ id, expectedSha256 }) => [id, expectedSha256])
|
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
),
|
|
expectedArchivePins
|
|
);
|
|
assert.equal(
|
|
SOURCE_PACKAGES.find(({ id }) => id === 'libplacebo').expectedGitCommit,
|
|
'cee9b076f2c63104ccfd497fa79c39a867293ec4'
|
|
);
|
|
});
|
|
|
|
test('rejects archive and git sources that differ from immutable pins', () => {
|
|
const freetype = SOURCE_PACKAGES.find(({ id }) => id === 'freetype');
|
|
assert.doesNotThrow(() =>
|
|
assertArchiveMatchesPin(freetype, freetype.expectedSha256)
|
|
);
|
|
assert.throws(
|
|
() => assertArchiveMatchesPin(freetype, '0'.repeat(64)),
|
|
/freetype.*SHA-256 mismatch.*expected 055035.*received 000000/i
|
|
);
|
|
|
|
const libplacebo = SOURCE_PACKAGES.find(({ id }) => id === 'libplacebo');
|
|
assert.doesNotThrow(() =>
|
|
assertGitCommitMatchesPin(libplacebo, libplacebo.expectedGitCommit)
|
|
);
|
|
assert.throws(
|
|
() => assertGitCommitMatchesPin(libplacebo, '0'.repeat(40)),
|
|
/libplacebo.*commit mismatch.*expected cee9b.*received 000000/i
|
|
);
|
|
});
|
|
|
|
test('verifies source pins before archive extraction or git submodules', () => {
|
|
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
|
assert.ok(
|
|
builderSource.indexOf(
|
|
'assertArchiveMatchesPin(sourcePackage, sourceSha256)'
|
|
) < builderSource.indexOf("context.run('tar'")
|
|
);
|
|
assert.ok(
|
|
builderSource.indexOf(
|
|
'assertGitCommitMatchesPin(sourcePackage, sourceGitCommit)'
|
|
) <
|
|
builderSource.indexOf(
|
|
"['submodule', 'update', '--init', '--recursive', '--depth', '1']"
|
|
)
|
|
);
|
|
});
|
|
|
|
test('rejects source metadata that does not match the immutable pins', () => {
|
|
const sourceRecords = createPinnedSourceRecords();
|
|
sourceRecords.freetype.sourceSha256 = '0'.repeat(64);
|
|
assert.throws(
|
|
() =>
|
|
createLinuxRuntimeManifest({
|
|
sourceRecords,
|
|
runtimeFiles: [
|
|
{
|
|
name: 'libmpv.so.2',
|
|
size: 1,
|
|
sha256: 'a'.repeat(64),
|
|
},
|
|
],
|
|
dependencyClosure: {
|
|
entries: [
|
|
{
|
|
name: 'libmpv.so.2',
|
|
needed: ['libc.so.6'],
|
|
rpath: [],
|
|
runpath: ['$ORIGIN'],
|
|
},
|
|
],
|
|
externalDependencies: ['libc.so.6'],
|
|
},
|
|
buildHost: {
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
tools: {},
|
|
},
|
|
}),
|
|
/freetype.*SHA-256 mismatch/i
|
|
);
|
|
});
|
|
|
|
test('defines shared-only source recipes with font discovery before playback', () => {
|
|
assert.deepEqual(Object.keys(BUILD_RECIPES), BUILD_ORDER);
|
|
assert.ok(BUILD_RECIPES.freetype.args.includes('--enable-shared'));
|
|
assert.ok(BUILD_RECIPES.freetype.args.includes('--disable-static'));
|
|
assert.ok(BUILD_RECIPES.fribidi.args.includes('--disable-docs'));
|
|
assert.ok(BUILD_RECIPES.fribidi.args.includes('--disable-bin'));
|
|
for (const flag of [
|
|
'-Dtests=disabled',
|
|
'-Ddocs=disabled',
|
|
'-Dutilities=disabled',
|
|
]) {
|
|
assert.ok(BUILD_RECIPES.harfbuzz.args.includes(flag), flag);
|
|
}
|
|
for (const flag of [
|
|
'-DEXPAT_SHARED_LIBS=ON',
|
|
'-DEXPAT_BUILD_TOOLS=OFF',
|
|
'-DEXPAT_BUILD_EXAMPLES=OFF',
|
|
'-DEXPAT_BUILD_TESTS=OFF',
|
|
'-DEXPAT_BUILD_DOCS=OFF',
|
|
]) {
|
|
assert.ok(BUILD_RECIPES.expat.args.includes(flag), flag);
|
|
}
|
|
for (const flag of [
|
|
'-Ddoc=disabled',
|
|
'-Dtests=disabled',
|
|
'-Dtools=disabled',
|
|
'-Dcache-build=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',
|
|
]) {
|
|
assert.ok(BUILD_RECIPES.fontconfig.args.includes(flag), flag);
|
|
}
|
|
assert.ok(BUILD_RECIPES.libass.args.includes('--enable-fontconfig'));
|
|
assert.equal(
|
|
BUILD_RECIPES.libass.args.includes('--disable-fontconfig'),
|
|
false
|
|
);
|
|
for (const flag of [
|
|
'shared',
|
|
'no-apps',
|
|
'no-docs',
|
|
'no-tests',
|
|
'--openssldir=/etc/ssl',
|
|
]) {
|
|
assert.ok(BUILD_RECIPES.openssl.args.includes(flag), flag);
|
|
}
|
|
assert.ok(
|
|
BUILD_ORDER.indexOf('fontconfig') < BUILD_ORDER.indexOf('libass')
|
|
);
|
|
assert.ok(BUILD_ORDER.indexOf('openssl') < BUILD_ORDER.indexOf('ffmpeg'));
|
|
assert.equal(BUILD_RECIPES.hwdata.buildSystem, 'data');
|
|
assert.equal(BUILD_RECIPES.hwdata.sharedOnly, false);
|
|
assert.equal(BUILD_RECIPES['libdisplay-info'].buildSystem, 'meson');
|
|
for (const packageId of BUILD_ORDER.filter(
|
|
(packageId) => packageId !== 'hwdata'
|
|
)) {
|
|
assert.equal(BUILD_RECIPES[packageId].sharedOnly, true, packageId);
|
|
}
|
|
});
|
|
|
|
test('stages pinned hwdata and excludes host pkg-config fallback', (t) => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-pinned-hwdata-')
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const sourcePath = path.join(root, 'sources', 'hwdata');
|
|
const prefix = path.join(root, 'runtime');
|
|
fs.mkdirSync(sourcePath, { recursive: true });
|
|
const pnpIds = 'ABC Example Display Vendor\n';
|
|
fs.writeFileSync(path.join(sourcePath, 'pnp.ids'), pnpIds);
|
|
|
|
const invocations = [];
|
|
const buildEnvironment = {
|
|
PATH: '/usr/bin',
|
|
PKG_CONFIG_PATH: '/host/pkgconfig',
|
|
PKG_CONFIG_LIBDIR:
|
|
'/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig',
|
|
};
|
|
const pinnedEnvironment = preparePinnedHwdataBuildInput({
|
|
buildEnvironment,
|
|
prefix,
|
|
runCapture(command, args, options) {
|
|
invocations.push({ args, command, env: options.env });
|
|
assert.equal(options.env.PKG_CONFIG_PATH.includes('/host'), false);
|
|
assert.equal(options.env.PKG_CONFIG_LIBDIR.includes('/usr'), false);
|
|
if (args[0] === '--variable=pcfiledir') {
|
|
return path.join(prefix, 'share', 'pkgconfig');
|
|
}
|
|
if (args[0] === '--variable=pkgdatadir') {
|
|
return path.join(prefix, 'share', 'hwdata');
|
|
}
|
|
if (args[0] === '--modversion') {
|
|
return '0.409';
|
|
}
|
|
throw new Error(`Unexpected pkg-config query: ${args.join(' ')}`);
|
|
},
|
|
sourcePath,
|
|
});
|
|
|
|
assert.equal(
|
|
fs.readFileSync(
|
|
path.join(prefix, 'share', 'hwdata', 'pnp.ids'),
|
|
'utf8'
|
|
),
|
|
pnpIds
|
|
);
|
|
const pkgConfig = fs.readFileSync(
|
|
path.join(prefix, 'share', 'pkgconfig', 'hwdata.pc'),
|
|
'utf8'
|
|
);
|
|
assert.match(pkgConfig, /^Version: 0\.409$/m);
|
|
assert.match(
|
|
pkgConfig,
|
|
new RegExp(
|
|
`^pkgdatadir=${path
|
|
.join(prefix, 'share', 'hwdata')
|
|
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
|
|
'm'
|
|
)
|
|
);
|
|
assert.doesNotMatch(pkgConfig, /\/usr\/share\/hwdata/);
|
|
assert.equal(pinnedEnvironment.PKG_CONFIG_PATH.includes('/host'), false);
|
|
assert.equal(pinnedEnvironment.PKG_CONFIG_LIBDIR.includes('/usr'), false);
|
|
assert.equal(
|
|
resolveLinuxPackageBuildEnvironment('libdisplay-info', {
|
|
buildEnvironment,
|
|
hwdataBuildEnvironment: pinnedEnvironment,
|
|
}),
|
|
pinnedEnvironment
|
|
);
|
|
assert.equal(
|
|
resolveLinuxPackageBuildEnvironment('mpv', {
|
|
buildEnvironment,
|
|
hwdataBuildEnvironment: pinnedEnvironment,
|
|
}),
|
|
buildEnvironment
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
resolveLinuxPackageBuildEnvironment('libdisplay-info', {
|
|
buildEnvironment,
|
|
}),
|
|
/libdisplay-info.*pinned hwdata/i
|
|
);
|
|
assert.deepEqual(
|
|
invocations.map(({ args, command }) => [command, ...args]),
|
|
[
|
|
['pkg-config', '--variable=pcfiledir', 'hwdata'],
|
|
['pkg-config', '--variable=pkgdatadir', 'hwdata'],
|
|
['pkg-config', '--modversion', 'hwdata'],
|
|
]
|
|
);
|
|
|
|
assert.throws(
|
|
() =>
|
|
preparePinnedHwdataBuildInput({
|
|
buildEnvironment,
|
|
prefix: path.join(root, 'rejected-runtime'),
|
|
runCapture(_command, args) {
|
|
return args[0] === '--modversion'
|
|
? '0.409'
|
|
: '/usr/share/hwdata';
|
|
},
|
|
sourcePath,
|
|
}),
|
|
/pinned hwdata.*host|host.*hwdata|resolved outside/i
|
|
);
|
|
});
|
|
|
|
test('rejects duplicate option assignments in every Meson recipe', () => {
|
|
assert.doesNotThrow(() =>
|
|
assertUniqueMesonOptionAssignments(BUILD_RECIPES)
|
|
);
|
|
|
|
for (const [packageId, duplicateFlag] of [
|
|
['fontconfig', '-Dxml-backend=libxml2'],
|
|
['libplacebo', '-Dvulkan=enabled'],
|
|
]) {
|
|
const duplicateRecipes = {
|
|
...BUILD_RECIPES,
|
|
[packageId]: {
|
|
...BUILD_RECIPES[packageId],
|
|
args: [...BUILD_RECIPES[packageId].args, duplicateFlag],
|
|
},
|
|
};
|
|
assert.throws(
|
|
() => assertUniqueMesonOptionAssignments(duplicateRecipes),
|
|
new RegExp(
|
|
`${packageId}.*${duplicateFlag.split('=')[0]}.*once`,
|
|
'i'
|
|
)
|
|
);
|
|
}
|
|
});
|
|
|
|
test('constructs a prefix-only build environment and ignores hostile host flags', () => {
|
|
const environment = createBuildEnvironment({
|
|
prefix: '/opt/runtime',
|
|
baseEnv: {
|
|
PATH: '/usr/bin',
|
|
KEEP_ME: 'benign',
|
|
CPPFLAGS: '-I/host/include',
|
|
CFLAGS: '-O0 -march=native',
|
|
CXXFLAGS: '-stdlib=hostile',
|
|
LDFLAGS: '-L/host/lib -Wl,--as-needed',
|
|
LD_LIBRARY_PATH: '/host/runtime',
|
|
LIBRARY_PATH: '/host/implicit',
|
|
CPATH: '/host/cpath',
|
|
C_INCLUDE_PATH: '/host/c-include',
|
|
CPLUS_INCLUDE_PATH: '/host/cxx-include',
|
|
CMAKE_PREFIX_PATH: '/host/cmake',
|
|
PKG_CONFIG_PATH: '/host/pkgconfig',
|
|
PKG_CONFIG_LIBDIR: '/host/pkgconfig-libdir',
|
|
PKG_CONFIG_SYSROOT_DIR: '/host/sysroot',
|
|
FONTCONFIG_PATH: '/host/fonts',
|
|
OPENSSL_MODULES: '/host/openssl-modules',
|
|
},
|
|
systemPkgConfigDirs: [
|
|
'/usr/lib/x86_64-linux-gnu/pkgconfig',
|
|
'/usr/share/pkgconfig',
|
|
],
|
|
});
|
|
|
|
assert.equal(environment.PATH, '/opt/runtime/bin:/usr/bin');
|
|
assert.equal(
|
|
environment.PKG_CONFIG_PATH,
|
|
'/opt/runtime/lib/pkgconfig:/opt/runtime/share/pkgconfig'
|
|
);
|
|
assert.equal(
|
|
environment.PKG_CONFIG_LIBDIR,
|
|
[
|
|
'/opt/runtime/lib/pkgconfig',
|
|
'/opt/runtime/share/pkgconfig',
|
|
'/usr/lib/x86_64-linux-gnu/pkgconfig',
|
|
'/usr/share/pkgconfig',
|
|
].join(':')
|
|
);
|
|
assert.equal(environment.CPPFLAGS, '-I/opt/runtime/include');
|
|
assert.equal(environment.CFLAGS, '-fPIC -I/opt/runtime/include');
|
|
assert.equal(environment.CXXFLAGS, '-fPIC -I/opt/runtime/include');
|
|
assert.equal(
|
|
environment.LDFLAGS,
|
|
'-L/opt/runtime/lib -Wl,-rpath-link,/opt/runtime/lib'
|
|
);
|
|
assert.equal(environment.LD_LIBRARY_PATH, '/opt/runtime/lib');
|
|
assert.equal(environment.CMAKE_PREFIX_PATH, '/opt/runtime');
|
|
assert.equal(environment.KEEP_ME, 'benign');
|
|
for (const variable of [
|
|
'LIBRARY_PATH',
|
|
'CPATH',
|
|
'C_INCLUDE_PATH',
|
|
'CPLUS_INCLUDE_PATH',
|
|
'PKG_CONFIG_SYSROOT_DIR',
|
|
]) {
|
|
assert.equal(environment[variable], undefined, variable);
|
|
}
|
|
assert.doesNotMatch(JSON.stringify(environment), /\/host\//);
|
|
});
|
|
|
|
test('rejects an existing unmarked output without changing its contents', (t) => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-output-ownership-')
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const outputPrefix = path.join(root, 'usr', 'local');
|
|
fs.mkdirSync(outputPrefix, { recursive: true });
|
|
fs.writeFileSync(path.join(outputPrefix, 'keep-me'), 'untouched');
|
|
|
|
assert.throws(
|
|
() => assertOwnedOutputDestination(outputPrefix),
|
|
/existing output.*ownership marker/i
|
|
);
|
|
assert.equal(
|
|
fs.readFileSync(path.join(outputPrefix, 'keep-me'), 'utf8'),
|
|
'untouched'
|
|
);
|
|
});
|
|
|
|
test('atomically publishes only owned outputs and rolls back failures', (t) => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-output-publish-')
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const outputPrefix = path.join(root, 'runtime');
|
|
fs.mkdirSync(outputPrefix);
|
|
fs.writeFileSync(
|
|
path.join(outputPrefix, OUTPUT_OWNERSHIP_MARKER),
|
|
'iptvnator-embedded-mpv-linux-runtime-v1\n'
|
|
);
|
|
fs.writeFileSync(path.join(outputPrefix, 'state'), 'previous');
|
|
|
|
const failedStagingPrefix = createOwnedStagingPrefix(outputPrefix, {
|
|
token: 'failed',
|
|
});
|
|
fs.writeFileSync(path.join(failedStagingPrefix, 'state'), 'replacement');
|
|
let renameCount = 0;
|
|
const failingFileSystem = {
|
|
...fs,
|
|
renameSync(source, destination) {
|
|
renameCount += 1;
|
|
if (renameCount === 2) {
|
|
throw new Error('injected publication failure');
|
|
}
|
|
return fs.renameSync(source, destination);
|
|
},
|
|
};
|
|
assert.throws(
|
|
() =>
|
|
publishOwnedOutput({
|
|
fileSystem: failingFileSystem,
|
|
outputPrefix,
|
|
stagingPrefix: failedStagingPrefix,
|
|
token: 'rollback',
|
|
}),
|
|
/injected publication failure/
|
|
);
|
|
assert.equal(
|
|
fs.readFileSync(path.join(outputPrefix, 'state'), 'utf8'),
|
|
'previous'
|
|
);
|
|
assert.equal(fs.existsSync(failedStagingPrefix), false);
|
|
assert.deepEqual(
|
|
fs.readdirSync(root).filter((name) => name.includes('backup')),
|
|
[]
|
|
);
|
|
|
|
const successfulStagingPrefix = createOwnedStagingPrefix(outputPrefix, {
|
|
token: 'successful',
|
|
});
|
|
fs.writeFileSync(
|
|
path.join(successfulStagingPrefix, 'state'),
|
|
'replacement'
|
|
);
|
|
publishOwnedOutput({
|
|
outputPrefix,
|
|
stagingPrefix: successfulStagingPrefix,
|
|
token: 'success',
|
|
});
|
|
assert.equal(
|
|
fs.readFileSync(path.join(outputPrefix, 'state'), 'utf8'),
|
|
'replacement'
|
|
);
|
|
assert.equal(
|
|
fs.readFileSync(
|
|
path.join(outputPrefix, OUTPUT_OWNERSHIP_MARKER),
|
|
'utf8'
|
|
),
|
|
'iptvnator-embedded-mpv-linux-runtime-v1\n'
|
|
);
|
|
});
|
|
|
|
test('builds in an owned sibling before atomically publishing output', () => {
|
|
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
|
assert.doesNotMatch(
|
|
builderSource,
|
|
/fs\.rmSync\(context\.prefix, \{ recursive: true, force: true \}\)/
|
|
);
|
|
const toolPreflight = builderSource.indexOf(
|
|
'assertMinimumToolVersions(toolVersions)'
|
|
);
|
|
const stagingMutation = builderSource.indexOf(
|
|
'createOwnedStagingPrefix(outputPrefix'
|
|
);
|
|
const publication = builderSource.indexOf('publishOwnedOutput({');
|
|
assert.notEqual(stagingMutation, -1);
|
|
assert.notEqual(publication, -1);
|
|
assert.ok(toolPreflight < stagingMutation);
|
|
assert.ok(stagingMutation < publication);
|
|
});
|
|
|
|
test('uses fixed Linux x64 pkg-config directories without host discovery', () => {
|
|
assert.deepEqual(DEFAULT_SYSTEM_PKG_CONFIG_DIRS, [
|
|
'/usr/lib/x86_64-linux-gnu/pkgconfig',
|
|
'/usr/lib64/pkgconfig',
|
|
'/usr/lib/pkgconfig',
|
|
'/usr/share/pkgconfig',
|
|
]);
|
|
assert.deepEqual(
|
|
resolveSystemPkgConfigDirs({}),
|
|
DEFAULT_SYSTEM_PKG_CONFIG_DIRS
|
|
);
|
|
assert.deepEqual(
|
|
resolveSystemPkgConfigDirs({
|
|
IPTVNATOR_EMBEDDED_MPV_SYSTEM_PKG_CONFIG_DIRS:
|
|
'/opt/interfaces/pkgconfig:/usr/share/pkgconfig:/opt/interfaces/pkgconfig',
|
|
}),
|
|
['/opt/interfaces/pkgconfig', '/usr/share/pkgconfig']
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
resolveSystemPkgConfigDirs({
|
|
IPTVNATOR_EMBEDDED_MPV_SYSTEM_PKG_CONFIG_DIRS:
|
|
'relative/pkgconfig',
|
|
}),
|
|
/must contain only absolute paths/
|
|
);
|
|
|
|
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
|
assert.doesNotMatch(builderSource, /--variable['"],\s*['"]pc_path/);
|
|
});
|
|
|
|
test('declares only the intended Linux system interface packages', () => {
|
|
assert.deepEqual(EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES, [
|
|
'alsa',
|
|
'egl',
|
|
'gbm',
|
|
'gl',
|
|
'libdrm',
|
|
'libpulse',
|
|
'libva',
|
|
'libva-drm',
|
|
]);
|
|
|
|
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
|
assert.match(
|
|
builderSource,
|
|
/pkg-config['"],\s*\[['"]--exists['"],\s*packageName\]/
|
|
);
|
|
assert.match(
|
|
builderSource,
|
|
/pkg-config['"],\s*\[['"]--modversion['"],\s*packageName\]/
|
|
);
|
|
});
|
|
|
|
test('requires ELF patching and inspection tools in addition to the build toolchain', () => {
|
|
for (const tool of [
|
|
'cc',
|
|
'cmake',
|
|
'curl',
|
|
'git',
|
|
'gperf',
|
|
'make',
|
|
'meson',
|
|
'nasm',
|
|
'ninja',
|
|
'patchelf',
|
|
'pkg-config',
|
|
'readelf',
|
|
'tar',
|
|
]) {
|
|
assert.ok(REQUIRED_TOOLS.includes(tool), tool);
|
|
}
|
|
});
|
|
|
|
test('preflights every required tool against a supported minimum version', () => {
|
|
assert.deepEqual(
|
|
Object.keys(MINIMUM_TOOL_VERSIONS).sort(),
|
|
[...REQUIRED_TOOLS].sort()
|
|
);
|
|
assert.equal(MINIMUM_TOOL_VERSIONS.meson, '1.6.0');
|
|
assert.equal(MINIMUM_TOOL_VERSIONS.nasm, '2.15.05');
|
|
assert.equal(MINIMUM_TOOL_VERSIONS.gperf, '3.1.0');
|
|
|
|
const supportedVersions = Object.fromEntries(
|
|
REQUIRED_TOOLS.map((tool) => [
|
|
tool,
|
|
`${tool} ${MINIMUM_TOOL_VERSIONS[tool]}`,
|
|
])
|
|
);
|
|
assert.doesNotThrow(() => assertMinimumToolVersions(supportedVersions));
|
|
|
|
const missingTool = { ...supportedVersions };
|
|
delete missingTool.nasm;
|
|
assert.throws(
|
|
() => assertMinimumToolVersions(missingTool),
|
|
/missing required tool version.*nasm/i
|
|
);
|
|
|
|
for (const [tool, oldVersion] of [
|
|
['gperf', 'GNU gperf 3.0.4'],
|
|
['meson', 'meson 1.5.9'],
|
|
['nasm', 'NASM version 2.15.04'],
|
|
]) {
|
|
assert.throws(
|
|
() =>
|
|
assertMinimumToolVersions({
|
|
...supportedVersions,
|
|
[tool]: oldVersion,
|
|
}),
|
|
new RegExp(`${tool}.*requires.*${MINIMUM_TOOL_VERSIONS[tool]}`, 'i')
|
|
);
|
|
}
|
|
});
|
|
|
|
test('completes tool and system-package preflight before filesystem mutation', () => {
|
|
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
|
const buildRootMutation = builderSource.indexOf(
|
|
'fs.mkdirSync(context.buildRoot'
|
|
);
|
|
const toolPreflight = builderSource.indexOf(
|
|
'assertMinimumToolVersions(toolVersions)'
|
|
);
|
|
const packagePreflight = builderSource.indexOf(
|
|
'verifySystemPkgConfigPackages(context)'
|
|
);
|
|
const mesonPolicyPreflight = builderSource.indexOf(
|
|
'assertUniqueMesonOptionAssignments(BUILD_RECIPES)'
|
|
);
|
|
assert.notEqual(mesonPolicyPreflight, -1);
|
|
assert.notEqual(toolPreflight, -1);
|
|
assert.notEqual(packagePreflight, -1);
|
|
assert.ok(mesonPolicyPreflight < buildRootMutation);
|
|
assert.ok(toolPreflight < buildRootMutation);
|
|
assert.ok(packagePreflight < buildRootMutation);
|
|
});
|
|
|
|
test('uses DESTDIR with standard fontconfig and OpenSSL runtime paths', () => {
|
|
assert.deepEqual(RUNTIME_EXTERNAL_CONFIGURATION, {
|
|
fontconfig: {
|
|
configDirectory: '/etc/fonts',
|
|
templateDirectory: '/usr/share/fontconfig',
|
|
cacheDirectory: '/var/cache/fontconfig',
|
|
ownership: 'system',
|
|
},
|
|
openssl: {
|
|
configFile: '/etc/ssl/openssl.cnf',
|
|
certificateFile: '/etc/ssl/cert.pem',
|
|
certificateDirectory: '/etc/ssl/certs',
|
|
ownership: 'system',
|
|
},
|
|
});
|
|
assert.doesNotMatch(
|
|
JSON.stringify(RUNTIME_EXTERNAL_CONFIGURATION),
|
|
/build|prefix|tmp/i
|
|
);
|
|
|
|
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
|
assert.match(builderSource, /DESTDIR/);
|
|
assert.match(builderSource, /installWithDestdir/);
|
|
assert.doesNotMatch(
|
|
builderSource,
|
|
/--openssldir=\$\{path\.join\(context\.prefix/
|
|
);
|
|
});
|
|
|
|
test('preserves relative library symlinks copied out of DESTDIR', async (t) => {
|
|
const { copyDirectoryContents } = await import(
|
|
pathToFileURL(builderScript).href
|
|
);
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-destdir-links-')
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const sourceDirectory = path.join(root, 'destdir', 'lib');
|
|
const destinationDirectory = path.join(root, 'prefix', 'lib');
|
|
fs.mkdirSync(sourceDirectory, { recursive: true });
|
|
fs.writeFileSync(path.join(sourceDirectory, 'libcrypto.so.3'), 'crypto');
|
|
fs.symlinkSync(
|
|
'libcrypto.so.3',
|
|
path.join(sourceDirectory, 'libcrypto.so')
|
|
);
|
|
|
|
copyDirectoryContents(sourceDirectory, destinationDirectory);
|
|
fs.rmSync(path.join(root, 'destdir'), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
|
|
assert.equal(
|
|
fs.readlinkSync(path.join(destinationDirectory, 'libcrypto.so')),
|
|
'libcrypto.so.3'
|
|
);
|
|
assert.equal(
|
|
fs.readFileSync(
|
|
path.join(destinationDirectory, 'libcrypto.so'),
|
|
'utf8'
|
|
),
|
|
'crypto'
|
|
);
|
|
});
|
|
|
|
test('uses an explicit LGPL FFmpeg HTTPS and HLS protocol baseline', () => {
|
|
const required = [
|
|
'--enable-shared',
|
|
'--disable-static',
|
|
'--disable-programs',
|
|
'--disable-doc',
|
|
'--disable-debug',
|
|
'--disable-autodetect',
|
|
'--disable-gpl',
|
|
'--disable-nonfree',
|
|
'--enable-pic',
|
|
'--enable-pthreads',
|
|
'--enable-openssl',
|
|
'--enable-network',
|
|
'--enable-protocol=file',
|
|
'--enable-protocol=http',
|
|
'--enable-protocol=https',
|
|
'--enable-protocol=tcp',
|
|
'--enable-protocol=tls',
|
|
'--enable-protocol=udp',
|
|
'--enable-protocol=crypto',
|
|
'--enable-protocol=data',
|
|
'--enable-demuxer=hls',
|
|
'--enable-vaapi',
|
|
];
|
|
|
|
for (const flag of required) {
|
|
assert.ok(FFMPEG_CONFIGURE_FLAGS.includes(flag), `missing ${flag}`);
|
|
}
|
|
for (const forbidden of [
|
|
'--enable-gpl',
|
|
'--enable-nonfree',
|
|
'--enable-version3',
|
|
]) {
|
|
assert.equal(FFMPEG_CONFIGURE_FLAGS.includes(forbidden), false);
|
|
}
|
|
});
|
|
|
|
test('uses valid mpv v0.41 Meson option names and pins the Linux backends', () => {
|
|
const upstreamMpv041Options = new Set([
|
|
'aaudio',
|
|
'alsa',
|
|
'android-media-ndk',
|
|
'audiotrack',
|
|
'audiounit',
|
|
'avfoundation',
|
|
'build-date',
|
|
'caca',
|
|
'cdda',
|
|
'coreaudio',
|
|
'cocoa',
|
|
'cplayer',
|
|
'cplugins',
|
|
'cuda-hwaccel',
|
|
'cuda-interop',
|
|
'd3d-hwaccel',
|
|
'd3d11',
|
|
'd3d9-hwaccel',
|
|
'direct3d',
|
|
'disable-packet-pool',
|
|
'dmabuf-wayland',
|
|
'drm',
|
|
'dvbin',
|
|
'dvdnav',
|
|
'egl',
|
|
'egl-android',
|
|
'egl-angle',
|
|
'egl-angle-lib',
|
|
'egl-angle-win32',
|
|
'egl-drm',
|
|
'egl-wayland',
|
|
'egl-x11',
|
|
'fuzzers',
|
|
'gbm',
|
|
'gl',
|
|
'gl-cocoa',
|
|
'gl-dxinterop',
|
|
'gl-dxinterop-d3d9',
|
|
'gl-win32',
|
|
'gl-x11',
|
|
'gpl',
|
|
'html-build',
|
|
'iconv',
|
|
'ios-gl',
|
|
'jack',
|
|
'javascript',
|
|
'jpeg',
|
|
'lcms2',
|
|
'libarchive',
|
|
'libavdevice',
|
|
'libbluray',
|
|
'libmpv',
|
|
'lua',
|
|
'macos-10-15-4-features',
|
|
'macos-11-3-features',
|
|
'macos-11-features',
|
|
'macos-12-features',
|
|
'macos-cocoa-cb',
|
|
'macos-media-player',
|
|
'macos-touchbar',
|
|
'manpage-build',
|
|
'openal',
|
|
'opensles',
|
|
'oss-audio',
|
|
'pdf-build',
|
|
'pipewire',
|
|
'plain-gl',
|
|
'pthread-debug',
|
|
'pulse',
|
|
'rubberband',
|
|
'sdl2-audio',
|
|
'sdl2-gamepad',
|
|
'sdl2-video',
|
|
'shaderc',
|
|
'sixel',
|
|
'sndio',
|
|
'spirv-cross',
|
|
'swift-build',
|
|
'tests',
|
|
'uchardet',
|
|
'uwp',
|
|
'vaapi',
|
|
'vaapi-drm',
|
|
'vaapi-wayland',
|
|
'vaapi-win32',
|
|
'vaapi-x11',
|
|
'vapoursynth',
|
|
'vdpau',
|
|
'vdpau-gl-x11',
|
|
'vector',
|
|
'videotoolbox-gl',
|
|
'videotoolbox-pl',
|
|
'vulkan',
|
|
'wasapi',
|
|
'wayland',
|
|
'win32-smtc',
|
|
'win32-threads',
|
|
'x11',
|
|
'x11-clipboard',
|
|
'xv',
|
|
'zimg',
|
|
'zlib',
|
|
]);
|
|
|
|
for (const flag of MPV_MESON_FLAGS) {
|
|
const optionName = flag.slice(2).split('=')[0];
|
|
assert.ok(
|
|
upstreamMpv041Options.has(optionName),
|
|
`unknown mpv v0.41 option ${optionName}`
|
|
);
|
|
assert.doesNotMatch(flag, /=auto$/);
|
|
}
|
|
|
|
for (const required of [
|
|
'-Dgpl=false',
|
|
'-Dlibmpv=true',
|
|
'-Dcplayer=false',
|
|
'-Dtests=false',
|
|
'-Dlua=disabled',
|
|
'-Djavascript=disabled',
|
|
'-Dcplugins=disabled',
|
|
'-Dmanpage-build=disabled',
|
|
'-Dhtml-build=disabled',
|
|
'-Dpdf-build=disabled',
|
|
'-Dlibarchive=disabled',
|
|
'-Dlibbluray=disabled',
|
|
'-Ddvdnav=disabled',
|
|
'-Dcdda=disabled',
|
|
'-Ddvbin=disabled',
|
|
'-Dvulkan=disabled',
|
|
'-Dshaderc=disabled',
|
|
'-Dspirv-cross=disabled',
|
|
'-Ddrm=enabled',
|
|
'-Dgl=enabled',
|
|
'-Dplain-gl=enabled',
|
|
'-Degl=enabled',
|
|
'-Dgbm=enabled',
|
|
'-Dpulse=enabled',
|
|
'-Dalsa=enabled',
|
|
'-Dvaapi=enabled',
|
|
'-Dvaapi-drm=enabled',
|
|
]) {
|
|
assert.ok(MPV_MESON_FLAGS.includes(required), `missing ${required}`);
|
|
}
|
|
assert.equal(MPV_MESON_FLAGS.includes('-Ddrm=disabled'), false);
|
|
for (const optionName of ['drm', 'gbm', 'vaapi-drm']) {
|
|
const assignments = MPV_MESON_FLAGS.filter((flag) =>
|
|
flag.startsWith(`-D${optionName}=`)
|
|
);
|
|
assert.deepEqual(assignments, [`-D${optionName}=enabled`]);
|
|
}
|
|
});
|
|
|
|
test('keeps the external dependency allowlists explicit and deterministic', () => {
|
|
assert.deepEqual(GLIBC_TOOLCHAIN_ALLOWLIST, [
|
|
'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',
|
|
]);
|
|
assert.deepEqual(
|
|
EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
|
|
[
|
|
'libEGL.so.1',
|
|
'libGL.so.1',
|
|
'libGLX.so.0',
|
|
'libOpenGL.so.0',
|
|
'libasound.so.2',
|
|
'libdrm.so.2',
|
|
'libgbm.so.1',
|
|
'libpulse.so.0',
|
|
'libva-drm.so.2',
|
|
'libva.so.2',
|
|
]
|
|
);
|
|
for (const externalLibrary of EXTERNAL_SYSTEM_LIBRARIES) {
|
|
assert.ok(externalLibrary.interface);
|
|
assert.ok(externalLibrary.reason);
|
|
}
|
|
});
|
|
|
|
test('parses readelf dynamic sections and validates an ORIGIN-only closure', () => {
|
|
const mpvDynamic = parseReadelfDynamic(`
|
|
0x000000000000000e (SONAME) Library soname: [libmpv.so.2]
|
|
0x0000000000000001 (NEEDED) Shared library: [libavcodec.so.62]
|
|
0x0000000000000001 (NEEDED) Shared library: [libEGL.so.1]
|
|
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
|
|
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN]
|
|
`);
|
|
assert.deepEqual(mpvDynamic, {
|
|
needed: ['libEGL.so.1', 'libavcodec.so.62', 'libc.so.6'],
|
|
rpath: [],
|
|
runpath: ['$ORIGIN'],
|
|
soname: 'libmpv.so.2',
|
|
});
|
|
|
|
const closure = validateRuntimeDependencyClosure({
|
|
entries: [
|
|
{
|
|
name: 'libavcodec.so.62',
|
|
...parseReadelfDynamic(`
|
|
0x0000000000000001 (NEEDED) Shared library: [libm.so.6]
|
|
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN]
|
|
`),
|
|
},
|
|
{ name: 'libmpv.so.2', ...mpvDynamic },
|
|
],
|
|
runtimeFileNames: ['libavcodec.so.62', 'libmpv.so.2'],
|
|
buildPrefix: '/tmp/iptvnator-prefix',
|
|
});
|
|
assert.deepEqual(closure.externalDependencies, [
|
|
'libEGL.so.1',
|
|
'libc.so.6',
|
|
'libm.so.6',
|
|
]);
|
|
assert.deepEqual(closure.entries[1].needed, [
|
|
'libEGL.so.1',
|
|
'libavcodec.so.62',
|
|
'libc.so.6',
|
|
]);
|
|
assert.equal(closure.entries[1].soname, 'libmpv.so.2');
|
|
|
|
const aliasClosure = validateRuntimeDependencyClosure({
|
|
entries: [
|
|
{
|
|
name: 'libmpv.so',
|
|
...mpvDynamic,
|
|
},
|
|
{
|
|
name: 'libmpv.so.2',
|
|
...mpvDynamic,
|
|
},
|
|
{
|
|
name: 'libavcodec.so.62',
|
|
needed: ['libm.so.6'],
|
|
rpath: [],
|
|
runpath: ['$ORIGIN'],
|
|
soname: 'libavcodec.so.62',
|
|
},
|
|
],
|
|
runtimeFileNames: ['libavcodec.so.62', 'libmpv.so', 'libmpv.so.2'],
|
|
buildPrefix: '/tmp/iptvnator-prefix',
|
|
});
|
|
assert.equal(
|
|
aliasClosure.entries.find(({ name }) => name === 'libmpv.so').soname,
|
|
'libmpv.so.2'
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
validateRuntimeDependencyClosure({
|
|
entries: aliasClosure.entries.map((entry) =>
|
|
entry.name === 'libmpv.so'
|
|
? { ...entry, soname: 'libmpv.so.99' }
|
|
: entry
|
|
),
|
|
runtimeFileNames: [
|
|
'libavcodec.so.62',
|
|
'libmpv.so',
|
|
'libmpv.so.2',
|
|
],
|
|
buildPrefix: '/tmp/iptvnator-prefix',
|
|
}),
|
|
/libmpv\.so must declare a versioned SONAME present in the runtime closure/
|
|
);
|
|
|
|
assert.throws(
|
|
() =>
|
|
validateRuntimeDependencyClosure({
|
|
entries: [
|
|
{
|
|
name: 'libmpv.so.2',
|
|
needed: ['libsurprise.so.1'],
|
|
rpath: [],
|
|
runpath: ['$ORIGIN'],
|
|
},
|
|
],
|
|
runtimeFileNames: ['libmpv.so.2'],
|
|
buildPrefix: '/tmp/iptvnator-prefix',
|
|
}),
|
|
/not bundled or allowlisted.*libsurprise\.so\.1/
|
|
);
|
|
for (const forbiddenRunpath of [
|
|
'/tmp/iptvnator-prefix/lib',
|
|
'/usr/local/lib',
|
|
'$ORIGIN:/tmp/host-lib',
|
|
]) {
|
|
assert.throws(
|
|
() =>
|
|
validateRuntimeDependencyClosure({
|
|
entries: [
|
|
{
|
|
name: 'libmpv.so.2',
|
|
needed: ['libc.so.6'],
|
|
rpath: [],
|
|
runpath: [forbiddenRunpath],
|
|
},
|
|
],
|
|
runtimeFileNames: ['libmpv.so.2'],
|
|
buildPrefix: '/tmp/iptvnator-prefix',
|
|
}),
|
|
/RUNPATH/
|
|
);
|
|
}
|
|
});
|
|
|
|
test('retains only the reachable SONAME closure plus the libmpv linker alias', (t) => {
|
|
const entries = [
|
|
{
|
|
name: 'libmpv.so',
|
|
soname: 'libmpv.so.2',
|
|
needed: ['libavcodec.so.62', 'libc.so.6'],
|
|
},
|
|
{
|
|
name: 'libmpv.so.2',
|
|
soname: 'libmpv.so.2',
|
|
needed: ['libavcodec.so.62', 'libc.so.6'],
|
|
},
|
|
{
|
|
name: 'libmpv.so.2.0.0',
|
|
soname: 'libmpv.so.2',
|
|
needed: ['libavcodec.so.62', 'libc.so.6'],
|
|
},
|
|
{
|
|
name: 'libavcodec.so',
|
|
soname: 'libavcodec.so.62',
|
|
needed: ['libm.so.6'],
|
|
},
|
|
{
|
|
name: 'libavcodec.so.62',
|
|
soname: 'libavcodec.so.62',
|
|
needed: ['libm.so.6'],
|
|
},
|
|
{
|
|
name: 'libavcodec.so.62.1.0',
|
|
soname: 'libavcodec.so.62',
|
|
needed: ['libm.so.6'],
|
|
},
|
|
{
|
|
name: 'libunused.so.1',
|
|
soname: 'libunused.so.1',
|
|
needed: ['libc.so.6'],
|
|
},
|
|
];
|
|
const retainedNames = selectReachableRuntimeLibraryNames(entries);
|
|
assert.deepEqual(retainedNames, [
|
|
'libavcodec.so.62',
|
|
'libmpv.so',
|
|
'libmpv.so.2',
|
|
]);
|
|
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-runtime-prune-')
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const libDir = path.join(root, 'lib');
|
|
fs.mkdirSync(libDir);
|
|
fs.writeFileSync(path.join(libDir, 'libmpv.so.2.0.0'), 'mpv');
|
|
fs.symlinkSync('libmpv.so.2.0.0', path.join(libDir, 'libmpv.so.2'));
|
|
fs.symlinkSync('libmpv.so.2', path.join(libDir, 'libmpv.so'));
|
|
fs.writeFileSync(path.join(libDir, 'libavcodec.so.62.1.0'), 'codec');
|
|
fs.symlinkSync(
|
|
'libavcodec.so.62.1.0',
|
|
path.join(libDir, 'libavcodec.so.62')
|
|
);
|
|
fs.symlinkSync('libavcodec.so.62', path.join(libDir, 'libavcodec.so'));
|
|
fs.writeFileSync(path.join(libDir, 'libunused.so.1'), 'unused');
|
|
|
|
retainRuntimeLibraries(libDir, retainedNames);
|
|
assert.deepEqual(fs.readdirSync(libDir).sort(), retainedNames);
|
|
for (const retainedName of retainedNames) {
|
|
assert.equal(
|
|
fs.lstatSync(path.join(libDir, retainedName)).isFile(),
|
|
true,
|
|
retainedName
|
|
);
|
|
}
|
|
});
|
|
|
|
test('enforces the Ubuntu 22.04 GLIBC and GLIBCXX symbol ceilings', () => {
|
|
assert.deepEqual(PORTABLE_ABI_BASELINE, {
|
|
distribution: 'Ubuntu 22.04',
|
|
glibcMaximum: '2.35',
|
|
glibcxxMaximum: '3.4.30',
|
|
});
|
|
const record = parseReadelfVersionInfo(
|
|
`
|
|
0x0010: Name: GLIBC_2.2.5 Flags: none Version: 7
|
|
0x0020: Name: GLIBC_2.34 Flags: none Version: 5
|
|
0x0030: Name: GLIBCXX_3.4.21 Flags: none Version: 4
|
|
0x0040: Name: GLIBCXX_3.4.29 Flags: none Version: 3
|
|
`,
|
|
'libmpv.so.2'
|
|
);
|
|
assert.deepEqual(record, {
|
|
name: 'libmpv.so.2',
|
|
requiredGlibc: '2.34',
|
|
requiredGlibcxx: '3.4.29',
|
|
});
|
|
assert.doesNotThrow(() => assertPortableAbiRecords([record]));
|
|
|
|
for (const [field, version] of [
|
|
['requiredGlibc', '2.36'],
|
|
['requiredGlibcxx', '3.4.31'],
|
|
]) {
|
|
assert.throws(
|
|
() =>
|
|
assertPortableAbiRecords([
|
|
{
|
|
...record,
|
|
[field]: version,
|
|
},
|
|
]),
|
|
/portable ABI baseline.*newer symbol/i
|
|
);
|
|
}
|
|
});
|
|
|
|
test('rejects build hosts newer than the portable glibc baseline', () => {
|
|
assert.doesNotThrow(() => assertPortableBuildHostGlibc('2.35'));
|
|
assert.throws(
|
|
() => assertPortableBuildHostGlibc('2.36'),
|
|
/build host glibc 2\.36.*portable ABI baseline.*2\.35/i
|
|
);
|
|
assert.throws(
|
|
() => assertPortableBuildHostGlibc(undefined),
|
|
/unable to determine the Linux build host glibc version/i
|
|
);
|
|
});
|
|
|
|
test('inspects every retained runtime file for portable symbol versions', () => {
|
|
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
|
assert.match(
|
|
builderSource,
|
|
/runCapture\('readelf', \[\s*'--version-info',\s*libraryPath,\s*\]\)/
|
|
);
|
|
assert.match(builderSource, /retainRuntimeLibraries\(libDir,/);
|
|
assert.match(builderSource, /assertPortableAbiRecords\(abiRecords\)/);
|
|
});
|
|
|
|
test('materializes symlink aliases and hashes every runtime library', (t) => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-linux-builder-')
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const libDir = path.join(root, 'lib');
|
|
fs.mkdirSync(libDir);
|
|
const contents = Buffer.from('fake-elf-runtime');
|
|
fs.writeFileSync(path.join(libDir, 'libmpv.so.2.0.0'), contents);
|
|
fs.symlinkSync('libmpv.so.2.0.0', path.join(libDir, 'libmpv.so.2'));
|
|
fs.symlinkSync('libmpv.so.2', path.join(libDir, 'libmpv.so'));
|
|
|
|
materializeLibrarySymlinks(libDir);
|
|
|
|
for (const name of ['libmpv.so', 'libmpv.so.2', 'libmpv.so.2.0.0']) {
|
|
const filePath = path.join(libDir, name);
|
|
assert.equal(fs.lstatSync(filePath).isFile(), true);
|
|
assert.deepEqual(fs.readFileSync(filePath), contents);
|
|
}
|
|
assert.deepEqual(createRuntimeFileRecords(libDir), [
|
|
{
|
|
name: 'libmpv.so',
|
|
size: contents.length,
|
|
sha256: crypto.createHash('sha256').update(contents).digest('hex'),
|
|
},
|
|
{
|
|
name: 'libmpv.so.2',
|
|
size: contents.length,
|
|
sha256: crypto.createHash('sha256').update(contents).digest('hex'),
|
|
},
|
|
{
|
|
name: 'libmpv.so.2.0.0',
|
|
size: contents.length,
|
|
sha256: crypto.createHash('sha256').update(contents).digest('hex'),
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('generates a hash-complete manifest accepted by the Linux validator', (t) => {
|
|
const root = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'iptvnator-linux-manifest-')
|
|
);
|
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
const libDir = path.join(root, 'lib');
|
|
fs.mkdirSync(libDir);
|
|
fs.writeFileSync(path.join(libDir, 'libavcodec.so.62'), 'avcodec');
|
|
fs.writeFileSync(path.join(libDir, 'libmpv.so'), 'mpv');
|
|
fs.writeFileSync(path.join(libDir, 'libmpv.so.2'), 'mpv');
|
|
const runtimeFiles = createRuntimeFileRecords(libDir);
|
|
const sourceRecords = createPinnedSourceRecords();
|
|
const abiRecords = runtimeFiles.map(({ name }) => ({
|
|
name,
|
|
requiredGlibc: '2.34',
|
|
requiredGlibcxx: null,
|
|
}));
|
|
const manifest = createLinuxRuntimeManifest({
|
|
sourceRecords,
|
|
runtimeFiles,
|
|
abiRecords,
|
|
dependencyClosure: {
|
|
entries: [
|
|
{
|
|
name: 'libavcodec.so.62',
|
|
soname: 'libavcodec.so.62',
|
|
needed: ['libm.so.6'],
|
|
rpath: [],
|
|
runpath: ['$ORIGIN'],
|
|
},
|
|
{
|
|
name: 'libmpv.so',
|
|
soname: 'libmpv.so.2',
|
|
needed: ['libavcodec.so.62', 'libEGL.so.1'],
|
|
rpath: [],
|
|
runpath: ['$ORIGIN'],
|
|
},
|
|
{
|
|
name: 'libmpv.so.2',
|
|
soname: 'libmpv.so.2',
|
|
needed: ['libavcodec.so.62', 'libEGL.so.1'],
|
|
rpath: [],
|
|
runpath: ['$ORIGIN'],
|
|
},
|
|
],
|
|
externalDependencies: ['libEGL.so.1', 'libm.so.6'],
|
|
},
|
|
buildHost: {
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
release: 'fixture-kernel',
|
|
glibcVersion: '2.35',
|
|
systemPkgConfigDirs: [...DEFAULT_SYSTEM_PKG_CONFIG_DIRS],
|
|
systemPkgConfigPackages: Object.fromEntries(
|
|
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES.map((packageName) => [
|
|
packageName,
|
|
`${packageName} fixture version`,
|
|
])
|
|
),
|
|
tools: Object.fromEntries(
|
|
REQUIRED_TOOLS.map((tool) => [
|
|
tool,
|
|
`${tool} ${MINIMUM_TOOL_VERSIONS[tool]}`,
|
|
])
|
|
),
|
|
},
|
|
generatedAt: '2026-07-17T00:00:00.000Z',
|
|
});
|
|
|
|
assert.deepEqual(validateLinuxRuntimeManifest(manifest), []);
|
|
assert.equal(
|
|
manifest.runtimeTotalBytes,
|
|
runtimeFiles.reduce((total, runtimeFile) => total + runtimeFile.size, 0)
|
|
);
|
|
assert.deepEqual(manifest.runtimeDependencyClosure.externalDependencies, [
|
|
'libEGL.so.1',
|
|
'libm.so.6',
|
|
]);
|
|
assert.equal(
|
|
manifest.runtimeDependencyClosure.entries.find(
|
|
({ name }) => name === 'libmpv.so'
|
|
).soname,
|
|
'libmpv.so.2'
|
|
);
|
|
assert.deepEqual(
|
|
manifest.externalSystemLibraries,
|
|
EXTERNAL_SYSTEM_LIBRARIES
|
|
);
|
|
assert.deepEqual(manifest.runtimeAbi, {
|
|
baseline: PORTABLE_ABI_BASELINE,
|
|
files: abiRecords,
|
|
});
|
|
assert.deepEqual(
|
|
manifest.runtimeExternalConfiguration,
|
|
RUNTIME_EXTERNAL_CONFIGURATION
|
|
);
|
|
assert.equal(manifest.buildHost.glibcVersion, '2.35');
|
|
assert.deepEqual(
|
|
Object.keys(manifest.buildHost.systemPkgConfigPackages),
|
|
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES
|
|
);
|
|
for (const sourcePackage of SOURCE_PACKAGES) {
|
|
if (sourcePackage.sourceKind === 'archive') {
|
|
assert.equal(
|
|
manifest.packages[sourcePackage.id].sourceSha256,
|
|
sourcePackage.expectedSha256
|
|
);
|
|
}
|
|
}
|
|
assert.equal(
|
|
manifest.packages.libplacebo.sourceGitCommit,
|
|
SOURCE_PACKAGES.find(({ id }) => id === 'libplacebo').expectedGitCommit
|
|
);
|
|
assert.doesNotMatch(manifest.sourceDistribution, /TBD|TODO/i);
|
|
assert.match(manifest.sourceDistribution, /source archives/i);
|
|
assert.match(manifest.sourceDistribution, /libdisplay-info/i);
|
|
assert.match(manifest.sourceDistribution, /hwdata/i);
|
|
assert.match(manifest.sourceDistribution, /pnp\.ids/i);
|
|
assert.deepEqual(manifest.packages.hwdata.buildInput, {
|
|
consumer: 'libdisplay-info',
|
|
relativePath: 'pnp.ids',
|
|
purpose: 'PNP vendor lookup table compiled into libdisplay-info.',
|
|
});
|
|
});
|
|
|
|
test('guards the CLI to Linux x64 and requires exactly one output prefix', () => {
|
|
assert.deepEqual(
|
|
parseCliInvocation({
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
argv: ['/tmp/runtime'],
|
|
cwd: '/workspace',
|
|
}),
|
|
{ prefix: '/tmp/runtime' }
|
|
);
|
|
assert.deepEqual(
|
|
parseCliInvocation({
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
argv: ['--', 'relative/runtime'],
|
|
cwd: '/workspace',
|
|
}),
|
|
{ prefix: '/workspace/relative/runtime' }
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseCliInvocation({
|
|
platform: 'darwin',
|
|
arch: 'x64',
|
|
argv: ['/tmp/runtime'],
|
|
cwd: '/workspace',
|
|
}),
|
|
/supported on Linux x64 only.*darwin\/x64/
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
parseCliInvocation({
|
|
platform: 'linux',
|
|
arch: 'arm64',
|
|
argv: ['/tmp/runtime'],
|
|
cwd: '/workspace',
|
|
}),
|
|
/supported on Linux x64 only.*linux\/arm64/
|
|
);
|
|
for (const argv of [[], ['/one', '/two']]) {
|
|
assert.throws(
|
|
() =>
|
|
parseCliInvocation({
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
argv,
|
|
cwd: '/workspace',
|
|
}),
|
|
/Usage: node tools\/embedded-mpv\/build-linux-runtime\.mjs <output-prefix>/
|
|
);
|
|
}
|
|
});
|
|
|
|
test('does not execute the Linux build when the entrypoint is imported', async () => {
|
|
await assert.doesNotReject(() => import(pathToFileURL(builderScript).href));
|
|
});
|
|
|
|
test('patches every materialized ELF runtime to ORIGIN before validating closure', () => {
|
|
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
|
assert.match(
|
|
builderSource,
|
|
/run\('patchelf', \['--set-rpath', '\$ORIGIN', libraryPath\]\)/
|
|
);
|
|
assert.match(
|
|
builderSource,
|
|
/runCapture\('readelf', \['-d', libraryPath\]\)/
|
|
);
|
|
assert.match(builderSource, /retainRuntimeLibraries\(libDir,/);
|
|
assert.match(builderSource, /validateRuntimeDependencyClosure\(\{/);
|
|
assert.match(builderSource, /createRuntimeFileRecords\(libDir\)/);
|
|
assert.match(builderSource, /runtime-manifest\.json/);
|
|
assert.match(builderSource, /validateLinuxRuntimeManifest\(manifest\)/);
|
|
});
|
|
|
|
test('registers the builder script and focused test with package and Nx', () => {
|
|
const packageMetadata = JSON.parse(
|
|
fs.readFileSync(path.join(workspaceRoot, 'package.json'), 'utf8')
|
|
);
|
|
assert.equal(
|
|
packageMetadata.scripts['embedded-mpv:build-runtime:linux'],
|
|
'node tools/embedded-mpv/build-linux-runtime.mjs'
|
|
);
|
|
|
|
const packagingProject = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(workspaceRoot, 'tools', 'packaging', 'project.json'),
|
|
'utf8'
|
|
)
|
|
);
|
|
const inputs = packagingProject.targets.test.inputs;
|
|
for (const registeredInput of [
|
|
'{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.cjs',
|
|
'{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.mjs',
|
|
'{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.test.mjs',
|
|
]) {
|
|
assert.ok(inputs.includes(registeredInput), registeredInput);
|
|
}
|
|
assert.match(
|
|
packagingProject.targets.test.options.command,
|
|
/tools\/embedded-mpv\/build-linux-runtime\.test\.mjs/
|
|
);
|
|
|
|
const lintInputs = packagingProject.targets.lint.inputs;
|
|
for (const registeredInput of [
|
|
'{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.mjs',
|
|
'{workspaceRoot}/tools/embedded-mpv/build-linux-runtime.test.mjs',
|
|
]) {
|
|
assert.ok(lintInputs.includes(registeredInput), registeredInput);
|
|
}
|
|
assert.match(
|
|
packagingProject.targets.lint.command,
|
|
/tools\/embedded-mpv\/build-linux-runtime\.\{mjs,test\.mjs\}/
|
|
);
|
|
});
|