feat(embedded-mpv): build relocatable Linux helper

This commit is contained in:
4gray committed 2026-07-17 22:42:36 +02:00
1 parent 95349ec1cb
commit 0fb9ee2789
3 files changed
+298 -28

No files matched your search

+236 -17
View File
@@ -1,3 +1,4 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
@@ -10,6 +11,15 @@ const {
const {
removeStaleFrameCopyArtifacts,
} = require('../../tools/packaging/embedded-mpv-frame-copy-files.cjs');
const {
isLinuxSystemBuildInputManifest,
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
} = require('../../tools/embedded-mpv/linux-runtime-manifest.cjs');
const LINUX_PACKAGE_RUNTIME_MODES = Object.freeze(['system', 'bundled']);
const LINUX_STAGED_RUNTIME_ORIGIN = 'vendored-lgpl';
const LINUX_SOURCE_RUNTIME_ORIGIN = 'vendored-lgpl-source-build';
const workspaceRoot = process.cwd();
const addonRoot = path.join(
@@ -109,6 +119,124 @@ function readRuntimeManifest(runtimeRoot) {
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
}
function validatedLinuxSourceRuntime(runtimeRoot) {
const stagedManifest = readRuntimeManifest(runtimeRoot);
if (
stagedManifest === null ||
typeof stagedManifest !== 'object' ||
Array.isArray(stagedManifest)
) {
throw new Error(
`Staged Linux runtime manifest must be an object: ${path.join(
runtimeRoot,
'runtime-manifest.json'
)}`
);
}
const envelopeErrors = [];
if (stagedManifest.origin !== LINUX_STAGED_RUNTIME_ORIGIN) {
envelopeErrors.push(`origin must be "${LINUX_STAGED_RUNTIME_ORIGIN}"`);
}
if (stagedManifest.platform !== 'linux') {
envelopeErrors.push('platform must be "linux"');
}
if (stagedManifest.arch !== targetArch) {
envelopeErrors.push(`arch must be "${targetArch}"`);
}
if (
typeof stagedManifest.stagedAt !== 'string' ||
stagedManifest.stagedAt.trim().length === 0
) {
envelopeErrors.push('stagedAt must be a non-empty string');
}
if (!Array.isArray(stagedManifest.runtimeFiles)) {
envelopeErrors.push('runtimeFiles must be an array');
}
const systemBuildInputs = isLinuxSystemBuildInputManifest(stagedManifest);
let buildInputMode;
let sourceRuntimeManifest;
if (systemBuildInputs) {
buildInputMode = 'system-build-inputs';
sourceRuntimeManifest = {
linuxBackend: stagedManifest.linuxBackend,
buildInputs: stagedManifest.buildInputs,
sourceDistribution: stagedManifest.sourceDistribution,
};
if (
Array.isArray(stagedManifest.runtimeFiles) &&
stagedManifest.runtimeFiles.length !== 0
) {
envelopeErrors.push(
'system build inputs must not declare staged runtime files'
);
}
if (stagedManifest.sourceBuildOrigin !== undefined) {
envelopeErrors.push(
'system build inputs must not declare sourceBuildOrigin'
);
}
} else {
buildInputMode = 'bundled-runtime';
const sourceBuildOrigin = stagedManifest.sourceBuildOrigin;
const sourceMetadata = { ...stagedManifest };
delete sourceMetadata.sourceBuildOrigin;
delete sourceMetadata.stagedAt;
sourceRuntimeManifest = {
...sourceMetadata,
origin: sourceBuildOrigin,
};
if (sourceBuildOrigin !== LINUX_SOURCE_RUNTIME_ORIGIN) {
envelopeErrors.push(
`sourceBuildOrigin must be "${LINUX_SOURCE_RUNTIME_ORIGIN}"`
);
}
}
const sourceManifestErrors =
buildInputMode === 'system-build-inputs'
? validateLinuxSystemBuildInputManifest(sourceRuntimeManifest)
: validateLinuxRuntimeManifest(sourceRuntimeManifest);
const declaredRuntimeFileNames = Array.isArray(
sourceRuntimeManifest.runtimeFiles
)
? sourceRuntimeManifest.runtimeFiles
.map((runtimeFile) => runtimeFile.name)
.sort()
: [];
const stagedRuntimeFileNames = listRuntimeFiles(
path.join(runtimeRoot, 'lib'),
runtimeFilePredicate
)
.map((runtimeFile) => path.basename(runtimeFile))
.sort();
if (
JSON.stringify(stagedRuntimeFileNames) !==
JSON.stringify(declaredRuntimeFileNames)
) {
envelopeErrors.push(
'staged lib directory must exactly match manifest runtimeFiles'
);
}
const errors = [...envelopeErrors, ...sourceManifestErrors];
if (errors.length > 0) {
throw new Error(
[
`Invalid staged Linux runtime at ${runtimeRoot}:`,
...errors.map((error) => `- ${error}`),
].join('\n')
);
}
return {
buildInputMode,
sourceRuntimeManifest,
sourceRuntimeValidated: true,
};
}
function fileExists(filePath) {
return fs.existsSync(filePath) && fs.statSync(filePath).isFile();
}
@@ -211,15 +339,19 @@ function findLinuxLibMpv(libDir) {
}
function resolveRuntime() {
const vendoredHeader = path.join(vendoredIncludeDir, 'mpv', 'client.h');
const stagedLinuxRuntime =
targetPlatform === 'linux' && fs.existsSync(vendoredHeader)
? validatedLinuxSourceRuntime(vendoredRuntimeRoot)
: null;
const vendoredLibMpv =
targetPlatform === 'darwin'
? findLibMpv(vendoredLibDir)
: targetPlatform === 'win32'
? findWindowsLibMpv(vendoredRuntimeRoot)
: targetPlatform === 'linux'
? true
? stagedLinuxRuntime
: null;
const vendoredHeader = path.join(vendoredIncludeDir, 'mpv', 'client.h');
if (vendoredLibMpv && fs.existsSync(vendoredHeader)) {
const windowsImportLib =
@@ -237,6 +369,7 @@ function resolveRuntime() {
binDir: vendoredBinDir,
manifest: readRuntimeManifest(vendoredRuntimeRoot),
windowsImportLib,
...(stagedLinuxRuntime ?? {}),
};
}
@@ -248,6 +381,16 @@ function resolveRuntime() {
const systemIncludeDir =
process.env.LIBMPV_INCLUDE_DIR || '/usr/include';
if (fs.existsSync(path.join(systemIncludeDir, 'mpv', 'client.h'))) {
if (embeddedMpvRequired) {
throw new Error(
'Required Linux builds must use an explicit staged runtime manifest.'
);
}
const sourceRuntimeManifest = {
linuxBackend: 'process-isolated mpv --wid',
warning: 'Development-only unmanaged system libmpv toolchain.',
};
return {
origin: 'system-dev',
includeDir: systemIncludeDir,
@@ -255,10 +398,10 @@ function resolveRuntime() {
process.env.LINUX_NATIVE_LIBRARY_DIR ||
defaultLinuxSystemLibDir(),
binDir: undefined,
manifest: {
warning:
'Development-only system libmpv toolchain. Release packaging keeps the external-mpv-process contract.',
},
manifest: sourceRuntimeManifest,
buildInputMode: 'system-dev',
sourceRuntimeManifest,
sourceRuntimeValidated: false,
windowsImportLib: null,
};
}
@@ -288,19 +431,96 @@ function resolveRuntime() {
return null;
}
function writeLinuxProcessRuntimeManifest(runtime) {
function copyLinuxRuntimeClosureToNativeBuild(runtime) {
fs.rmSync(outputLibDir, { recursive: true, force: true });
const declaredRuntimeFiles =
runtime.buildInputMode === 'bundled-runtime'
? runtime.sourceRuntimeManifest.runtimeFiles
: [];
if (declaredRuntimeFiles.length === 0) {
return [];
}
fs.mkdirSync(outputLibDir, { recursive: true });
const copiedRuntimeFiles = [];
for (const runtimeFile of declaredRuntimeFiles) {
const sourcePath = path.join(runtime.libDir, runtimeFile.name);
let descriptor;
try {
descriptor = fs.openSync(
sourcePath,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW
);
const stat = fs.fstatSync(descriptor);
if (!stat.isFile()) {
throw new Error(
`Staged Linux runtime path is not a regular file: ${sourcePath}`
);
}
const contents = fs.readFileSync(descriptor);
if (contents.byteLength !== runtimeFile.size) {
throw new Error(
`Size mismatch for staged Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.size}, received ${contents.byteLength}.`
);
}
const actualSha256 = crypto
.createHash('sha256')
.update(contents)
.digest('hex');
if (actualSha256 !== runtimeFile.sha256) {
throw new Error(
`SHA-256 mismatch for staged Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.sha256}, received ${actualSha256}.`
);
}
const destinationPath = path.join(outputLibDir, runtimeFile.name);
fs.writeFileSync(destinationPath, contents, { mode: 0o755 });
copiedRuntimeFiles.push({ ...runtimeFile });
} finally {
if (descriptor !== undefined) {
fs.closeSync(descriptor);
}
}
}
return copiedRuntimeFiles;
}
function writeLinuxFrameCopyBuildManifest(runtime) {
const copiedRuntimeFiles = copyLinuxRuntimeClosureToNativeBuild(runtime);
const manifest = {
...runtime.manifest,
origin: 'external-mpv-process',
schemaVersion: 1,
origin: 'linux-frame-copy-build',
generatedAt: new Date().toISOString(),
runtimeFiles: [],
linuxBackend:
runtime.manifest.linuxBackend ?? 'process-isolated mpv --wid',
mpvExecutable: 'mpv',
platform: targetPlatform,
targetArch,
arch: targetArch,
buildInputMode: runtime.buildInputMode,
sourceRuntimeValidated: runtime.sourceRuntimeValidated,
allowedPackageRuntimeModes: [...LINUX_PACKAGE_RUNTIME_MODES],
packageRuntimeAvailability: {
system: true,
bundled: copiedRuntimeFiles.length > 0,
},
artifacts: {
addon: 'embedded_mpv.node',
frameReader: 'embedded_mpv_frame_reader.node',
helper: 'iptvnator_mpv_helper',
},
processIsolation: {
addonLoadsLibmpv: false,
helperLinksLibmpv: true,
helperRunpath: ['$ORIGIN/lib'],
},
nativeViewFallback: 'process-isolated mpv --wid',
libmpvSoname: 'libmpv.so.2',
runtimeFiles: copiedRuntimeFiles,
runtimeTotalBytes: copiedRuntimeFiles.reduce(
(total, runtimeFile) => total + runtimeFile.size,
0
),
sourceRuntime: runtime.sourceRuntimeManifest,
};
fs.writeFileSync(
@@ -477,7 +697,7 @@ function main() {
},
})
: targetPlatform === 'linux'
? writeLinuxProcessRuntimeManifest(runtime)
? writeLinuxFrameCopyBuildManifest(runtime)
: copyGenericRuntimeToNativeBuild(runtime);
fs.rmSync(unavailableMarkerFile, { force: true });
@@ -496,8 +716,7 @@ function main() {
LIBMPV_INCLUDE_DIR: runtime.includeDir,
...(targetPlatform === 'linux'
? {
LINUX_NATIVE_LIBRARY_DIR:
process.env.LINUX_NATIVE_LIBRARY_DIR || runtime.libDir,
LINUX_NATIVE_LIBRARY_DIR: runtime.libDir,
}
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
...(runtime.windowsImportLib
+2 -2
View File
@@ -158,8 +158,8 @@
],
"ldflags": [
"-pthread",
"-Wl,-rpath,'$$ORIGIN/lib'",
"-Wl,-rpath,<!(node -p \"process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'\")"
"-Wl,--enable-new-dtags",
"-Wl,-rpath,'$$ORIGIN/lib'"
],
"libraries": [
"-L<!(node -p \"process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'\")",
@@ -570,11 +570,50 @@ describe('Embedded MPV native source recording invariants', () => {
expect(buildScriptSource).toContain('cleanNativeBuildIntermediates();');
});
it('does not stage Linux libmpv runtime libraries', () => {
expect(stageRuntimeSource).toContain(
"if (platform !== 'linux') {\n" +
' copyDirectory(sourceLibDir, destinationLibDir, runtimeFileFilter);\n' +
' }'
it('validates and copies only the staged Linux shared-library closure', () => {
expect(buildScriptSource).toContain(
"require('../../tools/embedded-mpv/linux-runtime-manifest.cjs')"
);
expect(buildScriptSource).toContain(
'validateLinuxRuntimeManifest(sourceRuntimeManifest)'
);
expect(buildScriptSource).toContain(
'validateLinuxSystemBuildInputManifest(sourceRuntimeManifest)'
);
expect(buildScriptSource).toContain(
'copyLinuxRuntimeClosureToNativeBuild(runtime)'
);
expect(buildScriptSource).toContain(
'runtime.sourceRuntimeManifest.runtimeFiles'
);
expect(buildScriptSource).toContain(
'SHA-256 mismatch for staged Linux runtime file'
);
expect(buildScriptSource).toContain(
'LINUX_NATIVE_LIBRARY_DIR: runtime.libDir'
);
expect(buildScriptSource).not.toContain(
'process.env.LINUX_NATIVE_LIBRARY_DIR || runtime.libDir'
);
});
it('writes a profile-neutral Linux frame-copy build manifest', () => {
expect(buildScriptSource).toContain(
"const LINUX_PACKAGE_RUNTIME_MODES = Object.freeze(['system', 'bundled']);"
);
expect(buildScriptSource).toContain("origin: 'linux-frame-copy-build'");
expect(buildScriptSource).toContain(
'allowedPackageRuntimeModes: [...LINUX_PACKAGE_RUNTIME_MODES]'
);
expect(buildScriptSource).toContain(
'buildInputMode: runtime.buildInputMode'
);
expect(buildScriptSource).toContain(
'sourceRuntime: runtime.sourceRuntimeManifest'
);
expect(buildScriptSource).toContain('runtimeFiles: copiedRuntimeFiles');
expect(buildScriptSource).not.toContain(
'writeLinuxProcessRuntimeManifest'
);
});
@@ -617,12 +656,9 @@ describe('Embedded MPV native source recording invariants', () => {
expect(buildAndMakeWorkflowSource).toContain(
'Linux frame-copy helper must link libmpv'
);
expect(buildScriptSource).toContain("origin: 'external-mpv-process'");
expect(buildScriptSource).toContain('writeLinuxProcessRuntimeManifest');
expect(buildScriptSource).toContain('runtimeFiles: []');
});
it('supports Linux system-development inputs without leaving stale frame-copy artifacts', () => {
it('keeps optional Linux system development separate from required staged inputs', () => {
expect(buildScriptSource).toContain(
"const systemIncludeDir =\n process.env.LIBMPV_INCLUDE_DIR || '/usr/include';"
);
@@ -635,6 +671,9 @@ describe('Embedded MPV native source recording invariants', () => {
expect(buildScriptSource).toContain(
"if (!embeddedMpvRequired && runtime.origin === 'system-dev')"
);
expect(buildScriptSource).toContain(
'Required Linux builds must use an explicit staged runtime manifest.'
);
expect(buildScriptSource).toContain(
'removeStaleFrameCopyArtifacts(outputDir);'
);
@@ -817,6 +856,7 @@ describe('Embedded MPV native build configuration', () => {
)?.[1];
expect(linuxAddonConfig?.libraries).not.toContain('-lmpv');
expect(JSON.stringify(linuxAddonConfig)).not.toContain('-lmpv');
expect(linuxHelperConfig?.libraries).toEqual(
expect.arrayContaining([
'-lmpv',
@@ -826,5 +866,16 @@ describe('Embedded MPV native build configuration', () => {
'-ldl',
])
);
const runtimeLinkerFlags = linuxHelperConfig?.ldflags.filter(
(flag: string) => flag.startsWith('-Wl,')
);
expect(runtimeLinkerFlags).toEqual([
'-Wl,--enable-new-dtags',
"-Wl,-rpath,'$$ORIGIN/lib'",
]);
expect(JSON.stringify(runtimeLinkerFlags)).not.toContain(
'LINUX_NATIVE_LIBRARY_DIR'
);
});
});