Files
iptvnator/tools/embedded-mpv/linux-runtime-manifest.test.mjs
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* docs: design Linux frame-copy packaging

* docs: plan Linux frame-copy packaging

* feat(packaging): define Linux frame-copy profiles

* fix(packaging): reject inherited profile names

* feat(embedded-mpv): validate staged Linux runtime

* fix(embedded-mpv): require Linux source packages

* fix(embedded-mpv): harden Linux runtime staging

* feat(embedded-mpv): build LGPL Linux runtime

* fix(embedded-mpv): pin Linux runtime inputs

* feat(embedded-mpv): build relocatable Linux helper

* fix(embedded-mpv): require bundled Linux runtime

* fix(embedded-mpv): make Linux runtime portable

* feat(packaging): ship Linux frame-copy artifacts

* fix(embedded-mpv): verify Linux helper linkage

* fix(packaging): enforce Linux frame-copy isolation

* fix(embedded-mpv): pin Linux display data

* docs(embedded-mpv): document Linux frame-copy packaging

* feat(embedded-mpv): probe Linux frame-copy runtime

* test(embedded-mpv): smoke packaged Linux frame-copy

* docs(embedded-mpv): clarify Linux system runtime baseline

* fix(embedded-mpv): harden Linux runtime capability gate

* ci: verify Linux frame-copy packages

* test(embedded-mpv): harden packaged Linux smoke

* test(embedded-mpv): preserve packaged GL mode

* test(packaging): harden Linux package probes

* fix(embedded-mpv): enable private Snap shared memory

* fix(embedded-mpv): sanitize Linux helper environment

* fix(packaging): enforce private Snap memory semantics

* fix(packaging): reject ambiguous Snap memory metadata

* fix(embedded-mpv): prioritize trusted Snap GL

* fix(packaging): reject advanced Snap YAML semantics

* fix(packaging): reject arbitrary Snap YAML aliases

* feat(packaging): ship Linux runtime license notices

* docs(embedded-mpv): document Linux runtime distribution

* fix(packaging): parse Snap trailing comments safely

* fix(release): gate Snap publish on public source release

* fix(packaging): strip VCS metadata from source bundle

* docs(packaging): clarify Linux source release gate

* test(embedded-mpv): smoke missing bundled libmpv

* style(embedded-mpv): format final validation inputs

* fix(e2e): satisfy fixture index signature typing

* fix(ci): declare fontconfig gperf generator

* fix(embedded-mpv): hash runtime cache identities

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): tighten runtime delivery gates

* fix(ci): decouple Linux runtime matrix

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): validate Linux frame-copy runtimes

* fix(packaging): scope Snap Electron library checks

* feat(packaging): ship Linux frame-copy runtimes

* fix(packaging): improve Linux runtime smoke diagnostics

* fix(packaging): expose bounded helper probe details

* test(packaging): trace Snap EGL probe failures

* fix(packaging): prefer core22 ABI in Snap helper

* fix(packaging): bound helper probe capture

* fix(packaging): harden Linux frame-copy releases

* fix(packaging): canonicalize libplacebo submodule identity

* fix(packaging): make source archive inspection portable

* fix(packaging): harden Snap release verification
2026-07-18 17:28:22 +02:00

1186 lines
41 KiB
JavaScript

import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const {
LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION,
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
} = require('./linux-runtime-manifest.cjs');
const {
DEFAULT_SYSTEM_PKG_CONFIG_DIRS,
EXTERNAL_SYSTEM_LIBRARIES,
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
GLIBC_TOOLCHAIN_ALLOWLIST,
MINIMUM_TOOL_VERSIONS,
PORTABLE_ABI_BASELINE,
REQUIRED_TOOLS,
RUNTIME_EXTERNAL_CONFIGURATION,
SOURCE_PACKAGES,
} = require('./build-linux-runtime.cjs');
const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url));
const stageRuntimeScript = path.join(
workspaceRoot,
'tools',
'embedded-mpv',
'stage-runtime.mjs'
);
const stageRuntimeSource = fs.readFileSync(stageRuntimeScript, 'utf8');
const EXPECTED_LIBPLACEBO_SOURCE_SUBMODULES = Object.freeze([
'450bd2232225d6c7728a4108055ac2e37cef6475 3rdparty/Vulkan-Headers',
'97b54ca9e75f5303507699d27c6b4f4efe4641a1 3rdparty/fast_float',
'73db193f853e2ee079bf3ca8a64aa2eaf6459043 3rdparty/glad',
'15206881c006c79667fe5154fe80c01c65410679 3rdparty/jinja',
'297fc8e356e6836a62087949245d09a28e9f1b13 3rdparty/markupsafe',
'242f35efa067a46c595645eeda7b1771ea1f83b1 demos/3rdparty/nuklear',
]);
function sha256(value) {
return crypto.createHash('sha256').update(value).digest('hex');
}
function runtimeFile(name, contents) {
return {
name,
size: Buffer.byteLength(contents),
sha256: sha256(contents),
};
}
function sourcePackageRecord(sourcePackage) {
return {
version: sourcePackage.version,
sourceUrl: sourcePackage.sourceUrl,
...(sourcePackage.sourceTag
? { sourceTag: sourcePackage.sourceTag }
: {}),
...(sourcePackage.buildInput
? { buildInput: structuredClone(sourcePackage.buildInput) }
: {}),
...(sourcePackage.sourceKind === 'archive'
? { sourceSha256: sourcePackage.expectedSha256 }
: {
sourceGitCommit: sourcePackage.expectedGitCommit,
sourceSubmodules: [...EXPECTED_LIBPLACEBO_SOURCE_SUBMODULES],
}),
license: sourcePackage.license,
};
}
function createValidManifest(
runtimeFiles = [
runtimeFile('libmpv.so.2', 'libmpv-runtime'),
runtimeFile('libavcodec.so.61', 'libavcodec-runtime'),
runtimeFile('libmpv.so', 'libmpv-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,
soname: runtimeEntry.name.startsWith('libmpv.so')
? 'libmpv.so.2'
: 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: {
...packages.ffmpeg,
configureFlags: [
'--enable-shared',
'--disable-gpl',
'--disable-nonfree',
],
},
mpv: {
...packages.mpv,
mesonFlags: ['-Dlibmpv=true', '-Dgpl=false'],
},
sourceDistribution:
'Publish the exact source archives, including pinned hwdata pnp.ids and the MIT-licensed libdisplay-info source archive.',
runtimeFiles,
runtimeTotalBytes: runtimeFiles.reduce(
(total, runtimeEntry) => total + (runtimeEntry?.size ?? 0),
0
),
runtimeAbi: {
baseline: { ...PORTABLE_ABI_BASELINE },
files: runtimeFiles
.filter(
(runtimeEntry) =>
runtimeEntry && typeof runtimeEntry.name === 'string'
)
.map(({ name }) => ({
name,
requiredGlibc: '2.34',
requiredGlibcxx: null,
})),
},
runtimeExternalConfiguration: structuredClone(
RUNTIME_EXTERNAL_CONFIGURATION
),
runtimeDependencyClosure: {
entries: closureEntries,
externalDependencies,
},
externalSystemLibraries: EXTERNAL_SYSTEM_LIBRARIES.map(
(externalLibrary) => ({ ...externalLibrary })
),
buildHost: {
platform: 'linux',
arch: 'x64',
release: 'fixture-kernel',
glibcVersion: '2.35',
systemPkgConfigDirs: [...DEFAULT_SYSTEM_PKG_CONFIG_DIRS],
systemPkgConfigPackages: Object.fromEntries(
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES.map((packageName) => [
packageName,
`${packageName} fixture version`,
])
),
tools: Object.fromEntries(
REQUIRED_TOOLS.map((tool) => [
tool,
`${tool} ${MINIMUM_TOOL_VERSIONS[tool]}`,
])
),
},
};
}
function createSystemBuildInputManifest() {
return {
linuxBackend: 'process-isolated mpv --wid',
buildInputs: {
libmpvDevPackage: '2:0.40.0-3ubuntu2',
mpvPackage: '0.40.0-3ubuntu2',
},
sourceDistribution:
'Linux CI build inputs come from Ubuntu runner packages. Runtime playback uses the system mpv executable; IPTVnator does not bundle or load libmpv in the Electron process on Linux.',
};
}
function createFixture(t, options = {}) {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'iptvnator-linux-runtime-test-')
);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const prefix = path.join(root, 'prefix');
const includeDir = path.join(prefix, 'include', 'mpv');
const libDir = path.join(prefix, 'lib');
fs.mkdirSync(includeDir, { recursive: true });
fs.mkdirSync(libDir, { recursive: true });
fs.writeFileSync(
path.join(includeDir, 'client.h'),
'/* libmpv header */\n'
);
const contentsByName = new Map([
['libmpv.so.2', 'libmpv-runtime'],
['libavcodec.so.61', 'libavcodec-runtime'],
['libmpv.so', 'libmpv-runtime'],
]);
for (const [name, contents] of contentsByName) {
fs.writeFileSync(path.join(libDir, name), contents);
}
const manifest =
options.manifest ??
createValidManifest(
[...contentsByName].map(([name, contents]) =>
runtimeFile(name, contents)
)
);
options.mutateManifest?.(manifest);
if (options.removeRuntimeFile) {
fs.rmSync(path.join(libDir, options.removeRuntimeFile), {
force: true,
});
}
if (options.removeHeader) {
fs.rmSync(path.join(includeDir, 'client.h'), { force: true });
}
if (!options.omitManifest) {
fs.writeFileSync(
path.join(prefix, 'runtime-manifest.json'),
`${JSON.stringify(manifest, null, 2)}\n`
);
}
return { libDir, manifest, prefix, root };
}
function destinationRootFor(fixture) {
return path.join(fixture.root, 'vendor', 'embedded-mpv', 'linux-x64');
}
function runStage(fixture) {
return spawnSync(
process.execPath,
[stageRuntimeScript, 'linux', 'x64', fixture.prefix],
{
cwd: fixture.root,
encoding: 'utf8',
}
);
}
function assertStageRejected(t, options, expectedError) {
const fixture = createFixture(t, options);
const result = runStage(fixture);
assert.notEqual(result.status, 0, result.stdout);
assert.match(result.stderr, expectedError);
}
test('exports the Linux runtime manifest schema version', () => {
assert.equal(LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION, 1);
});
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 submoduleCommitMismatch = createValidManifest();
submoduleCommitMismatch.packages.libplacebo.sourceSubmodules[0] = `${'f'.repeat(40)} 3rdparty/Vulkan-Headers`;
assert.match(
validateLinuxRuntimeManifest(submoduleCommitMismatch).join('\n'),
/packages\.libplacebo\.sourceSubmodules must equal the pinned records/
);
const hostHwdataInput = createValidManifest();
hostHwdataInput.packages.hwdata.buildInput.relativePath =
'/usr/share/hwdata/pnp.ids';
assert.match(
validateLinuxRuntimeManifest(hostHwdataInput).join('\n'),
/packages\.hwdata\.buildInput must equal the pinned build input/
);
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 safe SONAME metadata and a bundled versioned libmpv target', () => {
const missingLinkerAlias = createValidManifest();
const aliasFile = missingLinkerAlias.runtimeFiles.find(
({ name }) => name === 'libmpv.so'
);
missingLinkerAlias.runtimeFiles = missingLinkerAlias.runtimeFiles.filter(
({ name }) => name !== 'libmpv.so'
);
missingLinkerAlias.runtimeTotalBytes -= aliasFile.size;
missingLinkerAlias.runtimeAbi.files =
missingLinkerAlias.runtimeAbi.files.filter(
({ name }) => name !== 'libmpv.so'
);
missingLinkerAlias.runtimeDependencyClosure.entries =
missingLinkerAlias.runtimeDependencyClosure.entries.filter(
({ name }) => name !== 'libmpv.so'
);
assert.match(
validateLinuxRuntimeManifest(missingLinkerAlias).join('\n'),
/runtimeFiles must include the libmpv\.so linker alias/
);
const missingLibmpvSoname = createValidManifest();
missingLibmpvSoname.runtimeDependencyClosure.entries.find(
({ name }) => name === 'libmpv.so'
).soname = null;
assert.match(
validateLinuxRuntimeManifest(missingLibmpvSoname).join('\n'),
/libmpv\.so must declare a versioned SONAME present in runtimeFiles/
);
const unsafeSoname = createValidManifest();
unsafeSoname.runtimeDependencyClosure.entries[0].soname = '../libmpv.so.2';
assert.match(
validateLinuxRuntimeManifest(unsafeSoname).join('\n'),
/runtimeDependencyClosure\.entries\[0\]\.soname must be null or a safe shared-library basename/
);
const absentSonameTarget = createValidManifest();
absentSonameTarget.runtimeDependencyClosure.entries.find(
({ name }) => name === 'libmpv.so'
).soname = 'libmpv.so.99';
assert.match(
validateLinuxRuntimeManifest(absentSonameTarget).join('\n'),
/libmpv\.so must declare a versioned SONAME present in runtimeFiles/
);
});
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"/
);
const unsupportedGlibc = createValidManifest();
unsupportedGlibc.buildHost.glibcVersion = '2.36';
assert.match(
validateLinuxRuntimeManifest(unsupportedGlibc).join('\n'),
/buildHost\.glibcVersion.*portable ABI baseline.*2\.35/i
);
const unsupportedMeson = createValidManifest();
unsupportedMeson.buildHost.tools.meson = 'meson 1.5.9';
assert.match(
validateLinuxRuntimeManifest(unsupportedMeson).join('\n'),
/buildHost\.tools\.meson.*requires 1\.6\.0 or newer/i
);
});
test('requires exact portable ABI and external configuration records', () => {
const missingAbiFile = createValidManifest();
missingAbiFile.runtimeAbi.files.pop();
assert.match(
validateLinuxRuntimeManifest(missingAbiFile).join('\n'),
/runtimeAbi\.files must contain one record for every runtime file/
);
const newerGlibcSymbol = createValidManifest();
newerGlibcSymbol.runtimeAbi.files[0].requiredGlibc = '2.36';
assert.match(
validateLinuxRuntimeManifest(newerGlibcSymbol).join('\n'),
/runtimeAbi\.files\[0\]\.requiredGlibc.*maximum 2\.35/
);
const newerGlibcxxSymbol = createValidManifest();
newerGlibcxxSymbol.runtimeAbi.files[0].requiredGlibcxx = '3.4.31';
assert.match(
validateLinuxRuntimeManifest(newerGlibcxxSymbol).join('\n'),
/runtimeAbi\.files\[0\]\.requiredGlibcxx.*maximum 3\.4\.30/
);
const wrongBaseline = createValidManifest();
wrongBaseline.runtimeAbi.baseline.distribution = 'current host';
assert.match(
validateLinuxRuntimeManifest(wrongBaseline).join('\n'),
/runtimeAbi\.baseline must exactly match the portable ABI baseline/
);
const buildPathConfiguration = createValidManifest();
buildPathConfiguration.runtimeExternalConfiguration.fontconfig.configDirectory =
'/tmp/build-prefix/etc/fonts';
assert.match(
validateLinuxRuntimeManifest(buildPathConfiguration).join('\n'),
/runtimeExternalConfiguration must exactly match the system-owned runtime paths/
);
});
test('requires the source-distribution statement to name pinned display data', () => {
const manifest = createValidManifest();
manifest.sourceDistribution =
'Publish all of the other source archives with the binary release.';
const errors = validateLinuxRuntimeManifest(manifest).join('\n');
assert.match(errors, /sourceDistribution must explicitly include hwdata/);
assert.match(errors, /sourceDistribution must explicitly include pnp\.ids/);
assert.match(
errors,
/sourceDistribution must explicitly include libdisplay-info/
);
});
test('accepts and stages the current Linux CI system build-input manifest', (t) => {
const systemManifest = createSystemBuildInputManifest();
assert.deepEqual(validateLinuxSystemBuildInputManifest(systemManifest), []);
const fixture = createFixture(t, { manifest: systemManifest });
const result = runStage(fixture);
assert.equal(result.status, 0, result.stderr);
const destinationRoot = destinationRootFor(fixture);
assert.equal(
fs.readFileSync(
path.join(destinationRoot, 'include', 'mpv', 'client.h'),
'utf8'
),
'/* libmpv header */\n'
);
assert.equal(fs.existsSync(path.join(destinationRoot, 'lib')), false);
const stagedManifest = JSON.parse(
fs.readFileSync(
path.join(destinationRoot, 'runtime-manifest.json'),
'utf8'
)
);
assert.equal(stagedManifest.origin, 'vendored-lgpl');
assert.equal(stagedManifest.platform, 'linux');
assert.equal(stagedManifest.arch, 'x64');
assert.deepEqual(stagedManifest.runtimeFiles, []);
assert.deepEqual(stagedManifest.buildInputs, systemManifest.buildInputs);
assert.equal(stagedManifest.linuxBackend, systemManifest.linuxBackend);
assert.equal('sourceBuildOrigin' in stagedManifest, false);
});
test('rejects malformed system build-input manifests without falling through', (t) => {
const malformedSystemManifest = createSystemBuildInputManifest();
malformedSystemManifest.buildInputs.mpvPackage = '';
malformedSystemManifest.runtimeFiles = [];
assert.deepEqual(
validateLinuxSystemBuildInputManifest(malformedSystemManifest),
[
'Linux system build-input manifest buildInputs.mpvPackage must be a non-empty string.',
'Linux system build-input manifest must not include runtimeFiles.',
]
);
const fixture = createFixture(t, { manifest: malformedSystemManifest });
const result = runStage(fixture);
assert.notEqual(result.status, 0, result.stdout);
assert.match(result.stderr, /buildInputs\.mpvPackage/);
assert.match(result.stderr, /must not include runtimeFiles/);
});
test('returns deterministic validation errors for untrusted input', () => {
assert.deepEqual(validateLinuxRuntimeManifest(null), [
'Linux runtime manifest must be an object.',
]);
assert.deepEqual(validateLinuxRuntimeManifest(null), [
'Linux runtime manifest must be an object.',
]);
const manifest = createValidManifest();
manifest.runtimeFiles[0].sha256 = Symbol('not-a-digest');
assert.doesNotThrow(() => validateLinuxRuntimeManifest(manifest));
assert.match(
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', () => {
const manifest = createValidManifest();
manifest.schemaVersion = 2;
manifest.origin = 'vendored-lgpl';
manifest.platform = 'darwin';
manifest.arch = 'arm64';
manifest.packages.ffmpeg.sourceUrl = '';
manifest.packages.mpv.version = '';
manifest.sourceDistribution = ' ';
assert.deepEqual(validateLinuxRuntimeManifest(manifest), [
'Linux runtime manifest schemaVersion must be 1.',
'Linux runtime manifest origin must be "vendored-lgpl-source-build".',
'Linux runtime manifest platform must be "linux".',
'Linux runtime manifest arch must be "x64".',
'Linux runtime manifest packages.ffmpeg.sourceUrl must be a non-empty string.',
'Linux runtime manifest packages.mpv.version must be a non-empty string.',
'Linux runtime manifest sourceDistribution must be a non-empty string.',
]);
manifest.packages = {};
assert.match(
validateLinuxRuntimeManifest(manifest).join('\n'),
/packages must contain source package metadata/
);
});
test('requires exact FFmpeg and mpv source package records', () => {
const missingFfmpeg = createValidManifest();
missingFfmpeg.packages.libavcodec = missingFfmpeg.packages.ffmpeg;
delete missingFfmpeg.packages.ffmpeg;
assert.match(
validateLinuxRuntimeManifest(missingFfmpeg).join('\n'),
/packages\.ffmpeg must be an object/
);
const missingMpv = createValidManifest();
missingMpv.packages.libmpv = missingMpv.packages.mpv;
delete missingMpv.packages.mpv;
assert.match(
validateLinuxRuntimeManifest(missingMpv).join('\n'),
/packages\.mpv must be an object/
);
const substitutedPackages = createValidManifest();
substitutedPackages.packages = {
multimediaRuntime: {
version: '1.0.0',
sourceUrl: 'https://example.test/multimedia-runtime.tar.xz',
sourceSha256: 'c'.repeat(64),
license: 'LGPL-2.1-or-later',
},
};
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', () => {
const cases = [
{
flags: ['--disable-nonfree'],
expected: /must include "--disable-gpl"/,
},
{
flags: ['--disable-gpl'],
expected: /must include "--disable-nonfree"/,
},
{
flags: ['--disable-gpl', '--disable-nonfree', '--enable-gpl'],
expected: /must not include "--enable-gpl"/,
},
{
flags: ['--disable-gpl', '--disable-nonfree', '--enable-nonfree'],
expected: /must not include "--enable-nonfree"/,
},
];
for (const { flags, expected } of cases) {
const manifest = createValidManifest();
manifest.ffmpeg.configureFlags = flags;
assert.match(
validateLinuxRuntimeManifest(manifest).join('\n'),
expected
);
}
const manifest = createValidManifest();
manifest.ffmpeg = { configureFlags: ['--enable-gpl'] };
assert.match(validateLinuxRuntimeManifest(manifest)[0], /--enable-gpl/);
});
test('requires libmpv and GPL-disabled mpv Meson flags', () => {
const cases = [
{
flags: ['-Dgpl=false'],
expected: /must include "-Dlibmpv=true"/,
},
{
flags: ['-Dlibmpv=true'],
expected: /must include "-Dgpl=false"/,
},
{
flags: ['-Dlibmpv=true', '-Dgpl=false', '-Dgpl=true'],
expected: /must not include "-Dgpl=true"/,
},
];
for (const { flags, expected } of cases) {
const manifest = createValidManifest();
manifest.mpv.mesonFlags = flags;
assert.match(
validateLinuxRuntimeManifest(manifest).join('\n'),
expected
);
}
const manifest = createValidManifest();
manifest.mpv = { mesonFlags: ['-Dgpl=true'] };
assert.match(validateLinuxRuntimeManifest(manifest)[0], /-Dgpl=false/);
});
test('rejects duplicate or contradictory required mpv Meson assignments', () => {
const cases = [
{
flags: ['-Dlibmpv=true', '-Dlibmpv=false', '-Dgpl=false'],
expected: /must assign "-Dlibmpv" exactly once/,
},
{
flags: ['-Dlibmpv=true', '-Dgpl=false', '-Dgpl=true'],
expected: /must assign "-Dgpl" exactly once/,
},
{
flags: ['-Dlibmpv=true', '-Dlibmpv=true', '-Dgpl=false'],
expected: /must assign "-Dlibmpv" exactly once/,
},
];
for (const { expected, flags } of cases) {
const manifest = createValidManifest();
manifest.mpv.mesonFlags = flags;
assert.match(
validateLinuxRuntimeManifest(manifest).join('\n'),
expected
);
}
});
test('rejects duplicate assignments for every mpv Meson option', () => {
const manifest = createValidManifest();
manifest.mpv.mesonFlags.push('-Ddrm=enabled', '-Ddrm=enabled');
assert.match(
validateLinuxRuntimeManifest(manifest).join('\n'),
/mpv\.mesonFlags must assign "-Ddrm" exactly once/
);
});
test('validates safe, unique shared-library metadata', () => {
const manifest = createValidManifest([
{
name: '../libmpv.so.2',
size: 0,
sha256: 'ABC',
},
runtimeFile('libcodec.a', 'archive'),
runtimeFile('libcodec.a', 'duplicate'),
]);
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.',
'Linux runtime manifest runtimeFiles[1].name must end in ".so" or a numeric ".so.N" suffix.',
'Linux runtime manifest runtimeFiles[2].name must end in ".so" or a numeric ".so.N" suffix.',
'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', () => {
const manifest = createValidManifest();
manifest.runtimeFiles.push(
runtimeFile('libinjected.so.1\n', 'unsafe-name')
);
assert.match(
validateLinuxRuntimeManifest(manifest).join('\n'),
/runtimeFiles\[3\]\.name must be a safe shared-library basename/
);
});
test('stages only declared Linux libraries and materializes source symlinks', (t) => {
const fixture = createFixture(t);
fs.renameSync(
path.join(fixture.libDir, 'libmpv.so.2'),
path.join(fixture.libDir, 'libmpv.so.2.1.0')
);
fs.rmSync(path.join(fixture.libDir, 'libmpv.so'));
fs.symlinkSync('libmpv.so.2.1.0', path.join(fixture.libDir, 'libmpv.so.2'));
fs.symlinkSync('libmpv.so.2', path.join(fixture.libDir, 'libmpv.so'));
fs.writeFileSync(path.join(fixture.libDir, 'libundeclared.so.1'), 'extra');
fs.writeFileSync(
path.join(fixture.prefix, 'runtime-manifest.json'),
`${JSON.stringify(fixture.manifest, null, 2)}\n`
);
const result = runStage(fixture);
assert.equal(result.status, 0, result.stderr);
const destinationRoot = destinationRootFor(fixture);
const destinationLibDir = path.join(destinationRoot, 'lib');
assert.deepEqual(fs.readdirSync(destinationLibDir).sort(), [
'libavcodec.so.61',
'libmpv.so',
'libmpv.so.2',
]);
for (const runtimeEntry of fixture.manifest.runtimeFiles) {
const destinationPath = path.join(destinationLibDir, runtimeEntry.name);
const stat = fs.lstatSync(destinationPath);
assert.equal(stat.isFile(), true);
assert.equal(stat.isSymbolicLink(), false);
assert.equal(stat.size, runtimeEntry.size);
assert.equal(
sha256(fs.readFileSync(destinationPath)),
runtimeEntry.sha256
);
}
assert.equal(
fs.readFileSync(
path.join(destinationRoot, 'include', 'mpv', 'client.h'),
'utf8'
),
'/* libmpv header */\n'
);
const stagedManifest = JSON.parse(
fs.readFileSync(
path.join(destinationRoot, 'runtime-manifest.json'),
'utf8'
)
);
assert.equal(stagedManifest.origin, 'vendored-lgpl');
assert.equal(
stagedManifest.sourceBuildOrigin,
'vendored-lgpl-source-build'
);
assert.equal(stagedManifest.platform, 'linux');
assert.equal(stagedManifest.arch, 'x64');
assert.deepEqual(
stagedManifest.runtimeFiles,
fixture.manifest.runtimeFiles
);
for (const field of [
'packages',
'runtimeTotalBytes',
'runtimeAbi',
'runtimeExternalConfiguration',
'runtimeDependencyClosure',
'externalSystemLibraries',
'buildHost',
]) {
assert.deepEqual(stagedManifest[field], fixture.manifest[field], field);
}
});
test('copies runtime libraries only from the verified byte snapshot', () => {
assert.match(
stageRuntimeSource,
/function readVerifiedLinuxRuntimeFiles\(manifest\)/
);
assert.match(
stageRuntimeSource,
/contents = fs\.readFileSync\(sourcePath\)/
);
assert.match(
stageRuntimeSource,
/fs\.writeFileSync\(destinationPath, verifiedRuntimeFile\.contents\)/
);
assert.doesNotMatch(
stageRuntimeSource,
/function copyLinuxRuntimeFiles\(manifest\)/
);
});
test('atomically replaces the complete Linux destination tree', (t) => {
const fixture = createFixture(t);
const destinationRoot = destinationRootFor(fixture);
fs.mkdirSync(destinationRoot, { recursive: true });
fs.writeFileSync(path.join(destinationRoot, 'stale-runtime.txt'), 'stale');
const result = runStage(fixture);
assert.equal(result.status, 0, result.stderr);
assert.equal(
fs.existsSync(path.join(destinationRoot, 'stale-runtime.txt')),
false
);
assert.deepEqual(
fs
.readdirSync(path.dirname(destinationRoot))
.filter((name) => name.startsWith('.linux-x64.')),
[]
);
});
test('rejects a libmpv header symlink that escapes the source prefix', (t) => {
const fixture = createFixture(t);
const outsideHeader = path.join(fixture.root, 'outside-client.h');
const clientHeader = path.join(
fixture.prefix,
'include',
'mpv',
'client.h'
);
fs.writeFileSync(outsideHeader, '/* untrusted external header */\n');
fs.rmSync(clientHeader);
fs.symlinkSync(outsideHeader, clientHeader);
const result = runStage(fixture);
assert.notEqual(result.status, 0, result.stdout);
assert.match(
result.stderr,
/Linux header resolves outside prefix\/include/
);
});
test('rejects destination root and ancestor symlink redirection', (t) => {
for (const symlinkLocation of ['destination', 'ancestor']) {
const fixture = createFixture(t);
const destinationRoot = destinationRootFor(fixture);
const outsideRoot = path.join(
fixture.root,
`outside-${symlinkLocation}`
);
fs.mkdirSync(outsideRoot, { recursive: true });
fs.writeFileSync(path.join(outsideRoot, 'untouched.txt'), 'untouched');
if (symlinkLocation === 'destination') {
fs.mkdirSync(path.dirname(destinationRoot), { recursive: true });
fs.symlinkSync(outsideRoot, destinationRoot);
} else {
const embeddedMpvRoot = path.dirname(destinationRoot);
fs.mkdirSync(path.dirname(embeddedMpvRoot), { recursive: true });
fs.symlinkSync(outsideRoot, embeddedMpvRoot);
}
const result = runStage(fixture);
assert.notEqual(result.status, 0, result.stdout);
assert.match(
result.stderr,
/Linux runtime destination path contains a symbolic link/
);
assert.equal(
fs.readFileSync(path.join(outsideRoot, 'untouched.txt'), 'utf8'),
'untouched'
);
}
});
test('rejects missing Linux headers or manifests', (t) => {
assertStageRejected(t, { removeHeader: true }, /Missing libmpv header/);
assertStageRejected(
t,
{ omitManifest: true },
/Missing Linux runtime manifest/
);
});
test('rejects unsafe or duplicate declared library names', (t) => {
assertStageRejected(
t,
{
mutateManifest(manifest) {
manifest.runtimeFiles[0].name = '../libmpv.so.2';
},
},
/safe shared-library basename/
);
assertStageRejected(
t,
{
mutateManifest(manifest) {
manifest.runtimeFiles.push({ ...manifest.runtimeFiles[0] });
},
},
/duplicate name "libmpv.so.2"/
);
});
test('rejects missing files and mismatched size or hash metadata', (t) => {
assertStageRejected(
t,
{ removeRuntimeFile: 'libavcodec.so.61' },
/Missing declared Linux runtime file.*libavcodec\.so\.61/
);
assertStageRejected(
t,
{
mutateManifest(manifest) {
manifest.runtimeFiles[0].size += 1;
manifest.runtimeTotalBytes += 1;
},
},
/Size mismatch for Linux runtime file.*libmpv\.so\.2/
);
assertStageRejected(
t,
{
mutateManifest(manifest) {
manifest.runtimeFiles[0].sha256 = '0'.repeat(64);
},
},
/SHA-256 mismatch for Linux runtime file.*libmpv\.so\.2/
);
});
test('rejects forbidden build flags and a missing versioned libmpv entry', (t) => {
const invalidConfigurations = [
{
mutateManifest(manifest) {
manifest.ffmpeg.configureFlags.push('--enable-gpl');
},
expected: /must not include "--enable-gpl"/,
},
{
mutateManifest(manifest) {
manifest.ffmpeg.configureFlags.push('--enable-nonfree');
},
expected: /must not include "--enable-nonfree"/,
},
{
mutateManifest(manifest) {
manifest.mpv.mesonFlags = ['-Dlibmpv=true', '-Dgpl=true'];
},
expected: /must include "-Dgpl=false"/,
},
{
mutateManifest(manifest) {
manifest.mpv.mesonFlags = ['-Dgpl=false'];
},
expected: /must include "-Dlibmpv=true"/,
},
{
mutateManifest(manifest) {
manifest.mpv.mesonFlags = [
'-Dlibmpv=true',
'-Dlibmpv=false',
'-Dgpl=false',
];
},
expected: /must assign "-Dlibmpv" exactly once/,
},
{
mutateManifest(manifest) {
manifest.runtimeFiles = manifest.runtimeFiles.filter(
({ name }) => name !== 'libmpv.so.2'
);
},
expected: /must include a versioned libmpv\.so\.N entry/,
},
];
for (const { expected, mutateManifest } of invalidConfigurations) {
assertStageRejected(t, { mutateManifest }, expected);
}
});