fix(embedded-mpv): harden Linux runtime staging

This commit is contained in:
4gray committed 2026-07-17 22:42:36 +02:00
1 parent 064a72d256
commit d3a4d4dab8
3 files changed
+586 -110

No files matched your search

+71 -9
View File
@@ -9,6 +9,7 @@ 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 LINUX_SYSTEM_BACKEND = 'process-isolated mpv --wid';
function isObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -176,23 +177,32 @@ function validateMpv(errors, mpv) {
}
validateFlags(errors, mpv.mesonFlags, 'mpv.mesonFlags', {
forbidden: ['-Dgpl=true'],
forbidden: [],
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'
) {
for (const [option, requiredFlag] of [
['-Dgpl', '-Dgpl=false'],
['-Dlibmpv', '-Dlibmpv=true'],
]) {
const assignments = mpv.mesonFlags.filter(
(flag) =>
typeof flag === 'string' && flag.startsWith(`${option}=`)
);
if (assignments.length > 1) {
errors.push(
`Linux runtime manifest mpv.mesonFlags must not include "${flag}".`
`Linux runtime manifest mpv.mesonFlags must assign "${option}" exactly once.`
);
}
for (const flag of assignments) {
if (flag !== requiredFlag) {
errors.push(
`Linux runtime manifest mpv.mesonFlags must not include "${flag}".`
);
}
}
}
}
}
@@ -300,7 +310,59 @@ function validateLinuxRuntimeManifest(manifest) {
return errors;
}
function isLinuxSystemBuildInputManifest(manifest) {
return isObject(manifest) && manifest.linuxBackend === LINUX_SYSTEM_BACKEND;
}
function validateLinuxSystemBuildInputManifest(manifest) {
if (!isObject(manifest)) {
return ['Linux system build-input manifest must be an object.'];
}
const errors = [];
if (manifest.linuxBackend !== LINUX_SYSTEM_BACKEND) {
errors.push(
`Linux system build-input manifest linuxBackend must be "${LINUX_SYSTEM_BACKEND}".`
);
}
if (!isObject(manifest.buildInputs)) {
errors.push(
'Linux system build-input manifest buildInputs must be an object.'
);
} else {
for (const packageName of ['libmpvDevPackage', 'mpvPackage']) {
if (!isNonEmptyString(manifest.buildInputs[packageName])) {
errors.push(
`Linux system build-input manifest buildInputs.${packageName} must be a non-empty string.`
);
}
}
}
if (!isNonEmptyString(manifest.sourceDistribution)) {
errors.push(
'Linux system build-input manifest sourceDistribution must be a non-empty string.'
);
}
if (Object.prototype.hasOwnProperty.call(manifest, 'origin')) {
errors.push(
'Linux system build-input manifest must not include origin.'
);
}
if (Object.prototype.hasOwnProperty.call(manifest, 'runtimeFiles')) {
errors.push(
'Linux system build-input manifest must not include runtimeFiles.'
);
}
return errors;
}
module.exports = {
LINUX_SYSTEM_BACKEND,
LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION,
isLinuxSystemBuildInputManifest,
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
};
@@ -12,6 +12,7 @@ const require = createRequire(import.meta.url);
const {
LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION,
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
} = require('./linux-runtime-manifest.cjs');
const workspaceRoot = fileURLToPath(new URL('../..', import.meta.url));
@@ -21,6 +22,7 @@ const stageRuntimeScript = path.join(
'embedded-mpv',
'stage-runtime.mjs'
);
const stageRuntimeSource = fs.readFileSync(stageRuntimeScript, 'utf8');
function sha256(value) {
return crypto.createHash('sha256').update(value).digest('hex');
@@ -83,6 +85,18 @@ function createValidManifest(
};
}
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-')
@@ -108,11 +122,13 @@ function createFixture(t, options = {}) {
fs.writeFileSync(path.join(libDir, name), contents);
}
const manifest = createValidManifest(
[...contentsByName].map(([name, contents]) =>
runtimeFile(name, contents)
)
);
const manifest =
options.manifest ??
createValidManifest(
[...contentsByName].map(([name, contents]) =>
runtimeFile(name, contents)
)
);
options.mutateManifest?.(manifest);
if (options.removeRuntimeFile) {
@@ -133,6 +149,10 @@ function createFixture(t, options = {}) {
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,
@@ -160,6 +180,59 @@ test('accepts a complete LGPL Linux x64 source-build manifest', () => {
assert.deepEqual(validateLinuxRuntimeManifest(createValidManifest()), []);
});
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.',
@@ -300,6 +373,32 @@ test('requires libmpv and GPL-disabled mpv Meson flags', () => {
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('validates safe, unique shared-library metadata', () => {
const manifest = createValidManifest([
{
@@ -357,12 +456,7 @@ test('stages only declared Linux libraries and materializes source symlinks', (t
const result = runStage(fixture);
assert.equal(result.status, 0, result.stderr);
const destinationRoot = path.join(
fixture.root,
'vendor',
'embedded-mpv',
'linux-x64'
);
const destinationRoot = destinationRootFor(fixture);
const destinationLibDir = path.join(destinationRoot, 'lib');
assert.deepEqual(fs.readdirSync(destinationLibDir).sort(), [
'libavcodec.so.61',
@@ -407,6 +501,99 @@ test('stages only declared Linux libraries and materializes source symlinks', (t
);
});
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(
@@ -489,6 +676,16 @@ test('rejects forbidden build flags and a missing versioned libmpv entry', (t) =
},
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(
+307 -90
View File
@@ -5,7 +5,9 @@ import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const {
isLinuxSystemBuildInputManifest,
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
} = require('./linux-runtime-manifest.cjs');
const rawArgs = process.argv.slice(2);
@@ -185,137 +187,174 @@ function readLinuxRuntimeManifest() {
);
}
const errors = validateLinuxRuntimeManifest(manifest);
const mode = isLinuxSystemBuildInputManifest(manifest)
? 'system-build-inputs'
: 'bundled-runtime';
const errors =
mode === 'system-build-inputs'
? validateLinuxSystemBuildInputManifest(manifest)
: validateLinuxRuntimeManifest(manifest);
if (errors.length > 0) {
throw new Error(
['Invalid Linux runtime manifest.', ...errors].join('\n')
);
}
return manifest;
return { manifest, mode };
}
function sha256File(filePath) {
return crypto
.createHash('sha256')
.update(fs.readFileSync(filePath))
.digest('hex');
function sha256Contents(contents) {
return crypto.createHash('sha256').update(contents).digest('hex');
}
function assertPathInsideDirectory(filePath, directory) {
const relativePath = path.relative(
fs.realpathSync(directory),
fs.realpathSync(filePath)
);
function resolvePathInsideDirectory(filePath, directory, message) {
const resolvedDirectory = fs.realpathSync(directory);
const resolvedFilePath = fs.realpathSync(filePath);
const relativePath = path.relative(resolvedDirectory, resolvedFilePath);
if (
relativePath === '..' ||
relativePath.startsWith(`..${path.sep}`) ||
path.isAbsolute(relativePath)
) {
throw new Error(
`Declared Linux runtime file resolves outside prefix/lib: ${filePath}`
);
throw new Error(`${message}: ${filePath}`);
}
return resolvedFilePath;
}
function verifyLinuxRuntimeFiles(manifest) {
for (const runtimeFile of manifest.runtimeFiles) {
const sourcePath = path.join(sourceLibDir, runtimeFile.name);
if (!fs.existsSync(sourcePath)) {
function readVerifiedLinuxRuntimeFiles(manifest) {
return manifest.runtimeFiles.map((runtimeFile) => {
const declaredSourcePath = path.join(sourceLibDir, runtimeFile.name);
if (!fs.existsSync(declaredSourcePath)) {
throw new Error(
`Missing declared Linux runtime file: ${sourcePath}`
`Missing declared Linux runtime file: ${declaredSourcePath}`
);
}
assertPathInsideDirectory(sourcePath, sourceLibDir);
const sourcePath = resolvePathInsideDirectory(
declaredSourcePath,
sourceLibDir,
'Declared Linux runtime file resolves outside prefix/lib'
);
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}`
`Declared Linux runtime path is not a regular file: ${declaredSourcePath}`
);
}
const actualSha256 = sha256File(sourcePath);
const contents = fs.readFileSync(sourcePath);
if (contents.byteLength !== runtimeFile.size) {
throw new Error(
`Size mismatch for Linux runtime file ${declaredSourcePath}: expected ${runtimeFile.size}, received ${contents.byteLength}`
);
}
const actualSha256 = sha256Contents(contents);
if (actualSha256 !== runtimeFile.sha256) {
throw new Error(
`SHA-256 mismatch for Linux runtime file ${sourcePath}: expected ${runtimeFile.sha256}, received ${actualSha256}`
`SHA-256 mismatch for Linux runtime file ${declaredSourcePath}: expected ${runtimeFile.sha256}, received ${actualSha256}`
);
}
return { contents, runtimeFile };
});
}
function readLinuxHeaderFiles() {
resolvePathInsideDirectory(
sourceIncludeDir,
normalizedPrefix,
'Linux include directory resolves outside source prefix'
);
const headerFiles = [];
function visitDirectory(sourceDirectory, relativeDirectory) {
for (const entry of fs.readdirSync(sourceDirectory, {
withFileTypes: true,
})) {
const sourcePath = path.join(sourceDirectory, entry.name);
const relativePath = path.join(relativeDirectory, entry.name);
if (entry.isDirectory()) {
visitDirectory(sourcePath, relativePath);
continue;
}
if (!entry.isFile() && !entry.isSymbolicLink()) {
continue;
}
const resolvedSourcePath = resolvePathInsideDirectory(
sourcePath,
sourceIncludeDir,
'Linux header resolves outside prefix/include'
);
if (!fs.statSync(resolvedSourcePath).isFile()) {
throw new Error(
`Linux header is not a regular file: ${sourcePath}`
);
}
headerFiles.push({
contents: fs.readFileSync(resolvedSourcePath),
relativePath,
});
}
}
visitDirectory(path.join(sourceIncludeDir, 'mpv'), 'mpv');
return headerFiles;
}
function lstatIfExists(filePath) {
try {
return fs.lstatSync(filePath);
} catch (error) {
if (error?.code === 'ENOENT') {
return null;
}
throw error;
}
}
function assertSafeLinuxDestinationPath() {
const relativeDestination = path.relative(workspaceRoot, destinationRoot);
if (
relativeDestination === '..' ||
relativeDestination.startsWith(`..${path.sep}`) ||
path.isAbsolute(relativeDestination)
) {
throw new Error(
`Linux runtime destination escapes the workspace: ${destinationRoot}`
);
}
let currentPath = workspaceRoot;
for (const segment of relativeDestination.split(path.sep)) {
currentPath = path.join(currentPath, segment);
const stat = lstatIfExists(currentPath);
if (stat?.isSymbolicLink()) {
throw new Error(
`Linux runtime destination path contains a symbolic link: ${currentPath}`
);
}
}
}
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}`
);
}
if (platform === 'win32' && !hasWindowsImportLibrary(sourceLibDir)) {
throw new Error(
`Missing Windows libmpv import library in ${sourceLibDir}`
);
}
fs.rmSync(destinationIncludeDir, { recursive: true, force: true });
fs.rmSync(destinationLibDir, { recursive: true, force: true });
fs.mkdirSync(destinationRoot, { recursive: true });
copyDirectory(
path.join(sourceIncludeDir, 'mpv'),
path.join(destinationIncludeDir, 'mpv')
);
if (platform !== 'linux') {
copyDirectory(sourceLibDir, destinationLibDir, runtimeFileFilter);
} else {
copyLinuxRuntimeFiles(externalManifest);
}
if (platform === 'win32') {
copyDirectory(sourceBinDir, destinationLibDir, runtimeFileFilter);
}
const manifest = {
function createLinuxStagedManifest(externalManifest, mode) {
return {
...externalManifest,
origin: 'vendored-lgpl',
...(platform === 'linux'
...(mode === 'bundled-runtime'
? { sourceBuildOrigin: externalManifest.origin }
: {}),
platform,
arch,
stagedAt: new Date().toISOString(),
runtimeFiles:
platform === 'linux'
mode === 'bundled-runtime'
? externalManifest.runtimeFiles.map((runtimeFile) => ({
...runtimeFile,
}))
: listRuntimeFiles(destinationLibDir),
: [],
ffmpeg: {
licensePolicy:
'LGPL, built without --enable-gpl and --enable-nonfree',
@@ -332,15 +371,193 @@ try {
externalManifest.mpv?.mesonFlags ??
'Record the exact mpv Meson flags used to build this runtime.',
},
sourceDistribution:
externalManifest.sourceDistribution ??
`Publish exact source archives and local patches with the ${platform}-${arch} binary release.`,
sourceDistribution: externalManifest.sourceDistribution,
};
}
function writeLinuxStagingTree(
stagingRoot,
headerFiles,
verifiedRuntimeFiles,
stagedManifest
) {
for (const headerFile of headerFiles) {
const destinationPath = path.join(
stagingRoot,
'include',
headerFile.relativePath
);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, headerFile.contents);
fs.chmodSync(destinationPath, 0o644);
}
for (const verifiedRuntimeFile of verifiedRuntimeFiles) {
const destinationPath = path.join(
stagingRoot,
'lib',
verifiedRuntimeFile.runtimeFile.name
);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.writeFileSync(destinationPath, verifiedRuntimeFile.contents);
fs.chmodSync(destinationPath, 0o755);
}
fs.writeFileSync(
path.join(destinationRoot, 'runtime-manifest.json'),
`${JSON.stringify(manifest, null, 2)}\n`
path.join(stagingRoot, 'runtime-manifest.json'),
`${JSON.stringify(stagedManifest, null, 2)}\n`
);
}
function publishLinuxStagingTree(stagingRoot) {
const token = `${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
const backupRoot = path.join(
path.dirname(destinationRoot),
`.linux-x64.backup-${token}`
);
let movedPreviousDestination = false;
try {
assertSafeLinuxDestinationPath();
const destinationStat = lstatIfExists(destinationRoot);
if (destinationStat && !destinationStat.isDirectory()) {
throw new Error(
`Linux runtime destination is not a directory: ${destinationRoot}`
);
}
if (destinationStat) {
fs.renameSync(destinationRoot, backupRoot);
movedPreviousDestination = true;
}
fs.renameSync(stagingRoot, destinationRoot);
if (movedPreviousDestination) {
fs.rmSync(backupRoot, { recursive: true, force: true });
}
} catch (error) {
if (
movedPreviousDestination &&
!lstatIfExists(destinationRoot) &&
lstatIfExists(backupRoot)
) {
fs.renameSync(backupRoot, destinationRoot);
}
throw error;
} finally {
fs.rmSync(stagingRoot, { recursive: true, force: true });
if (lstatIfExists(destinationRoot)) {
fs.rmSync(backupRoot, { recursive: true, force: true });
}
}
}
function stageLinuxRuntime(headerFiles, verifiedRuntimeFiles, stagedManifest) {
assertSafeLinuxDestinationPath();
const destinationParent = path.dirname(destinationRoot);
fs.mkdirSync(destinationParent, { recursive: true });
assertSafeLinuxDestinationPath();
const stagingRoot = path.join(
destinationParent,
`.linux-x64.stage-${process.pid}-${crypto
.randomBytes(8)
.toString('hex')}`
);
fs.mkdirSync(stagingRoot);
try {
writeLinuxStagingTree(
stagingRoot,
headerFiles,
verifiedRuntimeFiles,
stagedManifest
);
publishLinuxStagingTree(stagingRoot);
} catch (error) {
fs.rmSync(stagingRoot, { recursive: true, force: true });
throw error;
}
}
try {
assertExists(
path.join(sourceIncludeDir, 'mpv', 'client.h'),
'Missing libmpv header'
);
if (platform === 'linux') {
const { manifest: externalManifest, mode } = readLinuxRuntimeManifest();
const headerFiles = readLinuxHeaderFiles();
const verifiedRuntimeFiles =
mode === 'bundled-runtime'
? readVerifiedLinuxRuntimeFiles(externalManifest)
: [];
stageLinuxRuntime(
headerFiles,
verifiedRuntimeFiles,
createLinuxStagedManifest(externalManifest, mode)
);
} else {
const externalManifest =
readJsonIfExists(
path.join(normalizedPrefix, 'runtime-manifest.json')
) ?? {};
if (!findRuntimeFile(sourceLibDir)) {
throw new Error(
`Missing libmpv runtime for ${platform} in ${sourceLibDir}`
);
}
if (platform === 'win32' && !hasWindowsImportLibrary(sourceLibDir)) {
throw new Error(
`Missing Windows libmpv import library in ${sourceLibDir}`
);
}
fs.rmSync(destinationIncludeDir, { recursive: true, force: true });
fs.rmSync(destinationLibDir, { recursive: true, force: true });
fs.mkdirSync(destinationRoot, { recursive: true });
copyDirectory(
path.join(sourceIncludeDir, 'mpv'),
path.join(destinationIncludeDir, 'mpv')
);
copyDirectory(sourceLibDir, destinationLibDir, runtimeFileFilter);
if (platform === 'win32') {
copyDirectory(sourceBinDir, destinationLibDir, runtimeFileFilter);
}
const manifest = {
...externalManifest,
origin: 'vendored-lgpl',
platform,
arch,
stagedAt: new Date().toISOString(),
runtimeFiles: listRuntimeFiles(destinationLibDir),
ffmpeg: {
licensePolicy:
'LGPL, built without --enable-gpl and --enable-nonfree',
...externalManifest.ffmpeg,
configureFlags:
externalManifest.ffmpeg?.configureFlags ??
'Record the exact FFmpeg configure flags used to build this runtime.',
},
mpv: {
licensePolicy:
'LGPL-compatible libmpv, built with -Dlibmpv=true -Dgpl=false',
...externalManifest.mpv,
mesonFlags:
externalManifest.mpv?.mesonFlags ??
'Record the exact mpv Meson flags used to build this runtime.',
},
sourceDistribution:
externalManifest.sourceDistribution ??
`Publish exact source archives and local patches with the ${platform}-${arch} binary release.`,
};
fs.writeFileSync(
path.join(destinationRoot, 'runtime-manifest.json'),
`${JSON.stringify(manifest, null, 2)}\n`
);
}
console.log(
`Staged embedded MPV runtime for ${platform}-${arch} at ${path.relative(