mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
fix(embedded-mpv): verify Linux helper linkage
This commit is contained in:
1 parent
45c3ab86d6
commit
d960997061
6 files changed
+904
-118
No files matched your search
@@ -16,11 +16,15 @@ const {
|
||||
validateLinuxRuntimeManifest,
|
||||
validateLinuxSystemBuildInputManifest,
|
||||
} = require('../../tools/embedded-mpv/linux-runtime-manifest.cjs');
|
||||
const {
|
||||
resolveVerifiedLinuxLibMpvSoname,
|
||||
runWithCleanup,
|
||||
validateLinuxFrameCopyLinkage,
|
||||
} = require('./embedded-mpv-linux-linkage.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 VERSIONED_LINUX_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
|
||||
|
||||
const workspaceRoot = process.cwd();
|
||||
const addonRoot = path.join(
|
||||
@@ -452,35 +456,6 @@ function assertRequiredLinuxFrameCopyRuntime(runtime) {
|
||||
}
|
||||
}
|
||||
|
||||
function deriveLinuxLibMpvSoname(runtime) {
|
||||
if (
|
||||
runtime.buildInputMode !== 'bundled-runtime' ||
|
||||
runtime.sourceRuntimeValidated !== true ||
|
||||
!Array.isArray(runtime.sourceRuntimeManifest?.runtimeFiles)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = runtime.sourceRuntimeManifest.runtimeFiles
|
||||
.map((runtimeFile) => runtimeFile.name)
|
||||
.filter((fileName) => VERSIONED_LINUX_LIBMPV_PATTERN.test(fileName))
|
||||
.sort((left, right) => {
|
||||
const leftVersionParts = left
|
||||
.slice('libmpv.so.'.length)
|
||||
.split('.').length;
|
||||
const rightVersionParts = right
|
||||
.slice('libmpv.so.'.length)
|
||||
.split('.').length;
|
||||
return (
|
||||
leftVersionParts - rightVersionParts ||
|
||||
left.length - right.length ||
|
||||
left.localeCompare(right)
|
||||
);
|
||||
});
|
||||
|
||||
return candidates[0] ?? null;
|
||||
}
|
||||
|
||||
function copyLinuxRuntimeClosureToNativeBuild(runtime) {
|
||||
fs.rmSync(outputLibDir, { recursive: true, force: true });
|
||||
|
||||
@@ -540,7 +515,18 @@ function copyLinuxRuntimeClosureToNativeBuild(runtime) {
|
||||
|
||||
function writeLinuxFrameCopyBuildManifest(runtime) {
|
||||
const copiedRuntimeFiles = copyLinuxRuntimeClosureToNativeBuild(runtime);
|
||||
const libmpvSoname = deriveLinuxLibMpvSoname(runtime);
|
||||
const libmpvSoname =
|
||||
runtime.sourceRuntimeValidated === true &&
|
||||
runtime.buildInputMode === 'bundled-runtime' &&
|
||||
copiedRuntimeFiles.length > 0
|
||||
? resolveVerifiedLinuxLibMpvSoname({
|
||||
outputLibDir,
|
||||
runtimeFiles: copiedRuntimeFiles,
|
||||
runtimeDependencyClosure:
|
||||
runtime.sourceRuntimeManifest.runtimeDependencyClosure,
|
||||
readDynamicSection: readLinuxDynamicSection,
|
||||
})
|
||||
: null;
|
||||
const packageRuntimeAvailable =
|
||||
runtime.sourceRuntimeValidated === true &&
|
||||
runtime.buildInputMode === 'bundled-runtime' &&
|
||||
@@ -704,12 +690,33 @@ function runNodeGyp(command, env) {
|
||||
}
|
||||
}
|
||||
|
||||
function readLinuxDynamicSection(filePath) {
|
||||
const result = spawnSync('readelf', ['-d', filePath], {
|
||||
cwd: workspaceRoot,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
if (result.error) {
|
||||
throw new Error(
|
||||
`Unable to run readelf for ${filePath}: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`readelf -d failed for ${filePath} with status ${
|
||||
result.status ?? 1
|
||||
}: ${(result.stderr ?? '').trim()}`
|
||||
);
|
||||
}
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
function main() {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
cleanDistNativeOutput();
|
||||
cleanOutput();
|
||||
|
||||
if (targetPlatform !== process.platform) {
|
||||
cleanOutput();
|
||||
if (embeddedMpvRequired) {
|
||||
throw new Error(
|
||||
`Embedded MPV is required for ${targetPlatform}-${targetArch}, but this host is ${process.platform}-${process.arch}.`
|
||||
@@ -743,57 +750,101 @@ function main() {
|
||||
|
||||
assertRequiredLinuxFrameCopyRuntime(runtime);
|
||||
|
||||
const runtimeManifest =
|
||||
targetPlatform === 'darwin'
|
||||
? copyRuntimeToNativeBuild({
|
||||
runtimeLibDir: runtime.libDir,
|
||||
outputLibDir,
|
||||
runtimeOrigin: runtime.origin,
|
||||
runtimeManifest: {
|
||||
targetArch,
|
||||
...runtime.manifest,
|
||||
},
|
||||
})
|
||||
: targetPlatform === 'linux'
|
||||
? writeLinuxFrameCopyBuildManifest(runtime)
|
||||
: copyGenericRuntimeToNativeBuild(runtime);
|
||||
fs.rmSync(unavailableMarkerFile, { force: true });
|
||||
|
||||
const electronPackageJson = require(
|
||||
path.join(workspaceRoot, 'node_modules', 'electron', 'package.json')
|
||||
);
|
||||
const electronVersion = electronPackageJson.version;
|
||||
const env = {
|
||||
...process.env,
|
||||
npm_config_runtime: 'electron',
|
||||
npm_config_target: electronVersion,
|
||||
npm_config_arch: targetArch,
|
||||
npm_config_disturl: 'https://electronjs.org/headers',
|
||||
npm_config_build_from_source: 'true',
|
||||
npm_config_update_binary: 'false',
|
||||
LIBMPV_INCLUDE_DIR: runtime.includeDir,
|
||||
...(targetPlatform === 'linux'
|
||||
? {
|
||||
LINUX_NATIVE_LIBRARY_DIR: runtime.libDir,
|
||||
}
|
||||
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
|
||||
...(runtime.windowsImportLib
|
||||
? {
|
||||
LIBMPV_IMPORT_LIB: path.join(
|
||||
const buildNativeArtifacts = () => {
|
||||
const runtimeManifest =
|
||||
targetPlatform === 'darwin'
|
||||
? copyRuntimeToNativeBuild({
|
||||
runtimeLibDir: runtime.libDir,
|
||||
outputLibDir,
|
||||
path.basename(runtime.windowsImportLib)
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
runtimeOrigin: runtime.origin,
|
||||
runtimeManifest: {
|
||||
targetArch,
|
||||
...runtime.manifest,
|
||||
},
|
||||
})
|
||||
: targetPlatform === 'linux'
|
||||
? writeLinuxFrameCopyBuildManifest(runtime)
|
||||
: copyGenericRuntimeToNativeBuild(runtime);
|
||||
|
||||
log(
|
||||
`Building native addon against Electron ${electronVersion} using ${runtime.origin} runtime for ${targetPlatform}-${targetArch}...`
|
||||
);
|
||||
cleanNativeBuildIntermediates();
|
||||
try {
|
||||
const electronPackageJson = require(
|
||||
path.join(workspaceRoot, 'node_modules', 'electron', 'package.json')
|
||||
);
|
||||
const electronVersion = electronPackageJson.version;
|
||||
const env = {
|
||||
...process.env,
|
||||
npm_config_runtime: 'electron',
|
||||
npm_config_target: electronVersion,
|
||||
npm_config_arch: targetArch,
|
||||
npm_config_disturl: 'https://electronjs.org/headers',
|
||||
npm_config_build_from_source: 'true',
|
||||
npm_config_update_binary: 'false',
|
||||
LIBMPV_INCLUDE_DIR: runtime.includeDir,
|
||||
...(targetPlatform === 'linux'
|
||||
? {
|
||||
LINUX_VERIFIED_RUNTIME_LIBRARY_DIR: outputLibDir,
|
||||
}
|
||||
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
|
||||
...(runtime.windowsImportLib
|
||||
? {
|
||||
LIBMPV_IMPORT_LIB: path.join(
|
||||
outputLibDir,
|
||||
path.basename(runtime.windowsImportLib)
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
log(
|
||||
`Building native addon against Electron ${electronVersion} using ${runtime.origin} runtime for ${targetPlatform}-${targetArch}...`
|
||||
);
|
||||
cleanNativeBuildIntermediates();
|
||||
runNodeGyp('configure', env);
|
||||
runNodeGyp('build', env);
|
||||
|
||||
if (!fs.existsSync(outputFile)) {
|
||||
throw new Error(`Build finished without producing ${outputFile}.`);
|
||||
}
|
||||
|
||||
if (targetPlatform === 'linux') {
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: runtimeManifest.libmpvSoname,
|
||||
outputDir,
|
||||
readDynamicSection: readLinuxDynamicSection,
|
||||
});
|
||||
}
|
||||
|
||||
if (targetPlatform === 'darwin') {
|
||||
patchAddonForBundledRuntime(outputFile, outputLibDir);
|
||||
// The frame-copy helper executable links libmpv too and sits next
|
||||
// to the same lib/ directory, so it gets the identical
|
||||
// dependency-path rewrite + ad-hoc re-sign.
|
||||
const frameHelperFile = path.join(
|
||||
outputDir,
|
||||
'iptvnator_mpv_helper'
|
||||
);
|
||||
if (fs.existsSync(frameHelperFile)) {
|
||||
patchAddonForBundledRuntime(frameHelperFile, outputLibDir);
|
||||
}
|
||||
const forbiddenLinkErrors = validateNoForbiddenRuntimeLinks([
|
||||
outputFile,
|
||||
...(fs.existsSync(frameHelperFile) ? [frameHelperFile] : []),
|
||||
...runtimeManifest.dylibs.map((dylib) =>
|
||||
path.join(outputLibDir, dylib)
|
||||
),
|
||||
]);
|
||||
if (
|
||||
runtime.origin === 'vendored-lgpl' &&
|
||||
forbiddenLinkErrors.length > 0
|
||||
) {
|
||||
throw new Error(forbiddenLinkErrors.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
fs.rmSync(unavailableMarkerFile, { force: true });
|
||||
};
|
||||
|
||||
try {
|
||||
runWithCleanup(buildNativeArtifacts, cleanOutput);
|
||||
} catch (error) {
|
||||
if (!embeddedMpvRequired && runtime.origin === 'system-dev') {
|
||||
// The system-dev fallback triggers on any machine with
|
||||
@@ -805,40 +856,11 @@ function main() {
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
cleanOutput();
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(outputFile)) {
|
||||
throw new Error(`Build finished without producing ${outputFile}.`);
|
||||
}
|
||||
|
||||
if (targetPlatform === 'darwin') {
|
||||
patchAddonForBundledRuntime(outputFile, outputLibDir);
|
||||
// The frame-copy helper executable links libmpv too and sits next to
|
||||
// the same lib/ directory, so it gets the identical dependency-path
|
||||
// rewrite + ad-hoc re-sign.
|
||||
const frameHelperFile = path.join(outputDir, 'iptvnator_mpv_helper');
|
||||
if (fs.existsSync(frameHelperFile)) {
|
||||
patchAddonForBundledRuntime(frameHelperFile, outputLibDir);
|
||||
}
|
||||
const forbiddenLinkErrors = validateNoForbiddenRuntimeLinks([
|
||||
outputFile,
|
||||
...(fs.existsSync(frameHelperFile) ? [frameHelperFile] : []),
|
||||
...runtimeManifest.dylibs.map((dylib) =>
|
||||
path.join(outputLibDir, dylib)
|
||||
),
|
||||
]);
|
||||
if (
|
||||
runtime.origin === 'vendored-lgpl' &&
|
||||
forbiddenLinkErrors.length > 0
|
||||
) {
|
||||
throw new Error(forbiddenLinkErrors.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
log(`Built ${path.relative(workspaceRoot, outputFile)}.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('node:crypto');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
|
||||
const LIBMPV_NEEDED_PATTERN = /^libmpv\.so(?:\..*)?$/;
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
|
||||
const LINUX_FRAME_COPY_ARTIFACTS = Object.freeze([
|
||||
Object.freeze({
|
||||
fileName: 'embedded_mpv.node',
|
||||
label: 'embedded MPV addon',
|
||||
mayLinkLibmpv: false,
|
||||
}),
|
||||
Object.freeze({
|
||||
fileName: 'embedded_mpv_frame_reader.node',
|
||||
label: 'embedded MPV frame reader',
|
||||
mayLinkLibmpv: false,
|
||||
}),
|
||||
Object.freeze({
|
||||
fileName: 'iptvnator_mpv_helper',
|
||||
label: 'embedded MPV frame-copy helper',
|
||||
mayLinkLibmpv: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
function parseReadelfDynamic(output) {
|
||||
if (typeof output !== 'string') {
|
||||
throw new TypeError('readelf dynamic output must be a string.');
|
||||
}
|
||||
|
||||
const dynamic = {
|
||||
needed: [],
|
||||
rpath: [],
|
||||
runpath: [],
|
||||
soname: [],
|
||||
};
|
||||
const dynamicEntryPattern =
|
||||
/\((NEEDED|RPATH|RUNPATH|SONAME)\)[^[]*\[([^\]]*)\]/g;
|
||||
for (const [, tag, value] of output.matchAll(dynamicEntryPattern)) {
|
||||
if (tag === 'NEEDED') {
|
||||
dynamic.needed.push(value);
|
||||
continue;
|
||||
}
|
||||
if (tag === 'SONAME') {
|
||||
dynamic.soname.push(value);
|
||||
continue;
|
||||
}
|
||||
dynamic[tag.toLowerCase()].push(
|
||||
...value.split(':').filter((entry) => entry.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
function exactlyOneRuntimeFile(runtimeFiles, name) {
|
||||
if (!Array.isArray(runtimeFiles)) {
|
||||
throw new Error('Linux runtimeFiles metadata must be an array.');
|
||||
}
|
||||
const matchingRecords = runtimeFiles.filter(
|
||||
(runtimeFile) => runtimeFile?.name === name
|
||||
);
|
||||
if (matchingRecords.length !== 1) {
|
||||
throw new Error(
|
||||
`Linux runtime must contain exactly one exact runtimeFiles record for ${name}.`
|
||||
);
|
||||
}
|
||||
|
||||
const [runtimeFile] = matchingRecords;
|
||||
if (
|
||||
!Number.isInteger(runtimeFile.size) ||
|
||||
runtimeFile.size <= 0 ||
|
||||
typeof runtimeFile.sha256 !== 'string' ||
|
||||
!SHA256_PATTERN.test(runtimeFile.sha256)
|
||||
) {
|
||||
throw new Error(
|
||||
`Linux runtimeFiles record for ${name} has invalid size or SHA-256 metadata.`
|
||||
);
|
||||
}
|
||||
return runtimeFile;
|
||||
}
|
||||
|
||||
function readVerifiedRuntimeFile(filePath, runtimeFile) {
|
||||
let stat;
|
||||
try {
|
||||
stat = fs.lstatSync(filePath);
|
||||
} catch {
|
||||
throw new Error(`Missing copied Linux runtime file: ${filePath}`);
|
||||
}
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`Copied Linux runtime file must be a regular non-symbolic-link file: ${filePath}`
|
||||
);
|
||||
}
|
||||
|
||||
let descriptor;
|
||||
try {
|
||||
descriptor = fs.openSync(
|
||||
filePath,
|
||||
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0)
|
||||
);
|
||||
const descriptorStat = fs.fstatSync(descriptor);
|
||||
if (!descriptorStat.isFile()) {
|
||||
throw new Error(
|
||||
`Copied Linux runtime path is not a regular file: ${filePath}`
|
||||
);
|
||||
}
|
||||
const contents = fs.readFileSync(descriptor);
|
||||
if (contents.byteLength !== runtimeFile.size) {
|
||||
throw new Error(
|
||||
`Size mismatch for copied 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 copied Linux runtime file ${runtimeFile.name}: expected ${runtimeFile.sha256}, received ${actualSha256}.`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (descriptor !== undefined) {
|
||||
fs.closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closureLibMpvSoname(runtimeDependencyClosure) {
|
||||
if (!Array.isArray(runtimeDependencyClosure?.entries)) {
|
||||
throw new Error(
|
||||
'Validated Linux runtime dependency closure entries are required.'
|
||||
);
|
||||
}
|
||||
|
||||
const libMpvEntries = runtimeDependencyClosure.entries.filter(
|
||||
(entry) =>
|
||||
entry?.name === 'libmpv.so' ||
|
||||
VERSIONED_LIBMPV_PATTERN.test(entry?.name)
|
||||
);
|
||||
const declaredSonames = libMpvEntries.map((entry) => entry.soname);
|
||||
const uniqueSonames = new Set(declaredSonames);
|
||||
if (
|
||||
declaredSonames.length === 0 ||
|
||||
declaredSonames.some(
|
||||
(soname) =>
|
||||
typeof soname !== 'string' ||
|
||||
!VERSIONED_LIBMPV_PATTERN.test(soname)
|
||||
) ||
|
||||
uniqueSonames.size !== 1
|
||||
) {
|
||||
throw new Error(
|
||||
'Validated Linux runtime closure must declare exactly one versioned libmpv SONAME.'
|
||||
);
|
||||
}
|
||||
|
||||
return declaredSonames[0];
|
||||
}
|
||||
|
||||
function resolveVerifiedLinuxLibMpvSoname({
|
||||
outputLibDir,
|
||||
runtimeFiles,
|
||||
runtimeDependencyClosure,
|
||||
readDynamicSection,
|
||||
}) {
|
||||
if (typeof readDynamicSection !== 'function') {
|
||||
throw new TypeError('Linux readelf dynamic reader is required.');
|
||||
}
|
||||
|
||||
const expectedSoname = closureLibMpvSoname(runtimeDependencyClosure);
|
||||
const linkerInputRecord = exactlyOneRuntimeFile(runtimeFiles, 'libmpv.so');
|
||||
const exactSonameRecord = exactlyOneRuntimeFile(
|
||||
runtimeFiles,
|
||||
expectedSoname
|
||||
);
|
||||
const linkerInputPath = path.join(outputLibDir, 'libmpv.so');
|
||||
const exactSonamePath = path.join(outputLibDir, expectedSoname);
|
||||
|
||||
readVerifiedRuntimeFile(linkerInputPath, linkerInputRecord);
|
||||
readVerifiedRuntimeFile(exactSonamePath, exactSonameRecord);
|
||||
|
||||
const dynamic = parseReadelfDynamic(readDynamicSection(linkerInputPath));
|
||||
if (
|
||||
dynamic.soname.length !== 1 ||
|
||||
!VERSIONED_LIBMPV_PATTERN.test(dynamic.soname[0])
|
||||
) {
|
||||
throw new Error(
|
||||
'Copied Linux libmpv.so must contain exactly one DT_SONAME with a versioned libmpv basename.'
|
||||
);
|
||||
}
|
||||
if (dynamic.soname[0] !== expectedSoname) {
|
||||
throw new Error(
|
||||
`Copied Linux libmpv.so DT_SONAME ${dynamic.soname[0]} does not match validated closure SONAME ${expectedSoname}.`
|
||||
);
|
||||
}
|
||||
|
||||
return expectedSoname;
|
||||
}
|
||||
|
||||
function assertRegularArtifact(filePath, label) {
|
||||
let stat;
|
||||
try {
|
||||
stat = fs.lstatSync(filePath);
|
||||
} catch {
|
||||
throw new Error(`Missing ${label}: ${filePath}`);
|
||||
}
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`${label} must be a regular non-symbolic-link file: ${filePath}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname,
|
||||
outputDir,
|
||||
readDynamicSection,
|
||||
}) {
|
||||
if (
|
||||
typeof expectedLibmpvSoname !== 'string' ||
|
||||
!VERSIONED_LIBMPV_PATTERN.test(expectedLibmpvSoname)
|
||||
) {
|
||||
throw new Error(
|
||||
'Linux frame-copy linkage validation requires an exact versioned libmpv SONAME.'
|
||||
);
|
||||
}
|
||||
if (typeof readDynamicSection !== 'function') {
|
||||
throw new TypeError('Linux readelf dynamic reader is required.');
|
||||
}
|
||||
|
||||
for (const artifact of LINUX_FRAME_COPY_ARTIFACTS) {
|
||||
const artifactPath = path.join(outputDir, artifact.fileName);
|
||||
assertRegularArtifact(artifactPath, artifact.label);
|
||||
const dynamic = parseReadelfDynamic(readDynamicSection(artifactPath));
|
||||
const libMpvDependencies = dynamic.needed.filter((dependency) =>
|
||||
LIBMPV_NEEDED_PATTERN.test(dependency)
|
||||
);
|
||||
|
||||
if (!artifact.mayLinkLibmpv) {
|
||||
if (libMpvDependencies.length > 0) {
|
||||
throw new Error(
|
||||
`${artifact.label} must not have a direct libmpv DT_NEEDED entry; found ${libMpvDependencies.join(', ')}.`
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
libMpvDependencies.length !== 1 ||
|
||||
libMpvDependencies[0] !== expectedLibmpvSoname
|
||||
) {
|
||||
throw new Error(
|
||||
`${artifact.label} DT_NEEDED must contain exactly ${expectedLibmpvSoname}; found ${
|
||||
libMpvDependencies.join(', ') || '<none>'
|
||||
}.`
|
||||
);
|
||||
}
|
||||
if (dynamic.rpath.length !== 0) {
|
||||
throw new Error(
|
||||
`${artifact.label} must not contain RPATH; found ${dynamic.rpath.join(':')}.`
|
||||
);
|
||||
}
|
||||
if (
|
||||
dynamic.runpath.length !== 1 ||
|
||||
dynamic.runpath[0] !== '$ORIGIN/lib'
|
||||
) {
|
||||
throw new Error(
|
||||
`${artifact.label} RUNPATH must be exactly $ORIGIN/lib; found ${
|
||||
dynamic.runpath.join(':') || '<none>'
|
||||
}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runWithCleanup(operation, cleanup) {
|
||||
try {
|
||||
return operation();
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseReadelfDynamic,
|
||||
resolveVerifiedLinuxLibMpvSoname,
|
||||
runWithCleanup,
|
||||
validateLinuxFrameCopyLinkage,
|
||||
};
|
||||
@@ -162,8 +162,8 @@
|
||||
"-Wl,-rpath,'$$ORIGIN/lib'"
|
||||
],
|
||||
"libraries": [
|
||||
"-L<!(node -p \"process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'\")",
|
||||
"<!(node -e \"const path = require('path'); const dir = process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'; process.stdout.write(path.join(dir, 'libmpv.so'))\")",
|
||||
"-L<!(node -e \"const dir = process.env.LINUX_VERIFIED_RUNTIME_LIBRARY_DIR; if (!dir) { throw new Error('Missing LINUX_VERIFIED_RUNTIME_LIBRARY_DIR'); } process.stdout.write(dir)\")",
|
||||
"<!(node -e \"const path = require('path'); const dir = process.env.LINUX_VERIFIED_RUNTIME_LIBRARY_DIR; if (!dir) { throw new Error('Missing LINUX_VERIFIED_RUNTIME_LIBRARY_DIR'); } process.stdout.write(path.join(dir, 'libmpv.so'))\")",
|
||||
"-lEGL",
|
||||
"-lOpenGL",
|
||||
"-lgbm",
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import { createHash } from 'crypto';
|
||||
import { createRequire } from 'module';
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
|
||||
const linkageModulePath = path.resolve(
|
||||
__dirname,
|
||||
'../../../embedded-mpv-linux-linkage.cjs'
|
||||
);
|
||||
const requireBuildHelper = createRequire(__filename);
|
||||
|
||||
interface RuntimeFileRecord {
|
||||
name: string;
|
||||
size: number;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
interface SonameFixture {
|
||||
exactPath: string;
|
||||
outputLibDir: string;
|
||||
runtimeDependencyClosure: {
|
||||
entries: Array<{
|
||||
name: string;
|
||||
needed: string[];
|
||||
rpath: string[];
|
||||
runpath: string[];
|
||||
soname: string | null;
|
||||
}>;
|
||||
};
|
||||
runtimeFiles: RuntimeFileRecord[];
|
||||
}
|
||||
|
||||
function loadLinkageModule(): {
|
||||
parseReadelfDynamic: (output: string) => {
|
||||
needed: string[];
|
||||
rpath: string[];
|
||||
runpath: string[];
|
||||
soname: string[];
|
||||
};
|
||||
resolveVerifiedLinuxLibMpvSoname: (options: {
|
||||
outputLibDir: string;
|
||||
readDynamicSection: (filePath: string) => string;
|
||||
runtimeDependencyClosure: SonameFixture['runtimeDependencyClosure'];
|
||||
runtimeFiles: RuntimeFileRecord[];
|
||||
}) => string;
|
||||
runWithCleanup: <T>(operation: () => T, cleanup: () => void) => T;
|
||||
validateLinuxFrameCopyLinkage: (options: {
|
||||
expectedLibmpvSoname: string;
|
||||
outputDir: string;
|
||||
readDynamicSection: (filePath: string) => string;
|
||||
}) => void;
|
||||
} {
|
||||
expect(existsSync(linkageModulePath)).toBe(true);
|
||||
return requireBuildHelper(linkageModulePath);
|
||||
}
|
||||
|
||||
function sha256(contents: Buffer): string {
|
||||
return createHash('sha256').update(contents).digest('hex');
|
||||
}
|
||||
|
||||
function runtimeFile(name: string, contents: Buffer): RuntimeFileRecord {
|
||||
return {
|
||||
name,
|
||||
size: contents.byteLength,
|
||||
sha256: sha256(contents),
|
||||
};
|
||||
}
|
||||
|
||||
function readelfDynamic(
|
||||
entries: Array<['NEEDED' | 'RPATH' | 'RUNPATH' | 'SONAME', string]>
|
||||
): string {
|
||||
return entries
|
||||
.map(
|
||||
([tag, value], index) =>
|
||||
` 0x${index
|
||||
.toString(16)
|
||||
.padStart(16, '0')} (${tag}) Library value: [${value}]`
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
describe('Linux Embedded MPV linkage verification', () => {
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function temporaryDirectory(): string {
|
||||
const directory = mkdtempSync(
|
||||
path.join(tmpdir(), 'iptvnator-mpv-linkage-')
|
||||
);
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
function createSonameFixture(soname = 'libmpv.so.2'): SonameFixture {
|
||||
const outputLibDir = path.join(temporaryDirectory(), 'lib');
|
||||
mkdirSync(outputLibDir, { recursive: true });
|
||||
const contents = Buffer.from('verified libmpv ELF contents');
|
||||
const aliasPath = path.join(outputLibDir, 'libmpv.so');
|
||||
const exactPath = path.join(outputLibDir, soname);
|
||||
writeFileSync(aliasPath, contents);
|
||||
writeFileSync(exactPath, contents);
|
||||
|
||||
return {
|
||||
exactPath,
|
||||
outputLibDir,
|
||||
runtimeFiles: [
|
||||
runtimeFile('libmpv.so', contents),
|
||||
runtimeFile(soname, contents),
|
||||
],
|
||||
runtimeDependencyClosure: {
|
||||
entries: [
|
||||
{
|
||||
name: 'libmpv.so',
|
||||
needed: [],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
soname,
|
||||
},
|
||||
{
|
||||
name: soname,
|
||||
needed: [],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
soname,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it('parses every dynamic tag without hiding duplicate SONAME entries', () => {
|
||||
const { parseReadelfDynamic } = loadLinkageModule();
|
||||
|
||||
expect(
|
||||
parseReadelfDynamic(
|
||||
readelfDynamic([
|
||||
['NEEDED', 'libmpv.so.2'],
|
||||
['RPATH', '/forbidden'],
|
||||
['RUNPATH', '$ORIGIN/lib'],
|
||||
['SONAME', 'libmpv.so.2'],
|
||||
['SONAME', 'libmpv.so.3'],
|
||||
])
|
||||
)
|
||||
).toEqual({
|
||||
needed: ['libmpv.so.2'],
|
||||
rpath: ['/forbidden'],
|
||||
runpath: ['$ORIGIN/lib'],
|
||||
soname: ['libmpv.so.2', 'libmpv.so.3'],
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the exact libmpv SONAME from closure metadata and verified copied files', () => {
|
||||
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
|
||||
const fixture = createSonameFixture();
|
||||
|
||||
expect(
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...fixture,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([['SONAME', 'libmpv.so.2']]),
|
||||
})
|
||||
).toBe('libmpv.so.2');
|
||||
});
|
||||
|
||||
it('rejects missing and ambiguous closure SONAME metadata', () => {
|
||||
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
|
||||
const missingFixture = createSonameFixture();
|
||||
for (const entry of missingFixture.runtimeDependencyClosure.entries) {
|
||||
entry.soname = null;
|
||||
}
|
||||
|
||||
expect(() =>
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...missingFixture,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([['SONAME', 'libmpv.so.2']]),
|
||||
})
|
||||
).toThrow(/exactly one versioned libmpv SONAME/i);
|
||||
|
||||
const ambiguousFixture = createSonameFixture();
|
||||
ambiguousFixture.runtimeDependencyClosure.entries.push({
|
||||
name: 'libmpv.so.3',
|
||||
needed: [],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
soname: 'libmpv.so.3',
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...ambiguousFixture,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([['SONAME', 'libmpv.so.2']]),
|
||||
})
|
||||
).toThrow(/exactly one versioned libmpv SONAME/i);
|
||||
});
|
||||
|
||||
it('rejects missing, ambiguous, and mismatched DT_SONAME values', () => {
|
||||
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
|
||||
const fixture = createSonameFixture();
|
||||
|
||||
expect(() =>
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...fixture,
|
||||
readDynamicSection: () => readelfDynamic([]),
|
||||
})
|
||||
).toThrow(/exactly one DT_SONAME/i);
|
||||
expect(() =>
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...fixture,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([
|
||||
['SONAME', 'libmpv.so.2'],
|
||||
['SONAME', 'libmpv.so.3'],
|
||||
]),
|
||||
})
|
||||
).toThrow(/exactly one DT_SONAME/i);
|
||||
expect(() =>
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...fixture,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([['SONAME', 'libmpv.so.3']]),
|
||||
})
|
||||
).toThrow(/does not match validated closure SONAME/i);
|
||||
});
|
||||
|
||||
(process.platform === 'win32' ? it.skip : it)(
|
||||
'rejects a symlinked copied linker input',
|
||||
() => {
|
||||
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
|
||||
const fixture = createSonameFixture();
|
||||
const aliasPath = path.join(fixture.outputLibDir, 'libmpv.so');
|
||||
unlinkSync(aliasPath);
|
||||
symlinkSync(path.basename(fixture.exactPath), aliasPath);
|
||||
|
||||
expect(() =>
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...fixture,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([['SONAME', 'libmpv.so.2']]),
|
||||
})
|
||||
).toThrow(/must be a regular non-symbolic-link file/i);
|
||||
}
|
||||
);
|
||||
|
||||
it('rejects a missing exact runtime record and a mismatched exact file hash', () => {
|
||||
const { resolveVerifiedLinuxLibMpvSoname } = loadLinkageModule();
|
||||
const missingRecordFixture = createSonameFixture();
|
||||
|
||||
expect(() =>
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...missingRecordFixture,
|
||||
runtimeFiles: missingRecordFixture.runtimeFiles.filter(
|
||||
({ name }) => name !== 'libmpv.so.2'
|
||||
),
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([['SONAME', 'libmpv.so.2']]),
|
||||
})
|
||||
).toThrow(/exact runtimeFiles record/i);
|
||||
|
||||
const mismatchedFileFixture = createSonameFixture();
|
||||
writeFileSync(
|
||||
mismatchedFileFixture.exactPath,
|
||||
Buffer.from('tampered exact SONAME file')
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveVerifiedLinuxLibMpvSoname({
|
||||
...mismatchedFileFixture,
|
||||
readDynamicSection: () =>
|
||||
readelfDynamic([['SONAME', 'libmpv.so.2']]),
|
||||
})
|
||||
).toThrow(/size|SHA-256/i);
|
||||
});
|
||||
|
||||
function createArtifactFixture(): {
|
||||
outputDir: string;
|
||||
readDynamicSection: (filePath: string) => string;
|
||||
outputs: Record<string, string>;
|
||||
} {
|
||||
const outputDir = temporaryDirectory();
|
||||
const outputs: Record<string, string> = {
|
||||
'embedded_mpv.node': readelfDynamic([['NEEDED', 'libX11.so.6']]),
|
||||
'embedded_mpv_frame_reader.node': readelfDynamic([]),
|
||||
iptvnator_mpv_helper: readelfDynamic([
|
||||
['NEEDED', 'libmpv.so.2'],
|
||||
['NEEDED', 'libEGL.so.1'],
|
||||
['RUNPATH', '$ORIGIN/lib'],
|
||||
]),
|
||||
};
|
||||
for (const artifact of Object.keys(outputs)) {
|
||||
writeFileSync(path.join(outputDir, artifact), 'ELF');
|
||||
}
|
||||
return {
|
||||
outputDir,
|
||||
outputs,
|
||||
readDynamicSection: (filePath: string) =>
|
||||
outputs[path.basename(filePath)],
|
||||
};
|
||||
}
|
||||
|
||||
it('accepts only process-isolated Linux frame-copy linkage', () => {
|
||||
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
|
||||
const fixture = createArtifactFixture();
|
||||
|
||||
expect(() =>
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: 'libmpv.so.2',
|
||||
outputDir: fixture.outputDir,
|
||||
readDynamicSection: fixture.readDynamicSection,
|
||||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('rejects a helper linked to the wrong libmpv SONAME', () => {
|
||||
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
|
||||
const fixture = createArtifactFixture();
|
||||
|
||||
fixture.outputs.iptvnator_mpv_helper = readelfDynamic([
|
||||
['NEEDED', 'libmpv.so.3'],
|
||||
['RUNPATH', '$ORIGIN/lib'],
|
||||
]);
|
||||
|
||||
expect(() =>
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: 'libmpv.so.2',
|
||||
outputDir: fixture.outputDir,
|
||||
readDynamicSection: fixture.readDynamicSection,
|
||||
})
|
||||
).toThrow(/helper.*DT_NEEDED must contain exactly libmpv\.so\.2/i);
|
||||
});
|
||||
|
||||
it('rejects helper RPATH and any RUNPATH other than $ORIGIN/lib', () => {
|
||||
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
|
||||
const rpathFixture = createArtifactFixture();
|
||||
rpathFixture.outputs.iptvnator_mpv_helper = readelfDynamic([
|
||||
['NEEDED', 'libmpv.so.2'],
|
||||
['RPATH', '/host/lib'],
|
||||
['RUNPATH', '$ORIGIN/lib'],
|
||||
]);
|
||||
|
||||
expect(() =>
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: 'libmpv.so.2',
|
||||
outputDir: rpathFixture.outputDir,
|
||||
readDynamicSection: rpathFixture.readDynamicSection,
|
||||
})
|
||||
).toThrow(/helper must not contain RPATH/i);
|
||||
|
||||
const runpathFixture = createArtifactFixture();
|
||||
runpathFixture.outputs.iptvnator_mpv_helper = readelfDynamic([
|
||||
['NEEDED', 'libmpv.so.2'],
|
||||
['RUNPATH', '$ORIGIN'],
|
||||
]);
|
||||
|
||||
expect(() =>
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: 'libmpv.so.2',
|
||||
outputDir: runpathFixture.outputDir,
|
||||
readDynamicSection: runpathFixture.readDynamicSection,
|
||||
})
|
||||
).toThrow(/helper RUNPATH must be exactly \$ORIGIN\/lib/i);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['embedded_mpv.node', 'addon'],
|
||||
['embedded_mpv_frame_reader.node', 'frame reader'],
|
||||
])('rejects Electron-side libmpv linkage from %s', (fileName, label) => {
|
||||
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
|
||||
const fixture = createArtifactFixture();
|
||||
fixture.outputs[fileName] = readelfDynamic([['NEEDED', 'libmpv.so.2']]);
|
||||
|
||||
expect(() =>
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: 'libmpv.so.2',
|
||||
outputDir: fixture.outputDir,
|
||||
readDynamicSection: fixture.readDynamicSection,
|
||||
})
|
||||
).toThrow(new RegExp(`${label} must not have a direct libmpv`, 'i'));
|
||||
});
|
||||
|
||||
it('rejects missing artifacts and readelf failures', () => {
|
||||
const { validateLinuxFrameCopyLinkage } = loadLinkageModule();
|
||||
const missingArtifactFixture = createArtifactFixture();
|
||||
unlinkSync(
|
||||
path.join(
|
||||
missingArtifactFixture.outputDir,
|
||||
'embedded_mpv_frame_reader.node'
|
||||
)
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: 'libmpv.so.2',
|
||||
outputDir: missingArtifactFixture.outputDir,
|
||||
readDynamicSection: missingArtifactFixture.readDynamicSection,
|
||||
})
|
||||
).toThrow(/missing.*frame reader/i);
|
||||
|
||||
const readelfFailureFixture = createArtifactFixture();
|
||||
expect(() =>
|
||||
validateLinuxFrameCopyLinkage({
|
||||
expectedLibmpvSoname: 'libmpv.so.2',
|
||||
outputDir: readelfFailureFixture.outputDir,
|
||||
readDynamicSection: () => {
|
||||
throw new Error('readelf is unavailable');
|
||||
},
|
||||
})
|
||||
).toThrow(/readelf is unavailable/);
|
||||
});
|
||||
|
||||
it('runs cleanup before rethrowing the original transaction failure', () => {
|
||||
const { runWithCleanup } = loadLinkageModule();
|
||||
const calls: string[] = [];
|
||||
const failure = new Error('post-link validation failed');
|
||||
|
||||
expect(() =>
|
||||
runWithCleanup(
|
||||
() => {
|
||||
calls.push('operation');
|
||||
throw failure;
|
||||
},
|
||||
() => calls.push('cleanup')
|
||||
)
|
||||
).toThrow(failure);
|
||||
expect(calls).toEqual(['operation', 'cleanup']);
|
||||
});
|
||||
});
|
||||
@@ -590,11 +590,14 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
'SHA-256 mismatch for staged Linux runtime file'
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'LINUX_NATIVE_LIBRARY_DIR: runtime.libDir'
|
||||
'LINUX_VERIFIED_RUNTIME_LIBRARY_DIR: outputLibDir'
|
||||
);
|
||||
expect(buildScriptSource).not.toContain(
|
||||
'process.env.LINUX_NATIVE_LIBRARY_DIR || runtime.libDir'
|
||||
);
|
||||
expect(buildScriptSource).not.toContain(
|
||||
'LINUX_NATIVE_LIBRARY_DIR: runtime.libDir'
|
||||
);
|
||||
});
|
||||
|
||||
it('writes a profile-neutral Linux frame-copy build manifest', () => {
|
||||
@@ -636,15 +639,15 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('derives packaged Linux libmpv identity from the validated runtime', () => {
|
||||
it('derives packaged Linux libmpv identity from validated closure SONAME metadata', () => {
|
||||
expect(buildScriptSource).toContain('resolveVerifiedLinuxLibMpvSoname');
|
||||
expect(buildScriptSource).toContain(
|
||||
'function deriveLinuxLibMpvSoname(runtime)'
|
||||
'runtime.sourceRuntimeManifest.runtimeDependencyClosure'
|
||||
);
|
||||
expect(buildScriptSource).toContain(
|
||||
'runtime.sourceRuntimeManifest.runtimeFiles'
|
||||
);
|
||||
expect(buildScriptSource).toContain('VERSIONED_LINUX_LIBMPV_PATTERN');
|
||||
expect(buildScriptSource).toContain('libmpvSoname,');
|
||||
expect(buildScriptSource).not.toContain(
|
||||
'VERSIONED_LINUX_LIBMPV_PATTERN'
|
||||
);
|
||||
expect(buildScriptSource).not.toContain("libmpvSoname: 'libmpv.so.2'");
|
||||
});
|
||||
|
||||
@@ -659,6 +662,24 @@ describe('Embedded MPV native source recording invariants', () => {
|
||||
expect(buildScriptSource).toContain('bundled: packageRuntimeAvailable');
|
||||
});
|
||||
|
||||
it('validates Linux post-link isolation before marking the build available', () => {
|
||||
const postLinkValidation = buildScriptSource.indexOf(
|
||||
'validateLinuxFrameCopyLinkage({'
|
||||
);
|
||||
const availabilityMarkerRemoval = buildScriptSource.lastIndexOf(
|
||||
'fs.rmSync(unavailableMarkerFile'
|
||||
);
|
||||
|
||||
expect(buildScriptSource).toContain(
|
||||
"spawnSync('readelf', ['-d', filePath]"
|
||||
);
|
||||
expect(postLinkValidation).toBeGreaterThanOrEqual(0);
|
||||
expect(availabilityMarkerRemoval).toBeGreaterThan(postLinkValidation);
|
||||
expect(buildScriptSource).toContain(
|
||||
'runWithCleanup(buildNativeArtifacts, cleanOutput)'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses platform-specific embedded MPV runtime cache key inputs in CI', () => {
|
||||
expect(buildAndMakeWorkflowSource).toContain(
|
||||
"const targetPlatform = '${{ matrix.embedded_mpv_platform }}';"
|
||||
@@ -907,7 +928,13 @@ describe('Embedded MPV native build configuration', () => {
|
||||
linuxHelperConfig?.libraries.find((library: string) =>
|
||||
library.includes("path.join(dir, 'libmpv.so')")
|
||||
)
|
||||
).toContain('LINUX_NATIVE_LIBRARY_DIR');
|
||||
).toContain('LINUX_VERIFIED_RUNTIME_LIBRARY_DIR');
|
||||
expect(JSON.stringify(linuxHelperConfig)).not.toContain(
|
||||
"LINUX_VERIFIED_RUNTIME_LIBRARY_DIR || '/usr/lib'"
|
||||
);
|
||||
expect(JSON.stringify(linuxHelperConfig)).toContain(
|
||||
'Missing LINUX_VERIFIED_RUNTIME_LIBRARY_DIR'
|
||||
);
|
||||
|
||||
const runtimeLinkerFlags = linuxHelperConfig?.ldflags.filter(
|
||||
(flag: string) => flag.startsWith('-Wl,')
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node10",
|
||||
"types": ["jest", "node"]
|
||||
|
||||
Reference in new issue
Block a user