fix(embedded-mpv): pin Linux runtime inputs

This commit is contained in:
4gray committed 2026-07-17 22:42:36 +02:00
1 parent e51fdc5286
commit 95349ec1cb
5 files changed
+1132 -119

No files matched your search

+114 -12
View File
@@ -12,6 +12,8 @@ const SOURCE_PACKAGES = Object.freeze(
sourceKind: 'archive',
sourceUrl:
'https://download.savannah.gnu.org/releases/freetype/freetype-2.13.3.tar.xz',
expectedSha256:
'0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289',
license: 'FreeType License (FTL)',
},
{
@@ -20,6 +22,8 @@ const SOURCE_PACKAGES = Object.freeze(
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',
},
{
@@ -28,6 +32,8 @@ const SOURCE_PACKAGES = Object.freeze(
sourceKind: 'archive',
sourceUrl:
'https://github.com/harfbuzz/harfbuzz/releases/download/8.5.0/harfbuzz-8.5.0.tar.xz',
expectedSha256:
'77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27',
license: 'MIT',
},
{
@@ -36,6 +42,8 @@ const SOURCE_PACKAGES = Object.freeze(
sourceKind: 'archive',
sourceUrl:
'https://github.com/libexpat/libexpat/releases/download/R_2_8_2/expat-2.8.2.tar.xz',
expectedSha256:
'3ad89b8588e6644bd4e49981480d48b21289eebbcd4f0a1a4afb1c29f99b6ab4',
license: 'MIT',
},
{
@@ -44,6 +52,8 @@ const SOURCE_PACKAGES = Object.freeze(
sourceKind: 'archive',
sourceUrl:
'https://www.freedesktop.org/software/fontconfig/release/fontconfig-2.16.0.tar.xz',
expectedSha256:
'6a33dc555cc9ba8b10caf7695878ef134eeb36d0af366041f639b1da9b6ed220',
license: 'MIT',
},
{
@@ -52,6 +62,8 @@ const SOURCE_PACKAGES = Object.freeze(
sourceKind: 'archive',
sourceUrl:
'https://github.com/libass/libass/releases/download/0.17.3/libass-0.17.3.tar.xz',
expectedSha256:
'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959',
license: 'ISC',
},
{
@@ -60,6 +72,8 @@ const SOURCE_PACKAGES = Object.freeze(
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',
},
{
@@ -67,6 +81,8 @@ const SOURCE_PACKAGES = Object.freeze(
version: '8.1',
sourceKind: 'archive',
sourceUrl: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
expectedSha256:
'b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a',
license: 'LGPL-2.1-or-later',
},
{
@@ -75,6 +91,7 @@ const SOURCE_PACKAGES = Object.freeze(
sourceKind: 'git',
sourceUrl: 'https://github.com/haasn/libplacebo.git',
sourceTag: 'v7.360.1',
expectedGitCommit: 'cee9b076f2c63104ccfd497fa79c39a867293ec4',
license: 'LGPL-2.1-or-later',
},
{
@@ -83,6 +100,8 @@ const SOURCE_PACKAGES = Object.freeze(
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))
@@ -375,6 +394,23 @@ const REQUIRED_TOOLS = Object.freeze([
'tar',
]);
const DEFAULT_SYSTEM_PKG_CONFIG_DIRS = Object.freeze([
'/usr/lib/x86_64-linux-gnu/pkgconfig',
'/usr/lib64/pkgconfig',
'/usr/lib/pkgconfig',
'/usr/share/pkgconfig',
]);
const EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES = Object.freeze([
'alsa',
'egl',
'gbm',
'gl',
'libpulse',
'libva',
'libva-drm',
]);
const GLIBC_TOOLCHAIN_ALLOWLIST = Object.freeze([
'ld-linux-x86-64.so.2',
'libc.so.6',
@@ -451,10 +487,50 @@ const allowedExternalLibraryNames = new Set([
...externalSystemLibraryNames,
]);
function assertArchiveMatchesPin(sourcePackage, actualSha256) {
if (actualSha256 !== sourcePackage.expectedSha256) {
throw new Error(
`${sourcePackage.id} archive SHA-256 mismatch: expected ${sourcePackage.expectedSha256}, received ${actualSha256}.`
);
}
}
function assertGitCommitMatchesPin(sourcePackage, actualGitCommit) {
if (actualGitCommit !== sourcePackage.expectedGitCommit) {
throw new Error(
`${sourcePackage.id} git commit mismatch: expected ${sourcePackage.expectedGitCommit}, received ${actualGitCommit}.`
);
}
}
function joinEnvironmentParts(parts, separator = ' ') {
return parts.filter((value) => value && value.trim()).join(separator);
}
function resolveSystemPkgConfigDirs(environment = {}) {
const explicitDirectories =
environment.IPTVNATOR_EMBEDDED_MPV_SYSTEM_PKG_CONFIG_DIRS;
if (!explicitDirectories) {
return [...DEFAULT_SYSTEM_PKG_CONFIG_DIRS];
}
const directories = explicitDirectories
.split(path.delimiter)
.map((directory) => directory.trim())
.filter(Boolean);
if (
directories.length === 0 ||
directories.some((directory) => !path.isAbsolute(directory))
) {
throw new Error(
'IPTVNATOR_EMBEDDED_MPV_SYSTEM_PKG_CONFIG_DIRS must contain only absolute paths.'
);
}
return [
...new Set(directories.map((directory) => path.normalize(directory))),
];
}
function createBuildEnvironment({
prefix,
baseEnv = process.env,
@@ -471,9 +547,31 @@ function createBuildEnvironment({
]),
];
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 {
...baseEnv,
...inheritedEnvironment,
PATH: joinEnvironmentParts(
[path.join(prefix, 'bin'), baseEnv.PATH],
path.delimiter
@@ -481,29 +579,20 @@ function createBuildEnvironment({
PKG_CONFIG_PATH: prefixPkgConfigDirs.join(path.delimiter),
PKG_CONFIG_LIBDIR: pkgConfigLibDirs.join(path.delimiter),
CMAKE_PREFIX_PATH: prefix,
CPPFLAGS: joinEnvironmentParts([
`-I${path.join(prefix, 'include')}`,
baseEnv.CPPFLAGS,
]),
CPPFLAGS: `-I${path.join(prefix, 'include')}`,
CFLAGS: joinEnvironmentParts([
'-fPIC',
`-I${path.join(prefix, 'include')}`,
baseEnv.CFLAGS,
]),
CXXFLAGS: joinEnvironmentParts([
'-fPIC',
`-I${path.join(prefix, 'include')}`,
baseEnv.CXXFLAGS,
]),
LDFLAGS: joinEnvironmentParts([
`-L${prefixLibDir}`,
`-Wl,-rpath-link,${prefixLibDir}`,
baseEnv.LDFLAGS,
]),
LD_LIBRARY_PATH: joinEnvironmentParts(
[prefixLibDir, baseEnv.LD_LIBRARY_PATH],
path.delimiter
),
LD_LIBRARY_PATH: prefixLibDir,
FONTCONFIG_PATH: path.join(prefix, 'etc', 'fonts'),
OPENSSL_MODULES: path.join(prefixLibDir, 'ossl-modules'),
};
@@ -768,6 +857,14 @@ function createLinuxRuntimeManifest({
if (!sourceRecord) {
throw new Error(`Missing source metadata for ${sourcePackage.id}.`);
}
if (sourcePackage.sourceKind === 'archive') {
assertArchiveMatchesPin(sourcePackage, sourceRecord.sourceSha256);
} else {
assertGitCommitMatchesPin(
sourcePackage,
sourceRecord.sourceGitCommit
);
}
packages[sourcePackage.id] = sourceManifestMetadata(sourceRecord);
}
@@ -817,18 +914,23 @@ function createLinuxRuntimeManifest({
module.exports = {
BUILD_RECIPES,
BUILD_ORDER,
DEFAULT_SYSTEM_PKG_CONFIG_DIRS,
EXTERNAL_SYSTEM_LIBRARIES,
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
FFMPEG_CONFIGURE_FLAGS,
GLIBC_TOOLCHAIN_ALLOWLIST,
MPV_MESON_FLAGS,
REQUIRED_TOOLS,
SOURCE_PACKAGES,
assertArchiveMatchesPin,
assertGitCommitMatchesPin,
createBuildEnvironment,
createLinuxRuntimeManifest,
createRuntimeFileRecords,
materializeLibrarySymlinks,
parseCliInvocation,
parseReadelfDynamic,
resolveSystemPkgConfigDirs,
runtimeLibraryNames,
sha256Buffer,
validateRuntimeDependencyClosure,
+26 -44
View File
@@ -11,15 +11,19 @@ const require = createRequire(import.meta.url);
const {
BUILD_RECIPES,
BUILD_ORDER,
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
MPV_MESON_FLAGS,
REQUIRED_TOOLS,
SOURCE_PACKAGES,
assertArchiveMatchesPin,
assertGitCommitMatchesPin,
createBuildEnvironment,
createLinuxRuntimeManifest,
createRuntimeFileRecords,
materializeLibrarySymlinks,
parseCliInvocation,
parseReadelfDynamic,
resolveSystemPkgConfigDirs,
runtimeLibraryNames,
sha256Buffer,
validateRuntimeDependencyClosure,
@@ -166,46 +170,6 @@ function assertSafeOutputPrefix(prefix, buildRoot) {
}
}
function resolveSystemPkgConfigDirs(environment) {
const explicitDirectories =
environment.IPTVNATOR_EMBEDDED_MPV_SYSTEM_PKG_CONFIG_DIRS;
if (explicitDirectories) {
return [
...new Set(
explicitDirectories
.split(path.delimiter)
.map((directory) => directory.trim())
.filter(Boolean)
.map((directory) => path.resolve(directory))
),
];
}
const result = spawnSync(
'pkg-config',
['--variable', 'pc_path', 'pkg-config'],
{
env: environment,
encoding: 'utf8',
stdio: 'pipe',
}
);
if (result.status !== 0 || !result.stdout.trim()) {
throw new Error(
'Unable to discover system pkg-config directories. Set IPTVNATOR_EMBEDDED_MPV_SYSTEM_PKG_CONFIG_DIRS explicitly.'
);
}
return [
...new Set(
result.stdout
.trim()
.split(path.delimiter)
.filter(Boolean)
.map((directory) => path.resolve(directory))
),
];
}
function archiveExtension(sourceUrl) {
for (const extension of ['.tar.xz', '.tar.gz', '.tar.bz2', '.tgz']) {
if (new URL(sourceUrl).pathname.endsWith(extension)) {
@@ -255,6 +219,8 @@ function downloadArchive(sourcePackage, context) {
fs.renameSync(temporaryArchivePath, archivePath);
}
const sourceSha256 = sha256File(archivePath);
assertArchiveMatchesPin(sourcePackage, sourceSha256);
const packageSourcePath = sourcePathFor(
sourcePackage.id,
context.sourceRoot
@@ -274,7 +240,7 @@ function downloadArchive(sourcePackage, context) {
return {
...sourcePackage,
sourceSha256: sha256File(archivePath),
sourceSha256,
};
}
@@ -293,15 +259,16 @@ function cloneGitSource(sourcePackage, context) {
sourcePackage.sourceUrl,
packageSourcePath,
]);
const sourceGitCommit = context.runCapture('git', ['rev-parse', 'HEAD'], {
cwd: packageSourcePath,
});
assertGitCommitMatchesPin(sourcePackage, sourceGitCommit);
context.run(
'git',
['submodule', 'update', '--init', '--recursive', '--depth', '1'],
{ cwd: packageSourcePath }
);
const sourceGitCommit = context.runCapture('git', ['rev-parse', 'HEAD'], {
cwd: packageSourcePath,
});
const submoduleOutput = context.runCapture(
'git',
['submodule', 'status', '--recursive'],
@@ -569,16 +536,30 @@ function collectBuildHost(context) {
context.runCapture(tool, versionArgs[tool] ?? ['--version'])
);
}
const systemPkgConfigPackages = {};
for (const packageName of EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES) {
systemPkgConfigPackages[packageName] = context.runCapture(
'pkg-config',
['--modversion', packageName]
);
}
return {
platform: process.platform,
arch: process.arch,
release: os.release(),
systemPkgConfigDirs: [...context.systemPkgConfigDirs],
systemPkgConfigPackages,
tools,
};
}
function verifySystemPkgConfigPackages(context) {
for (const packageName of EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES) {
context.run('pkg-config', ['--exists', packageName]);
}
}
function writeManifest(context, sourceRecords, runtimeMetadata) {
const mpvMesonFlags = mesonSetupArgs(context.prefix, MPV_MESON_FLAGS);
const manifest = createLinuxRuntimeManifest({
@@ -652,6 +633,7 @@ export function main({
fs.mkdirSync(context.buildRoot, { recursive: true });
fs.rmSync(context.prefix, { recursive: true, force: true });
fs.mkdirSync(context.prefix, { recursive: true });
verifySystemPkgConfigPackages(context);
const sourceRecords = acquireSources(context);
buildRuntime(context);
removeNonRuntimeBuildOutputs(prefix);
+245 -35
View File
@@ -15,24 +15,50 @@ 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,
MPV_MESON_FLAGS,
REQUIRED_TOOLS,
SOURCE_PACKAGES,
assertArchiveMatchesPin,
assertGitCommitMatchesPin,
createBuildEnvironment,
createLinuxRuntimeManifest,
createRuntimeFileRecords,
materializeLibrarySymlinks,
parseCliInvocation,
parseReadelfDynamic,
resolveSystemPkgConfigDirs,
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: [
`${'a'.repeat(40)} 3rdparty/example`,
],
}
: {
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);
@@ -90,6 +116,109 @@ test('pins the complete source stack and preserves dependency build order', () =
);
});
test('hardcodes the verified official archive digests and libplacebo commit', () => {
const expectedArchivePins = {
expat: '3ad89b8588e6644bd4e49981480d48b21289eebbcd4f0a1a4afb1c29f99b6ab4',
ffmpeg: 'b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a',
fontconfig:
'6a33dc555cc9ba8b10caf7695878ef134eeb36d0af366041f639b1da9b6ed220',
freetype:
'0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289',
fribidi:
'1b1cde5b235d40479e91be2f0e88a309e3214c8ab470ec8a2744d82a5a9ea05c',
harfbuzz:
'77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27',
libass: 'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959',
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'));
@@ -137,13 +266,27 @@ test('defines shared-only source recipes with font discovery before playback', (
}
});
test('constructs a prefix-first build environment with only declared system interfaces', () => {
test('constructs a prefix-only build environment and ignores hostile host flags', () => {
const environment = createBuildEnvironment({
prefix: '/opt/runtime',
baseEnv: {
PATH: '/usr/bin',
CFLAGS: '-O2',
LDFLAGS: '-Wl,--as-needed',
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',
@@ -165,12 +308,79 @@ test('constructs a prefix-first build environment with only declared system inte
'/usr/share/pkgconfig',
].join(':')
);
assert.match(environment.CPPFLAGS, /-I\/opt\/runtime\/include/);
assert.match(environment.CFLAGS, /-fPIC/);
assert.match(environment.CFLAGS, /-O2/);
assert.match(environment.LDFLAGS, /-L\/opt\/runtime\/lib/);
assert.match(environment.LDFLAGS, /-Wl,--as-needed/);
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('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',
'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', () => {
@@ -534,26 +744,7 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) =
fs.writeFileSync(path.join(libDir, 'libavcodec.so.62'), 'avcodec');
fs.writeFileSync(path.join(libDir, 'libmpv.so.2'), 'mpv');
const runtimeFiles = createRuntimeFileRecords(libDir);
const sourceRecords = Object.fromEntries(
SOURCE_PACKAGES.map((sourcePackage, index) => [
sourcePackage.id,
{
...sourcePackage,
...(sourcePackage.sourceKind === 'git'
? {
sourceGitCommit: String(index + 1)
.repeat(40)
.slice(0, 40),
sourceSubmodules: [
`${String(index + 2)
.repeat(40)
.slice(0, 40)} 3rdparty/example`,
],
}
: { sourceSha256: String(index).repeat(64).slice(0, 64) }),
},
])
);
const sourceRecords = createPinnedSourceRecords();
const manifest = createLinuxRuntimeManifest({
sourceRecords,
runtimeFiles,
@@ -578,13 +769,16 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) =
platform: 'linux',
arch: 'x64',
release: 'fixture-kernel',
tools: {
cc: 'cc fixture',
meson: '1.7.0',
ninja: '1.12.1',
patchelf: '0.18.0',
readelf: 'GNU readelf fixture',
},
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} fixture version`])
),
},
generatedAt: '2026-07-17T00:00:00.000Z',
});
@@ -602,6 +796,22 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) =
manifest.externalSystemLibraries,
EXTERNAL_SYSTEM_LIBRARIES
);
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);
});
+403 -1
View File
@@ -1,6 +1,13 @@
'use strict';
const path = require('node:path');
const {
EXTERNAL_SYSTEM_LIBRARIES,
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
GLIBC_TOOLCHAIN_ALLOWLIST,
REQUIRED_TOOLS,
SOURCE_PACKAGES,
} = require('./build-linux-runtime.cjs');
const LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION = 1;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
@@ -8,7 +15,16 @@ const GIT_COMMIT_PATTERN = /^[a-f0-9]{40,64}$/;
const SAFE_BASENAME_PATTERN = /^[A-Za-z0-9_+.-]+$/;
const SHARED_LIBRARY_PATTERN = /\.so(?:\.\d+)*$/;
const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
const REQUIRED_SOURCE_PACKAGES = ['ffmpeg', 'mpv'];
const REQUIRED_SOURCE_PACKAGES = SOURCE_PACKAGES.map(({ id }) => id);
const SOURCE_PACKAGE_BY_ID = new Map(
SOURCE_PACKAGES.map((sourcePackage) => [sourcePackage.id, sourcePackage])
);
const ALLOWED_EXTERNAL_LIBRARY_NAMES = new Set([
...GLIBC_TOOLCHAIN_ALLOWLIST,
...EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
]);
const SUBMODULE_RECORD_PATTERN =
/^[a-f0-9]{40,64}\s+([A-Za-z0-9_+./-]+)(?:\s+\(.+\))?$/;
const LINUX_SYSTEM_BACKEND = 'process-isolated mpv --wid';
function isObject(value) {
@@ -31,6 +47,23 @@ function isSafeBasename(value) {
);
}
function isValidSubmoduleRecord(value) {
if (typeof value !== 'string') {
return false;
}
const match = value.match(SUBMODULE_RECORD_PATTERN);
if (!match) {
return false;
}
const submodulePath = match[1];
return (
!path.posix.isAbsolute(submodulePath) &&
!submodulePath
.split('/')
.some((segment) => segment === '.' || segment === '..')
);
}
function validateSourceMetadata(errors, value, label) {
if (!isNonEmptyString(value.version)) {
errors.push(
@@ -98,6 +131,14 @@ function validatePackages(errors, packages) {
}
}
for (const packageName of Object.keys(packages).sort()) {
if (!SOURCE_PACKAGE_BY_ID.has(packageName)) {
errors.push(
`Linux runtime manifest packages contains unexpected source package "${packageName}".`
);
}
}
for (const packageName of Object.keys(packages).sort()) {
const packageMetadata = packages[packageName];
const label = `packages.${packageName}`;
@@ -116,6 +157,85 @@ function validatePackages(errors, packages) {
`Linux runtime manifest ${label}.license must be a non-empty string.`
);
}
const pinnedPackage = SOURCE_PACKAGE_BY_ID.get(packageName);
if (!pinnedPackage) {
continue;
}
for (const field of ['version', 'sourceUrl', 'license']) {
if (
isNonEmptyString(packageMetadata[field]) &&
packageMetadata[field] !== pinnedPackage[field]
) {
errors.push(
`Linux runtime manifest ${label}.${field} must equal the pinned value.`
);
}
}
if (pinnedPackage.sourceTag) {
if (packageMetadata.sourceTag !== pinnedPackage.sourceTag) {
errors.push(
`Linux runtime manifest ${label}.sourceTag must equal the pinned tag.`
);
}
} else if (packageMetadata.sourceTag !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include sourceTag.`
);
}
if (pinnedPackage.sourceKind === 'archive') {
if (
typeof packageMetadata.sourceSha256 === 'string' &&
SHA256_PATTERN.test(packageMetadata.sourceSha256) &&
packageMetadata.sourceSha256 !== pinnedPackage.expectedSha256
) {
errors.push(
`Linux runtime manifest ${label}.sourceSha256 must equal the pinned digest.`
);
}
if (packageMetadata.sourceGitCommit !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include sourceGitCommit.`
);
}
if (packageMetadata.sourceSubmodules !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include sourceSubmodules.`
);
}
continue;
}
if (
typeof packageMetadata.sourceGitCommit === 'string' &&
GIT_COMMIT_PATTERN.test(packageMetadata.sourceGitCommit) &&
packageMetadata.sourceGitCommit !== pinnedPackage.expectedGitCommit
) {
errors.push(
`Linux runtime manifest ${label}.sourceGitCommit must equal the pinned commit.`
);
}
if (packageMetadata.sourceSha256 !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include sourceSha256.`
);
}
if (
!Array.isArray(packageMetadata.sourceSubmodules) ||
packageMetadata.sourceSubmodules.length === 0 ||
packageMetadata.sourceSubmodules.some(
(record) => !isValidSubmoduleRecord(record)
) ||
new Set(packageMetadata.sourceSubmodules).size !==
packageMetadata.sourceSubmodules.length
) {
errors.push(
`Linux runtime manifest ${label}.sourceSubmodules must be a non-empty array of commit-and-path records.`
);
}
}
}
@@ -273,6 +393,276 @@ function validateRuntimeFiles(errors, runtimeFiles) {
}
}
function validateRuntimeTotalBytes(errors, runtimeFiles, runtimeTotalBytes) {
const expectedTotal = Array.isArray(runtimeFiles)
? runtimeFiles.reduce(
(total, runtimeFile) =>
total +
(isObject(runtimeFile) &&
Number.isInteger(runtimeFile.size) &&
runtimeFile.size > 0
? runtimeFile.size
: 0),
0
)
: 0;
if (
!Number.isInteger(runtimeTotalBytes) ||
runtimeTotalBytes !== expectedTotal
) {
errors.push(
`Linux runtime manifest runtimeTotalBytes must equal the sum of runtimeFiles sizes (${expectedTotal}).`
);
}
}
function validateRuntimeDependencyClosure(
errors,
runtimeDependencyClosure,
runtimeFiles
) {
if (!isObject(runtimeDependencyClosure)) {
errors.push(
'Linux runtime manifest runtimeDependencyClosure must be an object.'
);
return;
}
const runtimeNames = Array.isArray(runtimeFiles)
? runtimeFiles
.filter((runtimeFile) => isObject(runtimeFile))
.map(({ name }) => name)
.filter((name) => typeof name === 'string')
: [];
const runtimeNameSet = new Set(runtimeNames);
const { entries, externalDependencies } = runtimeDependencyClosure;
if (!Array.isArray(entries)) {
errors.push(
'Linux runtime manifest runtimeDependencyClosure.entries must contain one record for every runtime file.'
);
return;
}
const entryNames = new Set();
const computedExternalDependencies = new Set();
for (const [index, entry] of entries.entries()) {
const label = `runtimeDependencyClosure.entries[${index}]`;
if (!isObject(entry)) {
errors.push(`Linux runtime manifest ${label} must be an object.`);
continue;
}
if (
!isSafeBasename(entry.name) ||
!SHARED_LIBRARY_PATTERN.test(entry.name)
) {
errors.push(
`Linux runtime manifest ${label}.name must be a safe shared-library basename.`
);
}
if (entryNames.has(entry.name)) {
errors.push(
`Linux runtime manifest runtimeDependencyClosure.entries contains duplicate name "${String(
entry.name
)}".`
);
} else if (typeof entry.name === 'string') {
entryNames.add(entry.name);
}
if (
!Array.isArray(entry.needed) ||
entry.needed.some(
(dependencyName) =>
!isSafeBasename(dependencyName) ||
!SHARED_LIBRARY_PATTERN.test(dependencyName)
)
) {
errors.push(
`Linux runtime manifest ${label}.needed must be an array of safe shared-library names.`
);
} else {
const neededNames = new Set();
for (const dependencyName of entry.needed) {
if (neededNames.has(dependencyName)) {
errors.push(
`Linux runtime manifest ${label}.needed contains duplicate name "${dependencyName}".`
);
continue;
}
neededNames.add(dependencyName);
if (runtimeNameSet.has(dependencyName)) {
continue;
}
if (!ALLOWED_EXTERNAL_LIBRARY_NAMES.has(dependencyName)) {
errors.push(
`Linux runtime manifest dependency ${dependencyName} is not in the deterministic system-library allowlist.`
);
continue;
}
computedExternalDependencies.add(dependencyName);
}
}
if (!Array.isArray(entry.rpath) || entry.rpath.length !== 0) {
errors.push(
`Linux runtime manifest ${label}.rpath must be an empty array.`
);
}
if (
!Array.isArray(entry.runpath) ||
entry.runpath.length !== 1 ||
entry.runpath[0] !== '$ORIGIN'
) {
errors.push(
`Linux runtime manifest ${label}.runpath must contain only "$ORIGIN".`
);
}
}
if (
entries.length !== runtimeNames.length ||
runtimeNames.some((name) => !entryNames.has(name)) ||
[...entryNames].some((name) => !runtimeNameSet.has(name))
) {
errors.push(
'Linux runtime manifest runtimeDependencyClosure.entries must contain one record for every runtime file.'
);
}
const expectedExternalDependencies = [
...computedExternalDependencies,
].sort();
if (
!Array.isArray(externalDependencies) ||
externalDependencies.some(
(dependencyName) =>
!isSafeBasename(dependencyName) ||
!SHARED_LIBRARY_PATTERN.test(dependencyName)
) ||
JSON.stringify(externalDependencies) !==
JSON.stringify(expectedExternalDependencies)
) {
errors.push(
'Linux runtime manifest runtimeDependencyClosure.externalDependencies must exactly match the sorted allowlisted external dependency set.'
);
}
}
function validateExternalSystemLibraries(errors, externalSystemLibraries) {
const expectedKeys = ['interface', 'name', 'reason'];
const matchesDeterministicAllowlist =
Array.isArray(externalSystemLibraries) &&
externalSystemLibraries.length === EXTERNAL_SYSTEM_LIBRARIES.length &&
externalSystemLibraries.every((externalLibrary, index) => {
if (!isObject(externalLibrary)) {
return false;
}
const actualKeys = Object.keys(externalLibrary).sort();
if (
actualKeys.length !== expectedKeys.length ||
actualKeys.some(
(key, keyIndex) => key !== expectedKeys[keyIndex]
)
) {
return false;
}
const expectedLibrary = EXTERNAL_SYSTEM_LIBRARIES[index];
return expectedKeys.every(
(key) => externalLibrary[key] === expectedLibrary[key]
);
});
if (!matchesDeterministicAllowlist) {
errors.push(
'Linux runtime manifest externalSystemLibraries must exactly match the deterministic allowlist.'
);
}
}
function validateBuildHost(errors, buildHost) {
if (!isObject(buildHost)) {
errors.push('Linux runtime manifest buildHost must be an object.');
return;
}
if (buildHost.platform !== 'linux') {
errors.push(
'Linux runtime manifest buildHost.platform must be "linux".'
);
}
if (buildHost.arch !== 'x64') {
errors.push('Linux runtime manifest buildHost.arch must be "x64".');
}
if (!isNonEmptyString(buildHost.release)) {
errors.push(
'Linux runtime manifest buildHost.release must be a non-empty string.'
);
}
if (
!Array.isArray(buildHost.systemPkgConfigDirs) ||
buildHost.systemPkgConfigDirs.length === 0 ||
buildHost.systemPkgConfigDirs.some(
(directory) =>
!isNonEmptyString(directory) || !path.isAbsolute(directory)
) ||
new Set(buildHost.systemPkgConfigDirs).size !==
buildHost.systemPkgConfigDirs.length
) {
errors.push(
'Linux runtime manifest buildHost.systemPkgConfigDirs must be a non-empty array of unique absolute paths.'
);
}
if (!isObject(buildHost.systemPkgConfigPackages)) {
errors.push(
'Linux runtime manifest buildHost.systemPkgConfigPackages must be an object.'
);
} else {
for (const packageName of EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES) {
if (
!isNonEmptyString(
buildHost.systemPkgConfigPackages[packageName]
)
) {
errors.push(
`Linux runtime manifest buildHost.systemPkgConfigPackages.${packageName} must be a non-empty version string.`
);
}
}
for (const packageName of Object.keys(
buildHost.systemPkgConfigPackages
).sort()) {
if (!EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES.includes(packageName)) {
errors.push(
`Linux runtime manifest buildHost.systemPkgConfigPackages contains unexpected package "${packageName}".`
);
}
}
}
if (!isObject(buildHost.tools)) {
errors.push(
'Linux runtime manifest buildHost.tools must be an object.'
);
return;
}
for (const tool of REQUIRED_TOOLS) {
if (!isNonEmptyString(buildHost.tools[tool])) {
errors.push(
`Linux runtime manifest buildHost.tools.${tool} must be a non-empty version string.`
);
}
}
for (const tool of Object.keys(buildHost.tools).sort()) {
if (!REQUIRED_TOOLS.includes(tool)) {
errors.push(
`Linux runtime manifest buildHost.tools contains unexpected tool "${tool}".`
);
}
}
}
function validateLinuxRuntimeManifest(manifest) {
if (!isObject(manifest)) {
return ['Linux runtime manifest must be an object.'];
@@ -307,6 +697,18 @@ function validateLinuxRuntimeManifest(manifest) {
}
validateRuntimeFiles(errors, manifest.runtimeFiles);
validateRuntimeTotalBytes(
errors,
manifest.runtimeFiles,
manifest.runtimeTotalBytes
);
validateRuntimeDependencyClosure(
errors,
manifest.runtimeDependencyClosure,
manifest.runtimeFiles
);
validateExternalSystemLibraries(errors, manifest.externalSystemLibraries);
validateBuildHost(errors, manifest.buildHost);
return errors;
}
@@ -14,6 +14,14 @@ const {
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
} = require('./linux-runtime-manifest.cjs');
const {
DEFAULT_SYSTEM_PKG_CONFIG_DIRS,
EXTERNAL_SYSTEM_LIBRARIES,
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
GLIBC_TOOLCHAIN_ALLOWLIST,
REQUIRED_TOOLS,
SOURCE_PACKAGES,
} = require('./build-linux-runtime.cjs');
const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url));
const stageRuntimeScript = path.join(
@@ -36,36 +44,75 @@ function runtimeFile(name, contents) {
};
}
function sourcePackageRecord(sourcePackage) {
return {
version: sourcePackage.version,
sourceUrl: sourcePackage.sourceUrl,
...(sourcePackage.sourceTag
? { sourceTag: sourcePackage.sourceTag }
: {}),
...(sourcePackage.sourceKind === 'archive'
? { sourceSha256: sourcePackage.expectedSha256 }
: {
sourceGitCommit: sourcePackage.expectedGitCommit,
sourceSubmodules: [`${'a'.repeat(40)} 3rdparty/example`],
}),
license: sourcePackage.license,
};
}
function createValidManifest(
runtimeFiles = [
runtimeFile('libmpv.so.2', 'libmpv-runtime'),
runtimeFile('libavcodec.so.61', 'libavcodec-runtime'),
]
) {
const packages = Object.fromEntries(
SOURCE_PACKAGES.map((sourcePackage) => [
sourcePackage.id,
sourcePackageRecord(sourcePackage),
])
);
const runtimeNames = new Set(
runtimeFiles
.map((runtimeEntry) => runtimeEntry?.name)
.filter((name) => typeof name === 'string')
);
const closureEntries = runtimeFiles
.filter(
(runtimeEntry) =>
runtimeEntry && typeof runtimeEntry.name === 'string'
)
.map((runtimeEntry) => ({
name: runtimeEntry.name,
needed: runtimeEntry.name.startsWith('libmpv.so')
? [
...(runtimeNames.has('libavcodec.so.61')
? ['libavcodec.so.61']
: []),
'libEGL.so.1',
'libc.so.6',
]
: ['libm.so.6'],
rpath: [],
runpath: ['$ORIGIN'],
}));
const externalDependencies = [
...new Set(
closureEntries
.flatMap(({ needed }) => needed)
.filter((neededName) => !runtimeNames.has(neededName))
),
].sort();
return {
schemaVersion: 1,
origin: 'vendored-lgpl-source-build',
platform: 'linux',
arch: 'x64',
packages: {
ffmpeg: {
version: '8.1',
sourceUrl: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
sourceSha256: 'a'.repeat(64),
license: 'LGPL-2.1-or-later',
},
mpv: {
version: '0.41.0',
sourceUrl:
'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz',
sourceSha256: 'b'.repeat(64),
license: 'LGPL-2.1-or-later',
},
},
packages,
ffmpeg: {
version: '8.1',
sourceUrl: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
sourceSha256: 'a'.repeat(64),
...packages.ffmpeg,
configureFlags: [
'--enable-shared',
'--disable-gpl',
@@ -73,15 +120,38 @@ function createValidManifest(
],
},
mpv: {
version: '0.41.0',
sourceUrl:
'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz',
sourceSha256: 'b'.repeat(64),
...packages.mpv,
mesonFlags: ['-Dlibmpv=true', '-Dgpl=false'],
},
sourceDistribution:
'https://downloads.example.test/iptvnator/linux-runtime-sources.tar.zst',
runtimeFiles,
runtimeTotalBytes: runtimeFiles.reduce(
(total, runtimeEntry) => total + (runtimeEntry?.size ?? 0),
0
),
runtimeDependencyClosure: {
entries: closureEntries,
externalDependencies,
},
externalSystemLibraries: EXTERNAL_SYSTEM_LIBRARIES.map(
(externalLibrary) => ({ ...externalLibrary })
),
buildHost: {
platform: 'linux',
arch: 'x64',
release: 'fixture-kernel',
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} fixture version`])
),
},
};
}
@@ -180,6 +250,211 @@ test('accepts a complete LGPL Linux x64 source-build manifest', () => {
assert.deepEqual(validateLinuxRuntimeManifest(createValidManifest()), []);
});
test('requires the exact pinned source package set and provenance', () => {
const expectedPackageNames = SOURCE_PACKAGES.map(({ id }) => id).sort();
assert.deepEqual(
Object.keys(createValidManifest().packages).sort(),
expectedPackageNames
);
for (const packageName of expectedPackageNames) {
const manifest = createValidManifest();
delete manifest.packages[packageName];
assert.match(
validateLinuxRuntimeManifest(manifest).join('\n'),
new RegExp(`packages\\.${packageName} must be an object`)
);
}
const unexpectedPackage = createValidManifest();
unexpectedPackage.packages.unpinned = {
version: '1.0.0',
sourceUrl: 'https://example.test/unpinned.tar.xz',
sourceSha256: 'f'.repeat(64),
license: 'MIT',
};
assert.match(
validateLinuxRuntimeManifest(unexpectedPackage).join('\n'),
/packages contains unexpected source package "unpinned"/
);
});
test('requires pinned archive hashes and exact libplacebo git provenance', () => {
const archiveMismatch = createValidManifest();
archiveMismatch.packages.freetype.sourceSha256 = '0'.repeat(64);
assert.match(
validateLinuxRuntimeManifest(archiveMismatch).join('\n'),
/packages\.freetype\.sourceSha256 must equal the pinned digest/
);
const commitMismatch = createValidManifest();
commitMismatch.packages.libplacebo.sourceGitCommit = '0'.repeat(40);
assert.match(
validateLinuxRuntimeManifest(commitMismatch).join('\n'),
/packages\.libplacebo\.sourceGitCommit must equal the pinned commit/
);
const validSubmoduleRecord = `${'a'.repeat(40)} 3rdparty/example`;
for (const sourceSubmodules of [
[],
['not-a-submodule-record'],
[`${'a'.repeat(40)} ../escape`],
[validSubmoduleRecord, validSubmoduleRecord],
]) {
const invalidSubmodules = createValidManifest();
invalidSubmodules.packages.libplacebo.sourceSubmodules =
sourceSubmodules;
assert.match(
validateLinuxRuntimeManifest(invalidSubmodules).join('\n'),
/packages\.libplacebo\.sourceSubmodules/
);
}
});
test('requires runtimeTotalBytes to equal the declared runtime file sizes', () => {
const missingTotal = createValidManifest();
delete missingTotal.runtimeTotalBytes;
assert.match(
validateLinuxRuntimeManifest(missingTotal).join('\n'),
/runtimeTotalBytes must equal the sum/
);
const wrongTotal = createValidManifest();
wrongTotal.runtimeTotalBytes += 1;
assert.match(
validateLinuxRuntimeManifest(wrongTotal).join('\n'),
/runtimeTotalBytes must equal the sum/
);
});
test('requires a complete ORIGIN-only runtime dependency closure', () => {
const missingEntry = createValidManifest();
missingEntry.runtimeDependencyClosure.entries.pop();
assert.match(
validateLinuxRuntimeManifest(missingEntry).join('\n'),
/runtimeDependencyClosure\.entries must contain one record for every runtime file/
);
const duplicateEntry = createValidManifest();
duplicateEntry.runtimeDependencyClosure.entries[1].name =
duplicateEntry.runtimeDependencyClosure.entries[0].name;
assert.match(
validateLinuxRuntimeManifest(duplicateEntry).join('\n'),
/runtimeDependencyClosure\.entries contains duplicate name/
);
const invalidNeeded = createValidManifest();
invalidNeeded.runtimeDependencyClosure.entries[0].needed = 'libc.so.6';
assert.match(
validateLinuxRuntimeManifest(invalidNeeded).join('\n'),
/needed must be an array of safe shared-library names/
);
const absoluteRpath = createValidManifest();
absoluteRpath.runtimeDependencyClosure.entries[0].rpath = ['/tmp/lib'];
assert.match(
validateLinuxRuntimeManifest(absoluteRpath).join('\n'),
/rpath must be an empty array/
);
const wrongRunpath = createValidManifest();
wrongRunpath.runtimeDependencyClosure.entries[0].runpath = ['/tmp/lib'];
assert.match(
validateLinuxRuntimeManifest(wrongRunpath).join('\n'),
/runpath must contain only "\$ORIGIN"/
);
const unknownDependency = createValidManifest();
unknownDependency.runtimeDependencyClosure.entries[0].needed.push(
'libsurprise.so.1'
);
unknownDependency.runtimeDependencyClosure.externalDependencies.push(
'libsurprise.so.1'
);
assert.match(
validateLinuxRuntimeManifest(unknownDependency).join('\n'),
/libsurprise\.so\.1.*not in the deterministic system-library allowlist/
);
});
test('requires the deterministic external-system-library records', () => {
const manifest = createValidManifest();
manifest.externalSystemLibraries.reverse();
assert.match(
validateLinuxRuntimeManifest(manifest).join('\n'),
/externalSystemLibraries must exactly match the deterministic allowlist/
);
const allowedNames = new Set([
...GLIBC_TOOLCHAIN_ALLOWLIST,
...EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
]);
for (const dependencyName of createValidManifest().runtimeDependencyClosure
.externalDependencies) {
assert.equal(allowedNames.has(dependencyName), true, dependencyName);
}
});
test('requires a Linux x64 build host and every required tool version', () => {
const wrongPlatform = createValidManifest();
wrongPlatform.buildHost.platform = 'darwin';
assert.match(
validateLinuxRuntimeManifest(wrongPlatform).join('\n'),
/buildHost\.platform must be "linux"/
);
const wrongArch = createValidManifest();
wrongArch.buildHost.arch = 'arm64';
assert.match(
validateLinuxRuntimeManifest(wrongArch).join('\n'),
/buildHost\.arch must be "x64"/
);
for (const tool of REQUIRED_TOOLS) {
const missingTool = createValidManifest();
delete missingTool.buildHost.tools[tool];
assert.match(
validateLinuxRuntimeManifest(missingTool).join('\n'),
new RegExp(`buildHost\\.tools\\.${tool.replace('-', '\\-')}`)
);
}
const unexpectedTool = createValidManifest();
unexpectedTool.buildHost.tools.unexpected = '1.0';
assert.match(
validateLinuxRuntimeManifest(unexpectedTool).join('\n'),
/buildHost\.tools contains unexpected tool "unexpected"/
);
const relativePkgConfigDir = createValidManifest();
relativePkgConfigDir.buildHost.systemPkgConfigDirs = ['relative/pkgconfig'];
assert.match(
validateLinuxRuntimeManifest(relativePkgConfigDir).join('\n'),
/buildHost\.systemPkgConfigDirs must be a non-empty array of unique absolute paths/
);
for (const packageName of EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES) {
const missingPackage = createValidManifest();
delete missingPackage.buildHost.systemPkgConfigPackages[packageName];
assert.match(
validateLinuxRuntimeManifest(missingPackage).join('\n'),
new RegExp(
`buildHost\\.systemPkgConfigPackages\\.${packageName.replace(
'-',
'\\-'
)}`
)
);
}
const unexpectedPackage = createValidManifest();
unexpectedPackage.buildHost.systemPkgConfigPackages.unexpected = '1.0';
assert.match(
validateLinuxRuntimeManifest(unexpectedPackage).join('\n'),
/buildHost\.systemPkgConfigPackages contains unexpected package "unexpected"/
);
});
test('accepts and stages the current Linux CI system build-input manifest', (t) => {
const systemManifest = createSystemBuildInputManifest();
assert.deepEqual(validateLinuxSystemBuildInputManifest(systemManifest), []);
@@ -248,6 +523,14 @@ test('returns deterministic validation errors for untrusted input', () => {
validateLinuxRuntimeManifest(manifest).join('\n'),
/runtimeFiles\[0\]\.sha256/
);
const hostileAllowlist = createValidManifest();
hostileAllowlist.externalSystemLibraries = 1n;
assert.doesNotThrow(() => validateLinuxRuntimeManifest(hostileAllowlist));
assert.match(
validateLinuxRuntimeManifest(hostileAllowlist).join('\n'),
/externalSystemLibraries/
);
});
test('requires schema, provenance, target, package, and source metadata', () => {
@@ -303,10 +586,18 @@ test('requires exact FFmpeg and mpv source package records', () => {
license: 'LGPL-2.1-or-later',
},
};
assert.deepEqual(validateLinuxRuntimeManifest(substitutedPackages), [
'Linux runtime manifest packages.ffmpeg must be an object.',
'Linux runtime manifest packages.mpv must be an object.',
]);
const substitutedErrors =
validateLinuxRuntimeManifest(substitutedPackages).join('\n');
for (const packageName of SOURCE_PACKAGES.map(({ id }) => id)) {
assert.match(
substitutedErrors,
new RegExp(`packages\\.${packageName} must be an object`)
);
}
assert.match(
substitutedErrors,
/packages contains unexpected source package "multimediaRuntime"/
);
});
test('rejects GPL and nonfree FFmpeg configurations', () => {
@@ -410,7 +701,8 @@ test('validates safe, unique shared-library metadata', () => {
runtimeFile('libcodec.a', 'duplicate'),
]);
assert.deepEqual(validateLinuxRuntimeManifest(manifest), [
const errors = validateLinuxRuntimeManifest(manifest);
assert.deepEqual(errors.slice(0, 7), [
'Linux runtime manifest runtimeFiles[0].name must be a safe shared-library basename.',
'Linux runtime manifest runtimeFiles[0].size must be a positive integer.',
'Linux runtime manifest runtimeFiles[0].sha256 must be a lowercase 64-character hexadecimal digest.',
@@ -419,6 +711,14 @@ test('validates safe, unique shared-library metadata', () => {
'Linux runtime manifest runtimeFiles contains duplicate name "libcodec.a".',
'Linux runtime manifest runtimeFiles must include a versioned libmpv.so.N entry.',
]);
assert.match(
errors.join('\n'),
/runtimeDependencyClosure\.entries\[0\]\.name must be a safe shared-library basename/
);
assert.match(
errors.join('\n'),
/runtimeDependencyClosure\.entries contains duplicate name "libcodec\.a"/
);
});
test('rejects control characters in shared-library basenames', () => {
@@ -448,6 +748,13 @@ test('stages only declared Linux libraries and materializes source symlinks', (t
fixture.manifest.runtimeFiles.push(
runtimeFile('libmpv.so', versionedMpvContents)
);
fixture.manifest.runtimeTotalBytes += versionedMpvContents.length;
fixture.manifest.runtimeDependencyClosure.entries.push({
name: 'libmpv.so',
needed: ['libmpv.so.2'],
rpath: [],
runpath: ['$ORIGIN'],
});
fs.writeFileSync(
path.join(fixture.prefix, 'runtime-manifest.json'),
`${JSON.stringify(fixture.manifest, null, 2)}\n`
@@ -499,6 +806,15 @@ test('stages only declared Linux libraries and materializes source symlinks', (t
stagedManifest.runtimeFiles,
fixture.manifest.runtimeFiles
);
for (const field of [
'packages',
'runtimeTotalBytes',
'runtimeDependencyClosure',
'externalSystemLibraries',
'buildHost',
]) {
assert.deepEqual(stagedManifest[field], fixture.manifest[field], field);
}
});
test('copies runtime libraries only from the verified byte snapshot', () => {
@@ -635,6 +951,7 @@ test('rejects missing files and mismatched size or hash metadata', (t) => {
{
mutateManifest(manifest) {
manifest.runtimeFiles[0].size += 1;
manifest.runtimeTotalBytes += 1;
},
},
/Size mismatch for Linux runtime file.*libmpv\.so\.2/