mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
fix(embedded-mpv): harden Linux runtime staging
This commit is contained in:
1 parent
064a72d256
commit
d3a4d4dab8
3 files changed
+586
-110
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user