mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 06:06:18 -08:00
fix(embedded-mpv): pin Linux runtime inputs
This commit is contained in:
1 parent
e51fdc5286
commit
95349ec1cb
5 files changed
+1132
-119
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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/
|
||||
|
||||
Reference in new issue
Block a user