feat(embedded-mpv): validate staged Linux runtime

This commit is contained in:
4gray committed 2026-07-17 22:42:36 +02:00
1 parent cee65d0b2e
commit f643a85eaf
4 files changed
+876 -6

No files matched your search

@@ -0,0 +1,282 @@
'use strict';
const path = require('node:path');
const LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION = 1;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
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+)*$/;
function isObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function isSafeBasename(value) {
return (
isNonEmptyString(value) &&
path.basename(value) === value &&
!value.includes('/') &&
!value.includes('\\') &&
value !== '.' &&
value !== '..' &&
SAFE_BASENAME_PATTERN.test(value)
);
}
function validateSourceMetadata(errors, value, label) {
if (!isNonEmptyString(value.version)) {
errors.push(
`Linux runtime manifest ${label}.version must be a non-empty string.`
);
}
if (!isNonEmptyString(value.sourceUrl)) {
errors.push(
`Linux runtime manifest ${label}.sourceUrl must be a non-empty string.`
);
}
const hasSourceSha256 = value.sourceSha256 !== undefined;
const hasSourceGitCommit = value.sourceGitCommit !== undefined;
if (!hasSourceSha256 && !hasSourceGitCommit) {
errors.push(
`Linux runtime manifest ${label} must include sourceSha256 or sourceGitCommit.`
);
return;
}
if (
hasSourceSha256 &&
(typeof value.sourceSha256 !== 'string' ||
!SHA256_PATTERN.test(value.sourceSha256))
) {
errors.push(
`Linux runtime manifest ${label}.sourceSha256 must be a lowercase 64-character hexadecimal digest.`
);
}
if (
hasSourceGitCommit &&
(typeof value.sourceGitCommit !== 'string' ||
!GIT_COMMIT_PATTERN.test(value.sourceGitCommit))
) {
errors.push(
`Linux runtime manifest ${label}.sourceGitCommit must be a lowercase hexadecimal commit digest.`
);
}
}
function validatePackages(errors, packages) {
if (!isObject(packages) || Object.keys(packages).length === 0) {
errors.push(
'Linux runtime manifest packages must contain source package metadata.'
);
return;
}
for (const packageName of Object.keys(packages).sort()) {
const packageMetadata = packages[packageName];
const label = `packages.${packageName}`;
if (!isObject(packageMetadata)) {
errors.push(`Linux runtime manifest ${label} must be an object.`);
continue;
}
validateSourceMetadata(errors, packageMetadata, label);
if (!isNonEmptyString(packageMetadata.license)) {
errors.push(
`Linux runtime manifest ${label}.license must be a non-empty string.`
);
}
}
}
function validateFlags(errors, value, label, options) {
if (
!Array.isArray(value) ||
value.some((flag) => typeof flag !== 'string')
) {
errors.push(
`Linux runtime manifest ${label} must be an array of strings.`
);
return;
}
const addForbiddenErrors = () => {
for (const forbiddenFlag of options.forbidden) {
if (value.includes(forbiddenFlag)) {
errors.push(
`Linux runtime manifest ${label} must not include "${forbiddenFlag}".`
);
}
}
};
const addRequiredErrors = () => {
for (const requiredFlag of options.required) {
if (!value.includes(requiredFlag)) {
errors.push(
`Linux runtime manifest ${label} must include "${requiredFlag}".`
);
}
}
};
if (options.requiredFirst) {
addRequiredErrors();
addForbiddenErrors();
} else {
addForbiddenErrors();
addRequiredErrors();
}
}
function validateFfmpeg(errors, ffmpeg) {
if (!isObject(ffmpeg)) {
errors.push('Linux runtime manifest ffmpeg must be an object.');
return;
}
validateFlags(errors, ffmpeg.configureFlags, 'ffmpeg.configureFlags', {
forbidden: ['--enable-gpl', '--enable-nonfree'],
required: ['--disable-gpl', '--disable-nonfree'],
});
}
function validateMpv(errors, mpv) {
if (!isObject(mpv)) {
errors.push('Linux runtime manifest mpv must be an object.');
return;
}
validateFlags(errors, mpv.mesonFlags, 'mpv.mesonFlags', {
forbidden: ['-Dgpl=true'],
required: ['-Dgpl=false', '-Dlibmpv=true'],
requiredFirst: true,
});
if (Array.isArray(mpv.mesonFlags)) {
for (const flag of mpv.mesonFlags) {
if (
typeof flag === 'string' &&
flag.startsWith('-Dgpl=') &&
flag !== '-Dgpl=false' &&
flag !== '-Dgpl=true'
) {
errors.push(
`Linux runtime manifest mpv.mesonFlags must not include "${flag}".`
);
}
}
}
}
function validateRuntimeFiles(errors, runtimeFiles) {
if (!Array.isArray(runtimeFiles) || runtimeFiles.length === 0) {
errors.push(
'Linux runtime manifest runtimeFiles must be a non-empty array.'
);
return;
}
const names = new Set();
let hasVersionedLibMpv = false;
for (const [index, runtimeFile] of runtimeFiles.entries()) {
const label = `runtimeFiles[${index}]`;
if (!isObject(runtimeFile)) {
errors.push(`Linux runtime manifest ${label} must be an object.`);
continue;
}
const { name, sha256, size } = runtimeFile;
const safeName = isSafeBasename(name);
if (!safeName) {
errors.push(
`Linux runtime manifest ${label}.name must be a safe shared-library basename.`
);
}
if (
typeof name === 'string' &&
safeName &&
!SHARED_LIBRARY_PATTERN.test(name)
) {
errors.push(
`Linux runtime manifest ${label}.name must end in ".so" or a numeric ".so.N" suffix.`
);
}
if (!Number.isInteger(size) || size <= 0) {
errors.push(
`Linux runtime manifest ${label}.size must be a positive integer.`
);
}
if (typeof sha256 !== 'string' || !SHA256_PATTERN.test(sha256)) {
errors.push(
`Linux runtime manifest ${label}.sha256 must be a lowercase 64-character hexadecimal digest.`
);
}
if (typeof name === 'string') {
if (names.has(name)) {
errors.push(
`Linux runtime manifest runtimeFiles contains duplicate name "${name}".`
);
} else {
names.add(name);
}
if (VERSIONED_LIBMPV_PATTERN.test(name)) {
hasVersionedLibMpv = true;
}
}
}
if (!hasVersionedLibMpv) {
errors.push(
'Linux runtime manifest runtimeFiles must include a versioned libmpv.so.N entry.'
);
}
}
function validateLinuxRuntimeManifest(manifest) {
if (!isObject(manifest)) {
return ['Linux runtime manifest must be an object.'];
}
const errors = [];
if (manifest.schemaVersion !== LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION) {
errors.push(
`Linux runtime manifest schemaVersion must be ${LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION}.`
);
}
if (manifest.origin !== 'vendored-lgpl-source-build') {
errors.push(
'Linux runtime manifest origin must be "vendored-lgpl-source-build".'
);
}
if (manifest.platform !== 'linux') {
errors.push('Linux runtime manifest platform must be "linux".');
}
if (manifest.arch !== 'x64') {
errors.push('Linux runtime manifest arch must be "x64".');
}
validatePackages(errors, manifest.packages);
validateFfmpeg(errors, manifest.ffmpeg);
validateMpv(errors, manifest.mpv);
if (!isNonEmptyString(manifest.sourceDistribution)) {
errors.push(
'Linux runtime manifest sourceDistribution must be a non-empty string.'
);
}
validateRuntimeFiles(errors, manifest.runtimeFiles);
return errors;
}
module.exports = {
LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION,
validateLinuxRuntimeManifest,
};
@@ -0,0 +1,473 @@
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,
} = require('./linux-runtime-manifest.cjs');
const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url));
const stageRuntimeScript = path.join(
workspaceRoot,
'tools',
'embedded-mpv',
'stage-runtime.mjs'
);
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 createValidManifest(
runtimeFiles = [
runtimeFile('libmpv.so.2', 'libmpv-runtime'),
runtimeFile('libavcodec.so.61', 'libavcodec-runtime'),
]
) {
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',
},
},
ffmpeg: {
version: '8.1',
sourceUrl: 'https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz',
sourceSha256: 'a'.repeat(64),
configureFlags: [
'--enable-shared',
'--disable-gpl',
'--disable-nonfree',
],
},
mpv: {
version: '0.41.0',
sourceUrl:
'https://github.com/mpv-player/mpv/archive/refs/tags/v0.41.0.tar.gz',
sourceSha256: 'b'.repeat(64),
mesonFlags: ['-Dlibmpv=true', '-Dgpl=false'],
},
sourceDistribution:
'https://downloads.example.test/iptvnator/linux-runtime-sources.tar.zst',
runtimeFiles,
};
}
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'],
]);
for (const [name, contents] of contentsByName) {
fs.writeFileSync(path.join(libDir, name), contents);
}
const 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 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('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/
);
});
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('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('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'),
]);
assert.deepEqual(validateLinuxRuntimeManifest(manifest), [
'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.',
]);
});
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\[2\]\.name must be a safe shared-library basename/
);
});
test('stages only declared Linux libraries and materializes source symlinks', (t) => {
const fixture = createFixture(t);
const versionedMpvContents = fs.readFileSync(
path.join(fixture.libDir, 'libmpv.so.2')
);
fs.renameSync(
path.join(fixture.libDir, 'libmpv.so.2'),
path.join(fixture.libDir, 'libmpv.so.2.1.0')
);
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');
fixture.manifest.runtimeFiles.push(
runtimeFile('libmpv.so', versionedMpvContents)
);
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 = path.join(
fixture.root,
'vendor',
'embedded-mpv',
'linux-x64'
);
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
);
});
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;
},
},
/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.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);
}
});
+118 -5
View File
@@ -1,5 +1,12 @@
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const {
validateLinuxRuntimeManifest,
} = require('./linux-runtime-manifest.cjs');
const rawArgs = process.argv.slice(2);
const args = rawArgs[0] === '--' ? rawArgs.slice(1) : rawArgs;
@@ -161,11 +168,111 @@ function readJsonIfExists(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function readLinuxRuntimeManifest() {
const manifestPath = path.join(normalizedPrefix, 'runtime-manifest.json');
if (!fs.existsSync(manifestPath)) {
throw new Error(`Missing Linux runtime manifest: ${manifestPath}`);
}
let manifest;
try {
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
} catch (error) {
throw new Error(
`Invalid JSON in Linux runtime manifest ${manifestPath}: ${
error instanceof Error ? error.message : String(error)
}`
);
}
const errors = validateLinuxRuntimeManifest(manifest);
if (errors.length > 0) {
throw new Error(
['Invalid Linux runtime manifest.', ...errors].join('\n')
);
}
return manifest;
}
function sha256File(filePath) {
return crypto
.createHash('sha256')
.update(fs.readFileSync(filePath))
.digest('hex');
}
function assertPathInsideDirectory(filePath, directory) {
const relativePath = path.relative(
fs.realpathSync(directory),
fs.realpathSync(filePath)
);
if (
relativePath === '..' ||
relativePath.startsWith(`..${path.sep}`) ||
path.isAbsolute(relativePath)
) {
throw new Error(
`Declared Linux runtime file resolves outside prefix/lib: ${filePath}`
);
}
}
function verifyLinuxRuntimeFiles(manifest) {
for (const runtimeFile of manifest.runtimeFiles) {
const sourcePath = path.join(sourceLibDir, runtimeFile.name);
if (!fs.existsSync(sourcePath)) {
throw new Error(
`Missing declared Linux runtime file: ${sourcePath}`
);
}
assertPathInsideDirectory(sourcePath, sourceLibDir);
const sourceStat = fs.statSync(sourcePath);
if (!sourceStat.isFile()) {
throw new Error(
`Declared Linux runtime path is not a regular file: ${sourcePath}`
);
}
if (sourceStat.size !== runtimeFile.size) {
throw new Error(
`Size mismatch for Linux runtime file ${sourcePath}: expected ${runtimeFile.size}, received ${sourceStat.size}`
);
}
const actualSha256 = sha256File(sourcePath);
if (actualSha256 !== runtimeFile.sha256) {
throw new Error(
`SHA-256 mismatch for Linux runtime file ${sourcePath}: expected ${runtimeFile.sha256}, received ${actualSha256}`
);
}
}
}
function copyLinuxRuntimeFiles(manifest) {
fs.mkdirSync(destinationLibDir, { recursive: true });
for (const runtimeFile of manifest.runtimeFiles) {
const sourcePath = path.join(sourceLibDir, runtimeFile.name);
const destinationPath = path.join(destinationLibDir, runtimeFile.name);
fs.copyFileSync(sourcePath, destinationPath);
fs.chmodSync(destinationPath, 0o755);
}
}
try {
assertExists(
path.join(sourceIncludeDir, 'mpv', 'client.h'),
'Missing libmpv header'
);
const externalManifest =
platform === 'linux'
? readLinuxRuntimeManifest()
: (readJsonIfExists(
path.join(normalizedPrefix, 'runtime-manifest.json')
) ?? {});
if (platform === 'linux') {
verifyLinuxRuntimeFiles(externalManifest);
}
if (platform !== 'linux' && !findRuntimeFile(sourceLibDir)) {
throw new Error(
`Missing libmpv runtime for ${platform} in ${sourceLibDir}`
@@ -187,22 +294,28 @@ try {
);
if (platform !== 'linux') {
copyDirectory(sourceLibDir, destinationLibDir, runtimeFileFilter);
} else {
copyLinuxRuntimeFiles(externalManifest);
}
if (platform === 'win32') {
copyDirectory(sourceBinDir, destinationLibDir, runtimeFileFilter);
}
const externalManifest =
readJsonIfExists(
path.join(normalizedPrefix, 'runtime-manifest.json')
) ?? {};
const manifest = {
...externalManifest,
origin: 'vendored-lgpl',
...(platform === 'linux'
? { sourceBuildOrigin: externalManifest.origin }
: {}),
platform,
arch,
stagedAt: new Date().toISOString(),
runtimeFiles: listRuntimeFiles(destinationLibDir),
runtimeFiles:
platform === 'linux'
? externalManifest.runtimeFiles.map((runtimeFile) => ({
...runtimeFile,
}))
: listRuntimeFiles(destinationLibDir),
ffmpeg: {
licensePolicy:
'LGPL, built without --enable-gpl and --enable-nonfree',
+3 -1
View File
@@ -23,11 +23,13 @@
"{workspaceRoot}/tools/packaging/embedded-mpv-arch.test.mjs",
"{workspaceRoot}/tools/packaging/linux-frame-copy-profile.cjs",
"{workspaceRoot}/tools/packaging/linux-frame-copy-profile.test.mjs",
"{workspaceRoot}/tools/embedded-mpv/linux-runtime-manifest.cjs",
"{workspaceRoot}/tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"{workspaceRoot}/tools/embedded-mpv/stage-runtime.mjs",
"{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs"
],
"options": {
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs",
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs tools/embedded-mpv/linux-runtime-manifest.test.mjs",
"cwd": "{workspaceRoot}"
}
},