From d3a4d4dab871a23ad29d5830b3b70c02c44e16e4 Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 17 Jul 2026 18:43:50 +0200 Subject: [PATCH] fix(embedded-mpv): harden Linux runtime staging --- tools/embedded-mpv/linux-runtime-manifest.cjs | 80 +++- .../linux-runtime-manifest.test.mjs | 219 +++++++++- tools/embedded-mpv/stage-runtime.mjs | 397 ++++++++++++++---- 3 files changed, 586 insertions(+), 110 deletions(-) diff --git a/tools/embedded-mpv/linux-runtime-manifest.cjs b/tools/embedded-mpv/linux-runtime-manifest.cjs index 9fe55f2d2..742bf6a57 100644 --- a/tools/embedded-mpv/linux-runtime-manifest.cjs +++ b/tools/embedded-mpv/linux-runtime-manifest.cjs @@ -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, }; diff --git a/tools/embedded-mpv/linux-runtime-manifest.test.mjs b/tools/embedded-mpv/linux-runtime-manifest.test.mjs index 8b15f0346..472b4cc49 100644 --- a/tools/embedded-mpv/linux-runtime-manifest.test.mjs +++ b/tools/embedded-mpv/linux-runtime-manifest.test.mjs @@ -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( diff --git a/tools/embedded-mpv/stage-runtime.mjs b/tools/embedded-mpv/stage-runtime.mjs index 4c5633e7a..1b758034c 100644 --- a/tools/embedded-mpv/stage-runtime.mjs +++ b/tools/embedded-mpv/stage-runtime.mjs @@ -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(