mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
feat(embedded-mpv): build relocatable Linux helper
This commit is contained in:
1 parent
95349ec1cb
commit
0fb9ee2789
3 files changed
+298
-28
No files matched your search
@@ -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
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user