mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(embedded-mpv): make Linux runtime portable
This commit is contained in:
1 parent
84ff8de04a
commit
904fc6d3ec
5 files changed
+1584
-69
No files matched your search
@@ -94,6 +94,16 @@ const SOURCE_PACKAGES = Object.freeze(
|
||||
expectedGitCommit: 'cee9b076f2c63104ccfd497fa79c39a867293ec4',
|
||||
license: 'LGPL-2.1-or-later',
|
||||
},
|
||||
{
|
||||
id: 'libdisplay-info',
|
||||
version: '0.1.1',
|
||||
sourceKind: 'archive',
|
||||
sourceUrl:
|
||||
'https://gitlab.freedesktop.org/emersion/libdisplay-info/-/releases/0.1.1/downloads/libdisplay-info-0.1.1.tar.xz',
|
||||
expectedSha256:
|
||||
'0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60',
|
||||
license: 'MIT',
|
||||
},
|
||||
{
|
||||
id: 'mpv',
|
||||
version: '0.41.0',
|
||||
@@ -203,7 +213,7 @@ const MPV_MESON_FLAGS = Object.freeze([
|
||||
'-Dd3d11=disabled',
|
||||
'-Ddirect3d=disabled',
|
||||
'-Ddmabuf-wayland=disabled',
|
||||
'-Ddrm=disabled',
|
||||
'-Ddrm=enabled',
|
||||
'-Degl=enabled',
|
||||
'-Degl-android=disabled',
|
||||
'-Degl-angle=disabled',
|
||||
@@ -318,6 +328,12 @@ const BUILD_RECIPES = Object.freeze({
|
||||
'-Dtools=disabled',
|
||||
'-Dcache-build=disabled',
|
||||
'-Dnls=disabled',
|
||||
'-Dxml-backend=expat',
|
||||
'-Dbaseconfig-dir=/etc/fonts',
|
||||
'-Dconfig-dir=/etc/fonts/conf.d',
|
||||
'-Dtemplate-dir=/usr/share/fontconfig/conf.avail',
|
||||
'-Dcache-dir=/var/cache/fontconfig',
|
||||
'-Dxml-dir=/usr/share/xml/fontconfig',
|
||||
]),
|
||||
}),
|
||||
libass: Object.freeze({
|
||||
@@ -344,6 +360,7 @@ const BUILD_RECIPES = Object.freeze({
|
||||
'no-legacy',
|
||||
'no-module',
|
||||
'no-weak-ssl-ciphers',
|
||||
'--openssldir=/etc/ssl',
|
||||
]),
|
||||
}),
|
||||
ffmpeg: Object.freeze({
|
||||
@@ -371,6 +388,11 @@ const BUILD_RECIPES = Object.freeze({
|
||||
'-Dxxhash=disabled',
|
||||
]),
|
||||
}),
|
||||
'libdisplay-info': Object.freeze({
|
||||
buildSystem: 'meson',
|
||||
sharedOnly: true,
|
||||
args: Object.freeze([]),
|
||||
}),
|
||||
mpv: Object.freeze({
|
||||
buildSystem: 'meson',
|
||||
sharedOnly: true,
|
||||
@@ -385,6 +407,7 @@ const REQUIRED_TOOLS = Object.freeze([
|
||||
'git',
|
||||
'make',
|
||||
'meson',
|
||||
'nasm',
|
||||
'ninja',
|
||||
'patchelf',
|
||||
'perl',
|
||||
@@ -394,6 +417,23 @@ const REQUIRED_TOOLS = Object.freeze([
|
||||
'tar',
|
||||
]);
|
||||
|
||||
const MINIMUM_TOOL_VERSIONS = Object.freeze({
|
||||
cc: '9.0.0',
|
||||
cmake: '3.16.0',
|
||||
curl: '7.71.0',
|
||||
git: '2.30.0',
|
||||
make: '4.0.0',
|
||||
meson: '1.6.0',
|
||||
nasm: '2.15.05',
|
||||
ninja: '1.10.0',
|
||||
patchelf: '0.14.0',
|
||||
perl: '5.30.0',
|
||||
'pkg-config': '0.29.0',
|
||||
python3: '3.8.0',
|
||||
readelf: '2.35.0',
|
||||
tar: '1.30.0',
|
||||
});
|
||||
|
||||
const DEFAULT_SYSTEM_PKG_CONFIG_DIRS = Object.freeze([
|
||||
'/usr/lib/x86_64-linux-gnu/pkgconfig',
|
||||
'/usr/lib64/pkgconfig',
|
||||
@@ -406,6 +446,7 @@ const EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES = Object.freeze([
|
||||
'egl',
|
||||
'gbm',
|
||||
'gl',
|
||||
'libdrm',
|
||||
'libpulse',
|
||||
'libva',
|
||||
'libva-drm',
|
||||
@@ -477,6 +518,31 @@ const EXTERNAL_SYSTEM_LIBRARIES = Object.freeze(
|
||||
].map((externalLibrary) => Object.freeze(externalLibrary))
|
||||
);
|
||||
|
||||
const RUNTIME_EXTERNAL_CONFIGURATION = Object.freeze({
|
||||
fontconfig: Object.freeze({
|
||||
configDirectory: '/etc/fonts',
|
||||
templateDirectory: '/usr/share/fontconfig',
|
||||
cacheDirectory: '/var/cache/fontconfig',
|
||||
ownership: 'system',
|
||||
}),
|
||||
openssl: Object.freeze({
|
||||
configFile: '/etc/ssl/openssl.cnf',
|
||||
certificateFile: '/etc/ssl/cert.pem',
|
||||
certificateDirectory: '/etc/ssl/certs',
|
||||
ownership: 'system',
|
||||
}),
|
||||
});
|
||||
|
||||
const OUTPUT_OWNERSHIP_MARKER = '.iptvnator-linux-runtime-owner';
|
||||
const OUTPUT_OWNERSHIP_MARKER_CONTENT =
|
||||
'iptvnator-embedded-mpv-linux-runtime-v1\n';
|
||||
|
||||
const PORTABLE_ABI_BASELINE = Object.freeze({
|
||||
distribution: 'Ubuntu 22.04',
|
||||
glibcMaximum: '2.35',
|
||||
glibcxxMaximum: '3.4.30',
|
||||
});
|
||||
|
||||
const SHARED_LIBRARY_PATTERN = /\.so(?:\.\d+)*$/;
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const externalSystemLibraryNames = new Set(
|
||||
@@ -503,6 +569,217 @@ function assertGitCommitMatchesPin(sourcePackage, actualGitCommit) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseVersion(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const match = value.match(/\b(\d+\.\d+(?:\.\d+)*)\b/);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function compareVersions(left, right) {
|
||||
const leftParts = left.split('.').map(Number);
|
||||
const rightParts = right.split('.').map(Number);
|
||||
const length = Math.max(leftParts.length, rightParts.length);
|
||||
for (let index = 0; index < length; index += 1) {
|
||||
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
||||
if (difference !== 0) {
|
||||
return Math.sign(difference);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function assertMinimumToolVersions(toolVersions) {
|
||||
for (const tool of REQUIRED_TOOLS) {
|
||||
const declaredVersion = toolVersions?.[tool];
|
||||
if (typeof declaredVersion !== 'string' || !declaredVersion.trim()) {
|
||||
throw new Error(`Missing required tool version for ${tool}.`);
|
||||
}
|
||||
const actualVersion = parseVersion(declaredVersion);
|
||||
if (!actualVersion) {
|
||||
throw new Error(
|
||||
`Unable to parse required tool version for ${tool}: ${declaredVersion}.`
|
||||
);
|
||||
}
|
||||
const minimumVersion = MINIMUM_TOOL_VERSIONS[tool];
|
||||
if (compareVersions(actualVersion, minimumVersion) < 0) {
|
||||
throw new Error(
|
||||
`${tool} ${actualVersion} is unsupported; ${tool} requires ${minimumVersion} or newer for Linux runtime builds.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertUniqueMesonOptionAssignments(buildRecipes) {
|
||||
if (!buildRecipes || typeof buildRecipes !== 'object') {
|
||||
throw new TypeError('Linux runtime build recipes must be an object.');
|
||||
}
|
||||
for (const [packageId, recipe] of Object.entries(buildRecipes)) {
|
||||
if (recipe?.buildSystem !== 'meson') {
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(recipe.args)) {
|
||||
throw new Error(
|
||||
`${packageId} Meson recipe must declare an argument array.`
|
||||
);
|
||||
}
|
||||
|
||||
const assignmentsByOption = new Map();
|
||||
for (const flag of recipe.args) {
|
||||
const match =
|
||||
typeof flag === 'string' ? flag.match(/^(-D[^=]+)=/) : null;
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const option = match[1];
|
||||
const assignmentCount = (assignmentsByOption.get(option) ?? 0) + 1;
|
||||
assignmentsByOption.set(option, assignmentCount);
|
||||
if (assignmentCount > 1) {
|
||||
throw new Error(
|
||||
`${packageId} Meson recipe must assign ${option} exactly once.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lstatIfExists(fileSystem, filePath) {
|
||||
try {
|
||||
return fileSystem.lstatSync(filePath);
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function assertOwnedOutputDestination(outputPrefix, fileSystem = fs) {
|
||||
const outputStat = lstatIfExists(fileSystem, outputPrefix);
|
||||
if (!outputStat) {
|
||||
return;
|
||||
}
|
||||
if (!outputStat.isDirectory() || outputStat.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`Existing output ${outputPrefix} must be a non-symbolic-link directory carrying the IPTVnator ownership marker.`
|
||||
);
|
||||
}
|
||||
|
||||
const markerPath = path.join(outputPrefix, OUTPUT_OWNERSHIP_MARKER);
|
||||
const markerStat = lstatIfExists(fileSystem, markerPath);
|
||||
if (
|
||||
!markerStat ||
|
||||
!markerStat.isFile() ||
|
||||
markerStat.isSymbolicLink() ||
|
||||
fileSystem.readFileSync(markerPath, 'utf8') !==
|
||||
OUTPUT_OWNERSHIP_MARKER_CONTENT
|
||||
) {
|
||||
throw new Error(
|
||||
`Existing output ${outputPrefix} is missing the valid IPTVnator ownership marker.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ownedStagingPrefixPath(outputPrefix, token) {
|
||||
return path.join(
|
||||
path.dirname(outputPrefix),
|
||||
`.${path.basename(outputPrefix)}.iptvnator-stage-${token}`
|
||||
);
|
||||
}
|
||||
|
||||
function createOwnedStagingPrefix(
|
||||
outputPrefix,
|
||||
{ fileSystem = fs, token = crypto.randomBytes(8).toString('hex') } = {}
|
||||
) {
|
||||
assertOwnedOutputDestination(outputPrefix, fileSystem);
|
||||
const outputParent = path.dirname(outputPrefix);
|
||||
const stagingPrefix = ownedStagingPrefixPath(outputPrefix, token);
|
||||
if (lstatIfExists(fileSystem, stagingPrefix)) {
|
||||
throw new Error(
|
||||
`Refusing to reuse existing Linux runtime staging path ${stagingPrefix}.`
|
||||
);
|
||||
}
|
||||
|
||||
fileSystem.mkdirSync(outputParent, { recursive: true });
|
||||
fileSystem.mkdirSync(stagingPrefix);
|
||||
try {
|
||||
fileSystem.writeFileSync(
|
||||
path.join(stagingPrefix, OUTPUT_OWNERSHIP_MARKER),
|
||||
OUTPUT_OWNERSHIP_MARKER_CONTENT,
|
||||
{ mode: 0o644 }
|
||||
);
|
||||
} catch (error) {
|
||||
fileSystem.rmSync(stagingPrefix, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
return stagingPrefix;
|
||||
}
|
||||
|
||||
function publishOwnedOutput({
|
||||
outputPrefix,
|
||||
stagingPrefix,
|
||||
fileSystem = fs,
|
||||
token = crypto.randomBytes(8).toString('hex'),
|
||||
}) {
|
||||
assertOwnedOutputDestination(outputPrefix, fileSystem);
|
||||
assertOwnedOutputDestination(stagingPrefix, fileSystem);
|
||||
if (!lstatIfExists(fileSystem, stagingPrefix)) {
|
||||
throw new Error(
|
||||
`Linux runtime staging prefix does not exist: ${stagingPrefix}.`
|
||||
);
|
||||
}
|
||||
|
||||
const backupPrefix = path.join(
|
||||
path.dirname(outputPrefix),
|
||||
`.${path.basename(outputPrefix)}.iptvnator-backup-${token}`
|
||||
);
|
||||
if (lstatIfExists(fileSystem, backupPrefix)) {
|
||||
throw new Error(
|
||||
`Refusing to reuse existing Linux runtime backup path ${backupPrefix}.`
|
||||
);
|
||||
}
|
||||
|
||||
let movedPreviousOutput = false;
|
||||
let published = false;
|
||||
try {
|
||||
if (lstatIfExists(fileSystem, outputPrefix)) {
|
||||
fileSystem.renameSync(outputPrefix, backupPrefix);
|
||||
movedPreviousOutput = true;
|
||||
}
|
||||
fileSystem.renameSync(stagingPrefix, outputPrefix);
|
||||
published = true;
|
||||
if (movedPreviousOutput) {
|
||||
fileSystem.rmSync(backupPrefix, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
movedPreviousOutput &&
|
||||
!lstatIfExists(fileSystem, outputPrefix) &&
|
||||
lstatIfExists(fileSystem, backupPrefix)
|
||||
) {
|
||||
fileSystem.renameSync(backupPrefix, outputPrefix);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (lstatIfExists(fileSystem, stagingPrefix)) {
|
||||
fileSystem.rmSync(stagingPrefix, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
if (published && lstatIfExists(fileSystem, backupPrefix)) {
|
||||
fileSystem.rmSync(backupPrefix, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function joinEnvironmentParts(parts, separator = ' ') {
|
||||
return parts.filter((value) => value && value.trim()).join(separator);
|
||||
}
|
||||
@@ -625,9 +902,12 @@ function assertPathInside(parentPath, candidatePath, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function materializeLibrarySymlinks(libDir) {
|
||||
function materializeLibrarySymlinks(libDir, selectedNames = null) {
|
||||
const realLibDir = fs.realpathSync(libDir);
|
||||
for (const name of runtimeLibraryNames(libDir)) {
|
||||
if (selectedNames && !selectedNames.has(name)) {
|
||||
continue;
|
||||
}
|
||||
const libraryPath = path.join(libDir, name);
|
||||
const stat = fs.lstatSync(libraryPath);
|
||||
if (!stat.isSymbolicLink()) {
|
||||
@@ -654,6 +934,109 @@ function materializeLibrarySymlinks(libDir) {
|
||||
}
|
||||
}
|
||||
|
||||
function selectReachableRuntimeLibraryNames(entries) {
|
||||
if (!Array.isArray(entries)) {
|
||||
throw new TypeError('Runtime dynamic entries must be an array.');
|
||||
}
|
||||
|
||||
const entriesByName = new Map();
|
||||
for (const entry of entries) {
|
||||
if (
|
||||
!entry ||
|
||||
typeof entry.name !== 'string' ||
|
||||
!SHARED_LIBRARY_PATTERN.test(entry.name)
|
||||
) {
|
||||
throw new Error(
|
||||
'Runtime dynamic entry has an invalid library name.'
|
||||
);
|
||||
}
|
||||
if (entriesByName.has(entry.name)) {
|
||||
throw new Error(
|
||||
`Runtime dynamic entries contain duplicate library ${entry.name}.`
|
||||
);
|
||||
}
|
||||
entriesByName.set(entry.name, entry);
|
||||
}
|
||||
|
||||
const linkerAlias = entriesByName.get('libmpv.so');
|
||||
if (!linkerAlias) {
|
||||
throw new Error(
|
||||
'Linux runtime must contain the libmpv.so linker alias.'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof linkerAlias.soname !== 'string' ||
|
||||
!SHARED_LIBRARY_PATTERN.test(linkerAlias.soname) ||
|
||||
!entriesByName.has(linkerAlias.soname)
|
||||
) {
|
||||
throw new Error(
|
||||
'libmpv.so must declare a bundled SONAME before runtime pruning.'
|
||||
);
|
||||
}
|
||||
|
||||
const reachableNames = new Set(['libmpv.so']);
|
||||
const pendingNames = [linkerAlias.soname];
|
||||
while (pendingNames.length > 0) {
|
||||
const libraryName = pendingNames.shift();
|
||||
if (reachableNames.has(libraryName)) {
|
||||
continue;
|
||||
}
|
||||
reachableNames.add(libraryName);
|
||||
const entry = entriesByName.get(libraryName);
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
`Reachable runtime library ${libraryName} is missing its dynamic entry.`
|
||||
);
|
||||
}
|
||||
for (const neededName of entry.needed ?? []) {
|
||||
if (
|
||||
entriesByName.has(neededName) &&
|
||||
!reachableNames.has(neededName)
|
||||
) {
|
||||
pendingNames.push(neededName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...reachableNames].sort();
|
||||
}
|
||||
|
||||
function retainRuntimeLibraries(libDir, retainedNames) {
|
||||
if (!Array.isArray(retainedNames) || retainedNames.length === 0) {
|
||||
throw new Error('Runtime retention list must be a non-empty array.');
|
||||
}
|
||||
const retainedNameSet = new Set(retainedNames);
|
||||
if (retainedNameSet.size !== retainedNames.length) {
|
||||
throw new Error('Runtime retention list contains duplicate libraries.');
|
||||
}
|
||||
|
||||
const availableNames = runtimeLibraryNames(libDir);
|
||||
for (const retainedName of retainedNameSet) {
|
||||
if (!availableNames.includes(retainedName)) {
|
||||
throw new Error(
|
||||
`Retained runtime library does not exist: ${retainedName}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
materializeLibrarySymlinks(libDir, retainedNameSet);
|
||||
for (const libraryName of availableNames) {
|
||||
if (!retainedNameSet.has(libraryName)) {
|
||||
fs.rmSync(path.join(libDir, libraryName));
|
||||
}
|
||||
}
|
||||
|
||||
for (const retainedName of retainedNameSet) {
|
||||
const retainedPath = path.join(libDir, retainedName);
|
||||
const stat = fs.lstatSync(retainedPath);
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`Retained runtime library ${retainedName} must be a materialized regular file.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createRuntimeFileRecords(libDir) {
|
||||
return runtimeLibraryNames(libDir).map((name) => {
|
||||
const libraryPath = path.join(libDir, name);
|
||||
@@ -677,10 +1060,16 @@ function parseReadelfDynamic(output) {
|
||||
needed: [],
|
||||
rpath: [],
|
||||
runpath: [],
|
||||
soname: null,
|
||||
};
|
||||
const dynamicEntryPattern = /\((NEEDED|RPATH|RUNPATH)\)[^[]*\[([^\]]*)\]/g;
|
||||
const dynamicEntryPattern =
|
||||
/\((NEEDED|RPATH|RUNPATH|SONAME)\)[^[]*\[([^\]]*)\]/g;
|
||||
for (const match of output.matchAll(dynamicEntryPattern)) {
|
||||
const [, tag, value] = match;
|
||||
if (tag === 'SONAME') {
|
||||
dynamic.soname = value;
|
||||
continue;
|
||||
}
|
||||
if (tag === 'NEEDED') {
|
||||
dynamic.needed.push(value);
|
||||
continue;
|
||||
@@ -691,12 +1080,75 @@ function parseReadelfDynamic(output) {
|
||||
);
|
||||
}
|
||||
|
||||
for (const field of Object.keys(dynamic)) {
|
||||
for (const field of ['needed', 'rpath', 'runpath']) {
|
||||
dynamic[field] = [...new Set(dynamic[field])].sort();
|
||||
}
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
function parseReadelfVersionInfo(output, name) {
|
||||
if (typeof output !== 'string' || typeof name !== 'string' || !name) {
|
||||
throw new TypeError(
|
||||
'readelf version output and runtime library name are required.'
|
||||
);
|
||||
}
|
||||
|
||||
let requiredGlibc = null;
|
||||
let requiredGlibcxx = null;
|
||||
const versionPattern = /\b(GLIBCXX|GLIBC)_(\d+(?:\.\d+)+)\b/g;
|
||||
for (const [, namespace, version] of output.matchAll(versionPattern)) {
|
||||
if (
|
||||
namespace === 'GLIBC' &&
|
||||
(!requiredGlibc || compareVersions(version, requiredGlibc) > 0)
|
||||
) {
|
||||
requiredGlibc = version;
|
||||
}
|
||||
if (
|
||||
namespace === 'GLIBCXX' &&
|
||||
(!requiredGlibcxx || compareVersions(version, requiredGlibcxx) > 0)
|
||||
) {
|
||||
requiredGlibcxx = version;
|
||||
}
|
||||
}
|
||||
|
||||
return { name, requiredGlibc, requiredGlibcxx };
|
||||
}
|
||||
|
||||
function assertPortableAbiRecords(records) {
|
||||
if (!Array.isArray(records)) {
|
||||
throw new TypeError('Runtime ABI records must be an array.');
|
||||
}
|
||||
for (const record of records) {
|
||||
for (const [field, maximum] of [
|
||||
['requiredGlibc', PORTABLE_ABI_BASELINE.glibcMaximum],
|
||||
['requiredGlibcxx', PORTABLE_ABI_BASELINE.glibcxxMaximum],
|
||||
]) {
|
||||
const version = record?.[field];
|
||||
if (version && compareVersions(version, maximum) > 0) {
|
||||
throw new Error(
|
||||
`Portable ABI baseline ${PORTABLE_ABI_BASELINE.distribution} rejects newer symbol ${version} required by ${record.name}; maximum ${field} is ${maximum}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertPortableBuildHostGlibc(glibcVersion) {
|
||||
if (
|
||||
typeof glibcVersion !== 'string' ||
|
||||
!/^\d+(?:\.\d+)+$/.test(glibcVersion)
|
||||
) {
|
||||
throw new Error(
|
||||
'Unable to determine the Linux build host glibc version.'
|
||||
);
|
||||
}
|
||||
if (compareVersions(glibcVersion, PORTABLE_ABI_BASELINE.glibcMaximum) > 0) {
|
||||
throw new Error(
|
||||
`Build host glibc ${glibcVersion} exceeds the portable ABI baseline ${PORTABLE_ABI_BASELINE.distribution} maximum ${PORTABLE_ABI_BASELINE.glibcMaximum}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRuntimeDependencyClosure({
|
||||
entries,
|
||||
runtimeFileNames,
|
||||
@@ -714,6 +1166,7 @@ function validateRuntimeDependencyClosure({
|
||||
const normalizedEntries = [...entries]
|
||||
.map((entry) => ({
|
||||
name: entry.name,
|
||||
soname: entry.soname ?? null,
|
||||
needed: [...new Set(entry.needed ?? [])].sort(),
|
||||
rpath: [...new Set(entry.rpath ?? [])].sort(),
|
||||
runpath: [...new Set(entry.runpath ?? [])].sort(),
|
||||
@@ -733,6 +1186,27 @@ function validateRuntimeDependencyClosure({
|
||||
}
|
||||
entryNames.add(entry.name);
|
||||
|
||||
if (
|
||||
entry.soname !== null &&
|
||||
(typeof entry.soname !== 'string' ||
|
||||
!SHARED_LIBRARY_PATTERN.test(entry.soname) ||
|
||||
path.basename(entry.soname) !== entry.soname)
|
||||
) {
|
||||
throw new Error(
|
||||
`${entry.name} SONAME must be null or a safe shared-library basename.`
|
||||
);
|
||||
}
|
||||
if (
|
||||
entry.name === 'libmpv.so' &&
|
||||
(typeof entry.soname !== 'string' ||
|
||||
!/^libmpv\.so\.\d+(?:\.\d+)*$/.test(entry.soname) ||
|
||||
!bundledNames.has(entry.soname))
|
||||
) {
|
||||
throw new Error(
|
||||
'libmpv.so must declare a versioned SONAME present in the runtime closure.'
|
||||
);
|
||||
}
|
||||
|
||||
if (entry.rpath.length > 0) {
|
||||
throw new Error(
|
||||
`${entry.name} has forbidden RPATH ${entry.rpath.join(':')}.`
|
||||
@@ -845,6 +1319,7 @@ function sourceManifestMetadata(sourceRecord) {
|
||||
function createLinuxRuntimeManifest({
|
||||
sourceRecords,
|
||||
runtimeFiles,
|
||||
abiRecords,
|
||||
dependencyClosure,
|
||||
buildHost,
|
||||
generatedAt = new Date().toISOString(),
|
||||
@@ -867,6 +1342,7 @@ function createLinuxRuntimeManifest({
|
||||
}
|
||||
packages[sourcePackage.id] = sourceManifestMetadata(sourceRecord);
|
||||
}
|
||||
assertPortableAbiRecords(abiRecords);
|
||||
|
||||
const runtimeTotalBytes = runtimeFiles.reduce(
|
||||
(total, runtimeFile) => total + runtimeFile.size,
|
||||
@@ -893,9 +1369,18 @@ function createLinuxRuntimeManifest({
|
||||
},
|
||||
runtimeFiles: runtimeFiles.map((runtimeFile) => ({ ...runtimeFile })),
|
||||
runtimeTotalBytes,
|
||||
runtimeAbi: {
|
||||
baseline: { ...PORTABLE_ABI_BASELINE },
|
||||
files: abiRecords.map((record) => ({ ...record })),
|
||||
},
|
||||
runtimeExternalConfiguration: {
|
||||
fontconfig: { ...RUNTIME_EXTERNAL_CONFIGURATION.fontconfig },
|
||||
openssl: { ...RUNTIME_EXTERNAL_CONFIGURATION.openssl },
|
||||
},
|
||||
runtimeDependencyClosure: {
|
||||
entries: dependencyClosure.entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
soname: entry.soname ?? null,
|
||||
needed: [...entry.needed],
|
||||
rpath: [...entry.rpath],
|
||||
runpath: [...entry.runpath],
|
||||
@@ -907,7 +1392,7 @@ function createLinuxRuntimeManifest({
|
||||
),
|
||||
buildHost,
|
||||
sourceDistribution:
|
||||
'Attach a source archive to the corresponding Linux binary release containing the exact downloaded source archives, a checkout or git bundle of the recorded libplacebo commit and submodules, tools/embedded-mpv/build-linux-runtime.mjs, tools/embedded-mpv/build-linux-runtime.cjs, this runtime manifest, and any local patches.',
|
||||
'Attach a source archive to the corresponding Linux binary release containing the exact downloaded source archives, including the pinned MIT-licensed libdisplay-info source archive, a checkout or git bundle of the recorded libplacebo commit and submodules, tools/embedded-mpv/build-linux-runtime.mjs, tools/embedded-mpv/build-linux-runtime.cjs, this runtime manifest, and any local patches.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -919,19 +1404,36 @@ module.exports = {
|
||||
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
|
||||
FFMPEG_CONFIGURE_FLAGS,
|
||||
GLIBC_TOOLCHAIN_ALLOWLIST,
|
||||
MINIMUM_TOOL_VERSIONS,
|
||||
MPV_MESON_FLAGS,
|
||||
OUTPUT_OWNERSHIP_MARKER,
|
||||
PORTABLE_ABI_BASELINE,
|
||||
REQUIRED_TOOLS,
|
||||
RUNTIME_EXTERNAL_CONFIGURATION,
|
||||
SOURCE_PACKAGES,
|
||||
assertArchiveMatchesPin,
|
||||
assertGitCommitMatchesPin,
|
||||
assertMinimumToolVersions,
|
||||
assertOwnedOutputDestination,
|
||||
assertPortableAbiRecords,
|
||||
assertPortableBuildHostGlibc,
|
||||
assertUniqueMesonOptionAssignments,
|
||||
compareVersions,
|
||||
createBuildEnvironment,
|
||||
createLinuxRuntimeManifest,
|
||||
createOwnedStagingPrefix,
|
||||
createRuntimeFileRecords,
|
||||
materializeLibrarySymlinks,
|
||||
ownedStagingPrefixPath,
|
||||
parseCliInvocation,
|
||||
parseReadelfDynamic,
|
||||
parseReadelfVersionInfo,
|
||||
parseVersion,
|
||||
resolveSystemPkgConfigDirs,
|
||||
runtimeLibraryNames,
|
||||
sha256Buffer,
|
||||
publishOwnedOutput,
|
||||
retainRuntimeLibraries,
|
||||
selectReachableRuntimeLibraryNames,
|
||||
validateRuntimeDependencyClosure,
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs';
|
||||
import crypto from 'node:crypto';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
@@ -13,19 +14,30 @@ const {
|
||||
BUILD_ORDER,
|
||||
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
|
||||
MPV_MESON_FLAGS,
|
||||
PORTABLE_ABI_BASELINE,
|
||||
REQUIRED_TOOLS,
|
||||
SOURCE_PACKAGES,
|
||||
assertArchiveMatchesPin,
|
||||
assertGitCommitMatchesPin,
|
||||
assertMinimumToolVersions,
|
||||
assertOwnedOutputDestination,
|
||||
assertPortableAbiRecords,
|
||||
assertPortableBuildHostGlibc,
|
||||
assertUniqueMesonOptionAssignments,
|
||||
createBuildEnvironment,
|
||||
createLinuxRuntimeManifest,
|
||||
createOwnedStagingPrefix,
|
||||
createRuntimeFileRecords,
|
||||
materializeLibrarySymlinks,
|
||||
ownedStagingPrefixPath,
|
||||
parseCliInvocation,
|
||||
parseReadelfDynamic,
|
||||
parseReadelfVersionInfo,
|
||||
retainRuntimeLibraries,
|
||||
resolveSystemPkgConfigDirs,
|
||||
runtimeLibraryNames,
|
||||
selectReachableRuntimeLibraryNames,
|
||||
sha256Buffer,
|
||||
publishOwnedOutput,
|
||||
validateRuntimeDependencyClosure,
|
||||
} = require('./build-linux-runtime.cjs');
|
||||
const {
|
||||
@@ -344,6 +356,91 @@ function mesonInstall(packageId, recipe, context) {
|
||||
context.run('meson', ['install', '-C', buildPath], { cwd: sourcePath });
|
||||
}
|
||||
|
||||
function pathInsideDestdir(destdir, absolutePath) {
|
||||
return path.join(
|
||||
destdir,
|
||||
absolutePath.slice(path.parse(absolutePath).root.length)
|
||||
);
|
||||
}
|
||||
|
||||
function copyDirectoryContents(sourceDirectory, destinationDirectory) {
|
||||
if (!fs.existsSync(sourceDirectory)) {
|
||||
return;
|
||||
}
|
||||
fs.mkdirSync(destinationDirectory, { recursive: true });
|
||||
for (const entry of fs.readdirSync(sourceDirectory)) {
|
||||
fs.cpSync(
|
||||
path.join(sourceDirectory, entry),
|
||||
path.join(destinationDirectory, entry),
|
||||
{
|
||||
recursive: true,
|
||||
force: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function installWithDestdir(packageId, context, install, externalPaths = []) {
|
||||
const destdir = path.join(context.buildRoot, 'install-roots', packageId);
|
||||
fs.rmSync(destdir, { recursive: true, force: true });
|
||||
fs.mkdirSync(destdir, { recursive: true });
|
||||
try {
|
||||
install(destdir);
|
||||
copyDirectoryContents(
|
||||
pathInsideDestdir(destdir, context.prefix),
|
||||
context.prefix
|
||||
);
|
||||
for (const { destination, source } of externalPaths) {
|
||||
copyDirectoryContents(
|
||||
pathInsideDestdir(destdir, source),
|
||||
path.join(context.prefix, destination)
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(destdir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function fontconfigInstall(recipe, context) {
|
||||
const sourcePath = sourcePathFor('fontconfig', context.sourceRoot);
|
||||
const buildPath = path.join(sourcePath, 'build-iptvnator');
|
||||
fs.rmSync(buildPath, { recursive: true, force: true });
|
||||
context.run(
|
||||
'meson',
|
||||
['setup', buildPath, ...mesonSetupArgs(context.prefix, recipe.args)],
|
||||
{ cwd: sourcePath }
|
||||
);
|
||||
context.run(
|
||||
'meson',
|
||||
['compile', '--jobs', context.parallelism, '-C', buildPath],
|
||||
{ cwd: sourcePath }
|
||||
);
|
||||
installWithDestdir(
|
||||
'fontconfig',
|
||||
context,
|
||||
(destdir) =>
|
||||
context.run('meson', ['install', '-C', buildPath], {
|
||||
cwd: sourcePath,
|
||||
env: { ...context.buildEnvironment, DESTDIR: destdir },
|
||||
}),
|
||||
[
|
||||
{ source: '/etc/fonts', destination: path.join('etc', 'fonts') },
|
||||
{
|
||||
source: '/usr/share/fontconfig',
|
||||
destination: path.join('share', 'fontconfig'),
|
||||
},
|
||||
{
|
||||
source: '/usr/share/xml/fontconfig',
|
||||
destination: path.join('share', 'xml', 'fontconfig'),
|
||||
},
|
||||
{
|
||||
source: '/var/cache/fontconfig',
|
||||
destination: path.join('var', 'cache', 'fontconfig'),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
function cmakeInstall(packageId, recipe, context) {
|
||||
const sourcePath = sourcePathFor(packageId, context.sourceRoot);
|
||||
const buildPath = path.join(sourcePath, 'build-iptvnator');
|
||||
@@ -378,14 +475,17 @@ function opensslInstall(recipe, context) {
|
||||
'./Configure',
|
||||
'linux-x86_64',
|
||||
`--prefix=${context.prefix}`,
|
||||
`--openssldir=${path.join(context.prefix, 'etc', 'ssl')}`,
|
||||
'--libdir=lib',
|
||||
...recipe.args,
|
||||
],
|
||||
{ cwd: sourcePath }
|
||||
);
|
||||
context.run('make', [`-j${context.parallelism}`], { cwd: sourcePath });
|
||||
context.run('make', ['install_sw'], { cwd: sourcePath });
|
||||
installWithDestdir('openssl', context, (destdir) =>
|
||||
context.run('make', ['install_sw', `DESTDIR=${destdir}`], {
|
||||
cwd: sourcePath,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function ffmpegInstall(recipe, context) {
|
||||
@@ -401,6 +501,10 @@ function buildRuntime(context) {
|
||||
for (const packageId of BUILD_ORDER) {
|
||||
const recipe = BUILD_RECIPES[packageId];
|
||||
log(`Building ${packageId} as shared libraries`);
|
||||
if (packageId === 'fontconfig') {
|
||||
fontconfigInstall(recipe, context);
|
||||
continue;
|
||||
}
|
||||
switch (recipe.buildSystem) {
|
||||
case 'configure':
|
||||
configureInstall(packageId, recipe, context);
|
||||
@@ -477,8 +581,25 @@ function postProcessRuntime(context) {
|
||||
if (!fs.existsSync(libDir)) {
|
||||
throw new Error(`Runtime library directory was not built: ${libDir}.`);
|
||||
}
|
||||
materializeLibrarySymlinks(libDir);
|
||||
|
||||
const unprunedDynamicEntries = runtimeLibraryNames(libDir).map(
|
||||
(libraryName) => {
|
||||
const libraryPath = path.join(libDir, libraryName);
|
||||
assertElfLibrary(libraryPath);
|
||||
return {
|
||||
name: libraryName,
|
||||
...parseReadelfDynamic(
|
||||
context.runCapture('readelf', ['-d', libraryPath])
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
const retainedNames = selectReachableRuntimeLibraryNames(
|
||||
unprunedDynamicEntries
|
||||
);
|
||||
retainRuntimeLibraries(libDir, retainedNames);
|
||||
|
||||
const abiRecords = [];
|
||||
const dynamicEntries = [];
|
||||
for (const libraryName of runtimeLibraryNames(libDir)) {
|
||||
const libraryPath = path.join(libDir, libraryName);
|
||||
@@ -488,7 +609,13 @@ function postProcessRuntime(context) {
|
||||
context.runCapture('readelf', ['-d', libraryPath])
|
||||
);
|
||||
dynamicEntries.push({ name: libraryName, ...dynamic });
|
||||
const versionInfo = context.runCapture('readelf', [
|
||||
'--version-info',
|
||||
libraryPath,
|
||||
]);
|
||||
abiRecords.push(parseReadelfVersionInfo(versionInfo, libraryName));
|
||||
}
|
||||
assertPortableAbiRecords(abiRecords);
|
||||
|
||||
const runtimeFiles = createRuntimeFileRecords(libDir);
|
||||
if (runtimeFiles.length === 0) {
|
||||
@@ -502,7 +629,12 @@ function postProcessRuntime(context) {
|
||||
buildPrefix: context.prefix,
|
||||
});
|
||||
|
||||
return { dependencyClosure, runtimeFiles };
|
||||
return {
|
||||
abiBaseline: PORTABLE_ABI_BASELINE,
|
||||
abiRecords,
|
||||
dependencyClosure,
|
||||
runtimeFiles,
|
||||
};
|
||||
}
|
||||
|
||||
function firstLine(value) {
|
||||
@@ -515,6 +647,31 @@ function firstLine(value) {
|
||||
}
|
||||
|
||||
function collectBuildHost(context) {
|
||||
const tools = context.toolVersions;
|
||||
if (!tools) {
|
||||
throw new Error('Linux runtime tool versions were not preflighted.');
|
||||
}
|
||||
return {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
release: os.release(),
|
||||
glibcVersion: context.glibcVersion,
|
||||
systemPkgConfigDirs: [...context.systemPkgConfigDirs],
|
||||
systemPkgConfigPackages: {
|
||||
...context.systemPkgConfigPackages,
|
||||
},
|
||||
tools: { ...tools },
|
||||
};
|
||||
}
|
||||
|
||||
function detectBuildHostGlibcVersion() {
|
||||
const glibcVersion =
|
||||
process.report?.getReport?.()?.header?.glibcVersionRuntime;
|
||||
assertPortableBuildHostGlibc(glibcVersion);
|
||||
return glibcVersion;
|
||||
}
|
||||
|
||||
function collectToolVersions(context) {
|
||||
const versionArgs = {
|
||||
cc: ['--version'],
|
||||
cmake: ['--version'],
|
||||
@@ -522,42 +679,34 @@ function collectBuildHost(context) {
|
||||
git: ['--version'],
|
||||
make: ['--version'],
|
||||
meson: ['--version'],
|
||||
nasm: ['-v'],
|
||||
ninja: ['--version'],
|
||||
patchelf: ['--version'],
|
||||
perl: ['--version'],
|
||||
perl: ['-e', 'printf "%vd\\n", $^V'],
|
||||
'pkg-config': ['--version'],
|
||||
python3: ['--version'],
|
||||
readelf: ['--version'],
|
||||
tar: ['--version'],
|
||||
};
|
||||
const tools = {};
|
||||
const toolVersions = {};
|
||||
for (const tool of REQUIRED_TOOLS) {
|
||||
tools[tool] = firstLine(
|
||||
toolVersions[tool] = firstLine(
|
||||
context.runCapture(tool, versionArgs[tool] ?? ['--version'])
|
||||
);
|
||||
}
|
||||
return toolVersions;
|
||||
}
|
||||
|
||||
function verifySystemPkgConfigPackages(context) {
|
||||
const systemPkgConfigPackages = {};
|
||||
for (const packageName of EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES) {
|
||||
context.run('pkg-config', ['--exists', packageName]);
|
||||
systemPkgConfigPackages[packageName] = context.runCapture(
|
||||
'pkg-config',
|
||||
['--modversion', packageName]
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
release: os.release(),
|
||||
systemPkgConfigDirs: [...context.systemPkgConfigDirs],
|
||||
systemPkgConfigPackages,
|
||||
tools,
|
||||
};
|
||||
}
|
||||
|
||||
function verifySystemPkgConfigPackages(context) {
|
||||
for (const packageName of EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES) {
|
||||
context.run('pkg-config', ['--exists', packageName]);
|
||||
}
|
||||
return systemPkgConfigPackages;
|
||||
}
|
||||
|
||||
function writeManifest(context, sourceRecords, runtimeMetadata) {
|
||||
@@ -565,6 +714,7 @@ function writeManifest(context, sourceRecords, runtimeMetadata) {
|
||||
const manifest = createLinuxRuntimeManifest({
|
||||
sourceRecords,
|
||||
runtimeFiles: runtimeMetadata.runtimeFiles,
|
||||
abiRecords: runtimeMetadata.abiRecords,
|
||||
dependencyClosure: runtimeMetadata.dependencyClosure,
|
||||
buildHost: collectBuildHost(context),
|
||||
ffmpegConfigureFlags: context.ffmpegConfigureFlags,
|
||||
@@ -616,6 +766,7 @@ function createBuildContext(prefix, environment) {
|
||||
sourceRoot: path.join(buildRoot, 'sources'),
|
||||
parallelism: resolveParallelism(environment),
|
||||
systemPkgConfigDirs,
|
||||
buildEnvironment,
|
||||
ffmpegConfigureFlags: null,
|
||||
};
|
||||
}
|
||||
@@ -627,21 +778,56 @@ export function main({
|
||||
cwd = process.cwd(),
|
||||
environment = process.env,
|
||||
} = {}) {
|
||||
const { prefix } = parseCliInvocation({ platform, arch, argv, cwd });
|
||||
const { prefix: outputPrefix } = parseCliInvocation({
|
||||
platform,
|
||||
arch,
|
||||
argv,
|
||||
cwd,
|
||||
});
|
||||
assertUniqueMesonOptionAssignments(BUILD_RECIPES);
|
||||
ensureTools();
|
||||
const context = createBuildContext(prefix, environment);
|
||||
assertOwnedOutputDestination(outputPrefix);
|
||||
const stagingToken = `${process.pid}-${crypto
|
||||
.randomBytes(8)
|
||||
.toString('hex')}`;
|
||||
const stagingPrefix = ownedStagingPrefixPath(outputPrefix, stagingToken);
|
||||
const context = createBuildContext(stagingPrefix, environment);
|
||||
assertSafeOutputPrefix(outputPrefix, context.buildRoot);
|
||||
const toolVersions = collectToolVersions(context);
|
||||
assertMinimumToolVersions(toolVersions);
|
||||
context.toolVersions = toolVersions;
|
||||
context.glibcVersion = detectBuildHostGlibcVersion();
|
||||
context.systemPkgConfigPackages = verifySystemPkgConfigPackages(context);
|
||||
fs.mkdirSync(context.buildRoot, { recursive: true });
|
||||
fs.rmSync(context.prefix, { recursive: true, force: true });
|
||||
fs.mkdirSync(context.prefix, { recursive: true });
|
||||
verifySystemPkgConfigPackages(context);
|
||||
const sourceRecords = acquireSources(context);
|
||||
buildRuntime(context);
|
||||
removeNonRuntimeBuildOutputs(prefix);
|
||||
const runtimeMetadata = postProcessRuntime(context);
|
||||
const manifest = writeManifest(context, sourceRecords, runtimeMetadata);
|
||||
log(
|
||||
`Built ${manifest.runtimeFiles.length} LGPL-compatible runtime libraries (${manifest.runtimeTotalBytes} bytes) at ${prefix}`
|
||||
);
|
||||
let stagingCreated = false;
|
||||
try {
|
||||
const createdStagingPrefix = createOwnedStagingPrefix(outputPrefix, {
|
||||
token: stagingToken,
|
||||
});
|
||||
if (createdStagingPrefix !== stagingPrefix) {
|
||||
throw new Error(
|
||||
'Linux runtime staging prefix changed unexpectedly.'
|
||||
);
|
||||
}
|
||||
stagingCreated = true;
|
||||
const sourceRecords = acquireSources(context);
|
||||
buildRuntime(context);
|
||||
removeNonRuntimeBuildOutputs(context.prefix);
|
||||
const runtimeMetadata = postProcessRuntime(context);
|
||||
const manifest = writeManifest(context, sourceRecords, runtimeMetadata);
|
||||
publishOwnedOutput({
|
||||
outputPrefix,
|
||||
stagingPrefix,
|
||||
});
|
||||
stagingCreated = false;
|
||||
log(
|
||||
`Built ${manifest.runtimeFiles.length} LGPL-compatible runtime libraries (${manifest.runtimeTotalBytes} bytes) at ${outputPrefix}`
|
||||
);
|
||||
} finally {
|
||||
if (stagingCreated) {
|
||||
fs.rmSync(stagingPrefix, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const invokedScriptPath = process.argv[1]
|
||||
|
||||
@@ -20,18 +20,32 @@ const {
|
||||
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
|
||||
FFMPEG_CONFIGURE_FLAGS,
|
||||
GLIBC_TOOLCHAIN_ALLOWLIST,
|
||||
MINIMUM_TOOL_VERSIONS,
|
||||
MPV_MESON_FLAGS,
|
||||
OUTPUT_OWNERSHIP_MARKER,
|
||||
PORTABLE_ABI_BASELINE,
|
||||
REQUIRED_TOOLS,
|
||||
RUNTIME_EXTERNAL_CONFIGURATION,
|
||||
SOURCE_PACKAGES,
|
||||
assertArchiveMatchesPin,
|
||||
assertGitCommitMatchesPin,
|
||||
assertMinimumToolVersions,
|
||||
assertOwnedOutputDestination,
|
||||
assertPortableAbiRecords,
|
||||
assertPortableBuildHostGlibc,
|
||||
assertUniqueMesonOptionAssignments,
|
||||
createBuildEnvironment,
|
||||
createLinuxRuntimeManifest,
|
||||
createOwnedStagingPrefix,
|
||||
createRuntimeFileRecords,
|
||||
materializeLibrarySymlinks,
|
||||
parseCliInvocation,
|
||||
parseReadelfDynamic,
|
||||
parseReadelfVersionInfo,
|
||||
publishOwnedOutput,
|
||||
retainRuntimeLibraries,
|
||||
resolveSystemPkgConfigDirs,
|
||||
selectReachableRuntimeLibraryNames,
|
||||
validateRuntimeDependencyClosure,
|
||||
} = require('./build-linux-runtime.cjs');
|
||||
const {
|
||||
@@ -77,6 +91,7 @@ test('pins the complete source stack and preserves dependency build order', () =
|
||||
{ id: 'openssl', version: '3.5.7' },
|
||||
{ id: 'ffmpeg', version: '8.1' },
|
||||
{ id: 'libplacebo', version: '7.360.1' },
|
||||
{ id: 'libdisplay-info', version: '0.1.1' },
|
||||
{ id: 'mpv', version: '0.41.0' },
|
||||
]
|
||||
);
|
||||
@@ -114,6 +129,19 @@ test('pins the complete source stack and preserves dependency build order', () =
|
||||
byId.get('openssl').sourceUrl,
|
||||
'https://github.com/openssl/openssl/releases/download/openssl-3.5.7/openssl-3.5.7.tar.gz'
|
||||
);
|
||||
assert.deepEqual(byId.get('libdisplay-info'), {
|
||||
id: 'libdisplay-info',
|
||||
version: '0.1.1',
|
||||
sourceKind: 'archive',
|
||||
sourceUrl:
|
||||
'https://gitlab.freedesktop.org/emersion/libdisplay-info/-/releases/0.1.1/downloads/libdisplay-info-0.1.1.tar.xz',
|
||||
expectedSha256:
|
||||
'0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60',
|
||||
license: 'MIT',
|
||||
});
|
||||
assert.ok(
|
||||
BUILD_ORDER.indexOf('libdisplay-info') < BUILD_ORDER.indexOf('mpv')
|
||||
);
|
||||
});
|
||||
|
||||
test('hardcodes the verified official archive digests and libplacebo commit', () => {
|
||||
@@ -129,6 +157,8 @@ test('hardcodes the verified official archive digests and libplacebo commit', ()
|
||||
harfbuzz:
|
||||
'77e4f7f98f3d86bf8788b53e6832fb96279956e1c3961988ea3d4b7ca41ddc27',
|
||||
libass: 'eae425da50f0015c21f7b3a9c7262a910f0218af469e22e2931462fed3c50959',
|
||||
'libdisplay-info':
|
||||
'0d8731588e9f82a9cac96324a3d7c82e2ba5b1b5e006143fefe692c74069fb60',
|
||||
mpv: 'ee21092a5ee427353392360929dc64645c54479aefdb5babc5cfbb5fad626209',
|
||||
openssl:
|
||||
'a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8',
|
||||
@@ -246,6 +276,12 @@ test('defines shared-only source recipes with font discovery before playback', (
|
||||
'-Dtests=disabled',
|
||||
'-Dtools=disabled',
|
||||
'-Dcache-build=disabled',
|
||||
'-Dxml-backend=expat',
|
||||
'-Dbaseconfig-dir=/etc/fonts',
|
||||
'-Dconfig-dir=/etc/fonts/conf.d',
|
||||
'-Dtemplate-dir=/usr/share/fontconfig/conf.avail',
|
||||
'-Dcache-dir=/var/cache/fontconfig',
|
||||
'-Dxml-dir=/usr/share/xml/fontconfig',
|
||||
]) {
|
||||
assert.ok(BUILD_RECIPES.fontconfig.args.includes(flag), flag);
|
||||
}
|
||||
@@ -254,18 +290,51 @@ test('defines shared-only source recipes with font discovery before playback', (
|
||||
BUILD_RECIPES.libass.args.includes('--disable-fontconfig'),
|
||||
false
|
||||
);
|
||||
for (const flag of ['shared', 'no-apps', 'no-docs', 'no-tests']) {
|
||||
for (const flag of [
|
||||
'shared',
|
||||
'no-apps',
|
||||
'no-docs',
|
||||
'no-tests',
|
||||
'--openssldir=/etc/ssl',
|
||||
]) {
|
||||
assert.ok(BUILD_RECIPES.openssl.args.includes(flag), flag);
|
||||
}
|
||||
assert.ok(
|
||||
BUILD_ORDER.indexOf('fontconfig') < BUILD_ORDER.indexOf('libass')
|
||||
);
|
||||
assert.ok(BUILD_ORDER.indexOf('openssl') < BUILD_ORDER.indexOf('ffmpeg'));
|
||||
assert.equal(BUILD_RECIPES['libdisplay-info'].buildSystem, 'meson');
|
||||
for (const packageId of BUILD_ORDER) {
|
||||
assert.equal(BUILD_RECIPES[packageId].sharedOnly, true, packageId);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects duplicate option assignments in every Meson recipe', () => {
|
||||
assert.doesNotThrow(() =>
|
||||
assertUniqueMesonOptionAssignments(BUILD_RECIPES)
|
||||
);
|
||||
|
||||
for (const [packageId, duplicateFlag] of [
|
||||
['fontconfig', '-Dxml-backend=libxml2'],
|
||||
['libplacebo', '-Dvulkan=enabled'],
|
||||
]) {
|
||||
const duplicateRecipes = {
|
||||
...BUILD_RECIPES,
|
||||
[packageId]: {
|
||||
...BUILD_RECIPES[packageId],
|
||||
args: [...BUILD_RECIPES[packageId].args, duplicateFlag],
|
||||
},
|
||||
};
|
||||
assert.throws(
|
||||
() => assertUniqueMesonOptionAssignments(duplicateRecipes),
|
||||
new RegExp(
|
||||
`${packageId}.*${duplicateFlag.split('=')[0]}.*once`,
|
||||
'i'
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('constructs a prefix-only build environment and ignores hostile host flags', () => {
|
||||
const environment = createBuildEnvironment({
|
||||
prefix: '/opt/runtime',
|
||||
@@ -330,6 +399,117 @@ test('constructs a prefix-only build environment and ignores hostile host flags'
|
||||
assert.doesNotMatch(JSON.stringify(environment), /\/host\//);
|
||||
});
|
||||
|
||||
test('rejects an existing unmarked output without changing its contents', (t) => {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'iptvnator-output-ownership-')
|
||||
);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const outputPrefix = path.join(root, 'usr', 'local');
|
||||
fs.mkdirSync(outputPrefix, { recursive: true });
|
||||
fs.writeFileSync(path.join(outputPrefix, 'keep-me'), 'untouched');
|
||||
|
||||
assert.throws(
|
||||
() => assertOwnedOutputDestination(outputPrefix),
|
||||
/existing output.*ownership marker/i
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(outputPrefix, 'keep-me'), 'utf8'),
|
||||
'untouched'
|
||||
);
|
||||
});
|
||||
|
||||
test('atomically publishes only owned outputs and rolls back failures', (t) => {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'iptvnator-output-publish-')
|
||||
);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const outputPrefix = path.join(root, 'runtime');
|
||||
fs.mkdirSync(outputPrefix);
|
||||
fs.writeFileSync(
|
||||
path.join(outputPrefix, OUTPUT_OWNERSHIP_MARKER),
|
||||
'iptvnator-embedded-mpv-linux-runtime-v1\n'
|
||||
);
|
||||
fs.writeFileSync(path.join(outputPrefix, 'state'), 'previous');
|
||||
|
||||
const failedStagingPrefix = createOwnedStagingPrefix(outputPrefix, {
|
||||
token: 'failed',
|
||||
});
|
||||
fs.writeFileSync(path.join(failedStagingPrefix, 'state'), 'replacement');
|
||||
let renameCount = 0;
|
||||
const failingFileSystem = {
|
||||
...fs,
|
||||
renameSync(source, destination) {
|
||||
renameCount += 1;
|
||||
if (renameCount === 2) {
|
||||
throw new Error('injected publication failure');
|
||||
}
|
||||
return fs.renameSync(source, destination);
|
||||
},
|
||||
};
|
||||
assert.throws(
|
||||
() =>
|
||||
publishOwnedOutput({
|
||||
fileSystem: failingFileSystem,
|
||||
outputPrefix,
|
||||
stagingPrefix: failedStagingPrefix,
|
||||
token: 'rollback',
|
||||
}),
|
||||
/injected publication failure/
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(outputPrefix, 'state'), 'utf8'),
|
||||
'previous'
|
||||
);
|
||||
assert.equal(fs.existsSync(failedStagingPrefix), false);
|
||||
assert.deepEqual(
|
||||
fs.readdirSync(root).filter((name) => name.includes('backup')),
|
||||
[]
|
||||
);
|
||||
|
||||
const successfulStagingPrefix = createOwnedStagingPrefix(outputPrefix, {
|
||||
token: 'successful',
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(successfulStagingPrefix, 'state'),
|
||||
'replacement'
|
||||
);
|
||||
publishOwnedOutput({
|
||||
outputPrefix,
|
||||
stagingPrefix: successfulStagingPrefix,
|
||||
token: 'success',
|
||||
});
|
||||
assert.equal(
|
||||
fs.readFileSync(path.join(outputPrefix, 'state'), 'utf8'),
|
||||
'replacement'
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(
|
||||
path.join(outputPrefix, OUTPUT_OWNERSHIP_MARKER),
|
||||
'utf8'
|
||||
),
|
||||
'iptvnator-embedded-mpv-linux-runtime-v1\n'
|
||||
);
|
||||
});
|
||||
|
||||
test('builds in an owned sibling before atomically publishing output', () => {
|
||||
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
||||
assert.doesNotMatch(
|
||||
builderSource,
|
||||
/fs\.rmSync\(context\.prefix, \{ recursive: true, force: true \}\)/
|
||||
);
|
||||
const toolPreflight = builderSource.indexOf(
|
||||
'assertMinimumToolVersions(toolVersions)'
|
||||
);
|
||||
const stagingMutation = builderSource.indexOf(
|
||||
'createOwnedStagingPrefix(outputPrefix'
|
||||
);
|
||||
const publication = builderSource.indexOf('publishOwnedOutput({');
|
||||
assert.notEqual(stagingMutation, -1);
|
||||
assert.notEqual(publication, -1);
|
||||
assert.ok(toolPreflight < stagingMutation);
|
||||
assert.ok(stagingMutation < publication);
|
||||
});
|
||||
|
||||
test('uses fixed Linux x64 pkg-config directories without host discovery', () => {
|
||||
assert.deepEqual(DEFAULT_SYSTEM_PKG_CONFIG_DIRS, [
|
||||
'/usr/lib/x86_64-linux-gnu/pkgconfig',
|
||||
@@ -367,6 +547,7 @@ test('declares only the intended Linux system interface packages', () => {
|
||||
'egl',
|
||||
'gbm',
|
||||
'gl',
|
||||
'libdrm',
|
||||
'libpulse',
|
||||
'libva',
|
||||
'libva-drm',
|
||||
@@ -391,6 +572,7 @@ test('requires ELF patching and inspection tools in addition to the build toolch
|
||||
'git',
|
||||
'make',
|
||||
'meson',
|
||||
'nasm',
|
||||
'ninja',
|
||||
'patchelf',
|
||||
'pkg-config',
|
||||
@@ -401,6 +583,95 @@ test('requires ELF patching and inspection tools in addition to the build toolch
|
||||
}
|
||||
});
|
||||
|
||||
test('preflights every required tool against a supported minimum version', () => {
|
||||
assert.deepEqual(
|
||||
Object.keys(MINIMUM_TOOL_VERSIONS).sort(),
|
||||
[...REQUIRED_TOOLS].sort()
|
||||
);
|
||||
assert.equal(MINIMUM_TOOL_VERSIONS.meson, '1.6.0');
|
||||
assert.equal(MINIMUM_TOOL_VERSIONS.nasm, '2.15.05');
|
||||
|
||||
const supportedVersions = Object.fromEntries(
|
||||
REQUIRED_TOOLS.map((tool) => [
|
||||
tool,
|
||||
`${tool} ${MINIMUM_TOOL_VERSIONS[tool]}`,
|
||||
])
|
||||
);
|
||||
assert.doesNotThrow(() => assertMinimumToolVersions(supportedVersions));
|
||||
|
||||
const missingTool = { ...supportedVersions };
|
||||
delete missingTool.nasm;
|
||||
assert.throws(
|
||||
() => assertMinimumToolVersions(missingTool),
|
||||
/missing required tool version.*nasm/i
|
||||
);
|
||||
|
||||
for (const [tool, oldVersion] of [
|
||||
['meson', 'meson 1.5.9'],
|
||||
['nasm', 'NASM version 2.15.04'],
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertMinimumToolVersions({
|
||||
...supportedVersions,
|
||||
[tool]: oldVersion,
|
||||
}),
|
||||
new RegExp(`${tool}.*requires.*${MINIMUM_TOOL_VERSIONS[tool]}`, 'i')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('completes tool and system-package preflight before filesystem mutation', () => {
|
||||
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
||||
const buildRootMutation = builderSource.indexOf(
|
||||
'fs.mkdirSync(context.buildRoot'
|
||||
);
|
||||
const toolPreflight = builderSource.indexOf(
|
||||
'assertMinimumToolVersions(toolVersions)'
|
||||
);
|
||||
const packagePreflight = builderSource.indexOf(
|
||||
'verifySystemPkgConfigPackages(context)'
|
||||
);
|
||||
const mesonPolicyPreflight = builderSource.indexOf(
|
||||
'assertUniqueMesonOptionAssignments(BUILD_RECIPES)'
|
||||
);
|
||||
assert.notEqual(mesonPolicyPreflight, -1);
|
||||
assert.notEqual(toolPreflight, -1);
|
||||
assert.notEqual(packagePreflight, -1);
|
||||
assert.ok(mesonPolicyPreflight < buildRootMutation);
|
||||
assert.ok(toolPreflight < buildRootMutation);
|
||||
assert.ok(packagePreflight < buildRootMutation);
|
||||
});
|
||||
|
||||
test('uses DESTDIR with standard fontconfig and OpenSSL runtime paths', () => {
|
||||
assert.deepEqual(RUNTIME_EXTERNAL_CONFIGURATION, {
|
||||
fontconfig: {
|
||||
configDirectory: '/etc/fonts',
|
||||
templateDirectory: '/usr/share/fontconfig',
|
||||
cacheDirectory: '/var/cache/fontconfig',
|
||||
ownership: 'system',
|
||||
},
|
||||
openssl: {
|
||||
configFile: '/etc/ssl/openssl.cnf',
|
||||
certificateFile: '/etc/ssl/cert.pem',
|
||||
certificateDirectory: '/etc/ssl/certs',
|
||||
ownership: 'system',
|
||||
},
|
||||
});
|
||||
assert.doesNotMatch(
|
||||
JSON.stringify(RUNTIME_EXTERNAL_CONFIGURATION),
|
||||
/build|prefix|tmp/i
|
||||
);
|
||||
|
||||
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
||||
assert.match(builderSource, /DESTDIR/);
|
||||
assert.match(builderSource, /installWithDestdir/);
|
||||
assert.doesNotMatch(
|
||||
builderSource,
|
||||
/--openssldir=\$\{path\.join\(context\.prefix/
|
||||
);
|
||||
});
|
||||
|
||||
test('uses an explicit LGPL FFmpeg HTTPS and HLS protocol baseline', () => {
|
||||
const required = [
|
||||
'--enable-shared',
|
||||
@@ -573,6 +844,7 @@ test('uses valid mpv v0.41 Meson option names and pins the Linux backends', () =
|
||||
'-Dvulkan=disabled',
|
||||
'-Dshaderc=disabled',
|
||||
'-Dspirv-cross=disabled',
|
||||
'-Ddrm=enabled',
|
||||
'-Dgl=enabled',
|
||||
'-Dplain-gl=enabled',
|
||||
'-Degl=enabled',
|
||||
@@ -584,6 +856,13 @@ test('uses valid mpv v0.41 Meson option names and pins the Linux backends', () =
|
||||
]) {
|
||||
assert.ok(MPV_MESON_FLAGS.includes(required), `missing ${required}`);
|
||||
}
|
||||
assert.equal(MPV_MESON_FLAGS.includes('-Ddrm=disabled'), false);
|
||||
for (const optionName of ['drm', 'gbm', 'vaapi-drm']) {
|
||||
const assignments = MPV_MESON_FLAGS.filter((flag) =>
|
||||
flag.startsWith(`-D${optionName}=`)
|
||||
);
|
||||
assert.deepEqual(assignments, [`-D${optionName}=enabled`]);
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps the external dependency allowlists explicit and deterministic', () => {
|
||||
@@ -620,6 +899,7 @@ test('keeps the external dependency allowlists explicit and deterministic', () =
|
||||
|
||||
test('parses readelf dynamic sections and validates an ORIGIN-only closure', () => {
|
||||
const mpvDynamic = parseReadelfDynamic(`
|
||||
0x000000000000000e (SONAME) Library soname: [libmpv.so.2]
|
||||
0x0000000000000001 (NEEDED) Shared library: [libavcodec.so.62]
|
||||
0x0000000000000001 (NEEDED) Shared library: [libEGL.so.1]
|
||||
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
|
||||
@@ -629,6 +909,7 @@ test('parses readelf dynamic sections and validates an ORIGIN-only closure', ()
|
||||
needed: ['libEGL.so.1', 'libavcodec.so.62', 'libc.so.6'],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
soname: 'libmpv.so.2',
|
||||
});
|
||||
|
||||
const closure = validateRuntimeDependencyClosure({
|
||||
@@ -655,6 +936,50 @@ test('parses readelf dynamic sections and validates an ORIGIN-only closure', ()
|
||||
'libavcodec.so.62',
|
||||
'libc.so.6',
|
||||
]);
|
||||
assert.equal(closure.entries[1].soname, 'libmpv.so.2');
|
||||
|
||||
const aliasClosure = validateRuntimeDependencyClosure({
|
||||
entries: [
|
||||
{
|
||||
name: 'libmpv.so',
|
||||
...mpvDynamic,
|
||||
},
|
||||
{
|
||||
name: 'libmpv.so.2',
|
||||
...mpvDynamic,
|
||||
},
|
||||
{
|
||||
name: 'libavcodec.so.62',
|
||||
needed: ['libm.so.6'],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
soname: 'libavcodec.so.62',
|
||||
},
|
||||
],
|
||||
runtimeFileNames: ['libavcodec.so.62', 'libmpv.so', 'libmpv.so.2'],
|
||||
buildPrefix: '/tmp/iptvnator-prefix',
|
||||
});
|
||||
assert.equal(
|
||||
aliasClosure.entries.find(({ name }) => name === 'libmpv.so').soname,
|
||||
'libmpv.so.2'
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
validateRuntimeDependencyClosure({
|
||||
entries: aliasClosure.entries.map((entry) =>
|
||||
entry.name === 'libmpv.so'
|
||||
? { ...entry, soname: 'libmpv.so.99' }
|
||||
: entry
|
||||
),
|
||||
runtimeFileNames: [
|
||||
'libavcodec.so.62',
|
||||
'libmpv.so',
|
||||
'libmpv.so.2',
|
||||
],
|
||||
buildPrefix: '/tmp/iptvnator-prefix',
|
||||
}),
|
||||
/libmpv\.so must declare a versioned SONAME present in the runtime closure/
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
@@ -696,6 +1021,140 @@ test('parses readelf dynamic sections and validates an ORIGIN-only closure', ()
|
||||
}
|
||||
});
|
||||
|
||||
test('retains only the reachable SONAME closure plus the libmpv linker alias', (t) => {
|
||||
const entries = [
|
||||
{
|
||||
name: 'libmpv.so',
|
||||
soname: 'libmpv.so.2',
|
||||
needed: ['libavcodec.so.62', 'libc.so.6'],
|
||||
},
|
||||
{
|
||||
name: 'libmpv.so.2',
|
||||
soname: 'libmpv.so.2',
|
||||
needed: ['libavcodec.so.62', 'libc.so.6'],
|
||||
},
|
||||
{
|
||||
name: 'libmpv.so.2.0.0',
|
||||
soname: 'libmpv.so.2',
|
||||
needed: ['libavcodec.so.62', 'libc.so.6'],
|
||||
},
|
||||
{
|
||||
name: 'libavcodec.so',
|
||||
soname: 'libavcodec.so.62',
|
||||
needed: ['libm.so.6'],
|
||||
},
|
||||
{
|
||||
name: 'libavcodec.so.62',
|
||||
soname: 'libavcodec.so.62',
|
||||
needed: ['libm.so.6'],
|
||||
},
|
||||
{
|
||||
name: 'libavcodec.so.62.1.0',
|
||||
soname: 'libavcodec.so.62',
|
||||
needed: ['libm.so.6'],
|
||||
},
|
||||
{
|
||||
name: 'libunused.so.1',
|
||||
soname: 'libunused.so.1',
|
||||
needed: ['libc.so.6'],
|
||||
},
|
||||
];
|
||||
const retainedNames = selectReachableRuntimeLibraryNames(entries);
|
||||
assert.deepEqual(retainedNames, [
|
||||
'libavcodec.so.62',
|
||||
'libmpv.so',
|
||||
'libmpv.so.2',
|
||||
]);
|
||||
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'iptvnator-runtime-prune-')
|
||||
);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const libDir = path.join(root, 'lib');
|
||||
fs.mkdirSync(libDir);
|
||||
fs.writeFileSync(path.join(libDir, 'libmpv.so.2.0.0'), 'mpv');
|
||||
fs.symlinkSync('libmpv.so.2.0.0', path.join(libDir, 'libmpv.so.2'));
|
||||
fs.symlinkSync('libmpv.so.2', path.join(libDir, 'libmpv.so'));
|
||||
fs.writeFileSync(path.join(libDir, 'libavcodec.so.62.1.0'), 'codec');
|
||||
fs.symlinkSync(
|
||||
'libavcodec.so.62.1.0',
|
||||
path.join(libDir, 'libavcodec.so.62')
|
||||
);
|
||||
fs.symlinkSync('libavcodec.so.62', path.join(libDir, 'libavcodec.so'));
|
||||
fs.writeFileSync(path.join(libDir, 'libunused.so.1'), 'unused');
|
||||
|
||||
retainRuntimeLibraries(libDir, retainedNames);
|
||||
assert.deepEqual(fs.readdirSync(libDir).sort(), retainedNames);
|
||||
for (const retainedName of retainedNames) {
|
||||
assert.equal(
|
||||
fs.lstatSync(path.join(libDir, retainedName)).isFile(),
|
||||
true,
|
||||
retainedName
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('enforces the Ubuntu 22.04 GLIBC and GLIBCXX symbol ceilings', () => {
|
||||
assert.deepEqual(PORTABLE_ABI_BASELINE, {
|
||||
distribution: 'Ubuntu 22.04',
|
||||
glibcMaximum: '2.35',
|
||||
glibcxxMaximum: '3.4.30',
|
||||
});
|
||||
const record = parseReadelfVersionInfo(
|
||||
`
|
||||
0x0010: Name: GLIBC_2.2.5 Flags: none Version: 7
|
||||
0x0020: Name: GLIBC_2.34 Flags: none Version: 5
|
||||
0x0030: Name: GLIBCXX_3.4.21 Flags: none Version: 4
|
||||
0x0040: Name: GLIBCXX_3.4.29 Flags: none Version: 3
|
||||
`,
|
||||
'libmpv.so.2'
|
||||
);
|
||||
assert.deepEqual(record, {
|
||||
name: 'libmpv.so.2',
|
||||
requiredGlibc: '2.34',
|
||||
requiredGlibcxx: '3.4.29',
|
||||
});
|
||||
assert.doesNotThrow(() => assertPortableAbiRecords([record]));
|
||||
|
||||
for (const [field, version] of [
|
||||
['requiredGlibc', '2.36'],
|
||||
['requiredGlibcxx', '3.4.31'],
|
||||
]) {
|
||||
assert.throws(
|
||||
() =>
|
||||
assertPortableAbiRecords([
|
||||
{
|
||||
...record,
|
||||
[field]: version,
|
||||
},
|
||||
]),
|
||||
/portable ABI baseline.*newer symbol/i
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects build hosts newer than the portable glibc baseline', () => {
|
||||
assert.doesNotThrow(() => assertPortableBuildHostGlibc('2.35'));
|
||||
assert.throws(
|
||||
() => assertPortableBuildHostGlibc('2.36'),
|
||||
/build host glibc 2\.36.*portable ABI baseline.*2\.35/i
|
||||
);
|
||||
assert.throws(
|
||||
() => assertPortableBuildHostGlibc(undefined),
|
||||
/unable to determine the Linux build host glibc version/i
|
||||
);
|
||||
});
|
||||
|
||||
test('inspects every retained runtime file for portable symbol versions', () => {
|
||||
const builderSource = fs.readFileSync(builderScript, 'utf8');
|
||||
assert.match(
|
||||
builderSource,
|
||||
/runCapture\('readelf', \[\s*'--version-info',\s*libraryPath,\s*\]\)/
|
||||
);
|
||||
assert.match(builderSource, /retainRuntimeLibraries\(libDir,/);
|
||||
assert.match(builderSource, /assertPortableAbiRecords\(abiRecords\)/);
|
||||
});
|
||||
|
||||
test('materializes symlink aliases and hashes every runtime library', (t) => {
|
||||
const root = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), 'iptvnator-linux-builder-')
|
||||
@@ -742,22 +1201,38 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) =
|
||||
const libDir = path.join(root, 'lib');
|
||||
fs.mkdirSync(libDir);
|
||||
fs.writeFileSync(path.join(libDir, 'libavcodec.so.62'), 'avcodec');
|
||||
fs.writeFileSync(path.join(libDir, 'libmpv.so'), 'mpv');
|
||||
fs.writeFileSync(path.join(libDir, 'libmpv.so.2'), 'mpv');
|
||||
const runtimeFiles = createRuntimeFileRecords(libDir);
|
||||
const sourceRecords = createPinnedSourceRecords();
|
||||
const abiRecords = runtimeFiles.map(({ name }) => ({
|
||||
name,
|
||||
requiredGlibc: '2.34',
|
||||
requiredGlibcxx: null,
|
||||
}));
|
||||
const manifest = createLinuxRuntimeManifest({
|
||||
sourceRecords,
|
||||
runtimeFiles,
|
||||
abiRecords,
|
||||
dependencyClosure: {
|
||||
entries: [
|
||||
{
|
||||
name: 'libavcodec.so.62',
|
||||
soname: 'libavcodec.so.62',
|
||||
needed: ['libm.so.6'],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
},
|
||||
{
|
||||
name: 'libmpv.so',
|
||||
soname: 'libmpv.so.2',
|
||||
needed: ['libavcodec.so.62', 'libEGL.so.1'],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
},
|
||||
{
|
||||
name: 'libmpv.so.2',
|
||||
soname: 'libmpv.so.2',
|
||||
needed: ['libavcodec.so.62', 'libEGL.so.1'],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
@@ -769,6 +1244,7 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) =
|
||||
platform: 'linux',
|
||||
arch: 'x64',
|
||||
release: 'fixture-kernel',
|
||||
glibcVersion: '2.35',
|
||||
systemPkgConfigDirs: [...DEFAULT_SYSTEM_PKG_CONFIG_DIRS],
|
||||
systemPkgConfigPackages: Object.fromEntries(
|
||||
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES.map((packageName) => [
|
||||
@@ -777,7 +1253,10 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) =
|
||||
])
|
||||
),
|
||||
tools: Object.fromEntries(
|
||||
REQUIRED_TOOLS.map((tool) => [tool, `${tool} fixture version`])
|
||||
REQUIRED_TOOLS.map((tool) => [
|
||||
tool,
|
||||
`${tool} ${MINIMUM_TOOL_VERSIONS[tool]}`,
|
||||
])
|
||||
),
|
||||
},
|
||||
generatedAt: '2026-07-17T00:00:00.000Z',
|
||||
@@ -792,10 +1271,25 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) =
|
||||
'libEGL.so.1',
|
||||
'libm.so.6',
|
||||
]);
|
||||
assert.equal(
|
||||
manifest.runtimeDependencyClosure.entries.find(
|
||||
({ name }) => name === 'libmpv.so'
|
||||
).soname,
|
||||
'libmpv.so.2'
|
||||
);
|
||||
assert.deepEqual(
|
||||
manifest.externalSystemLibraries,
|
||||
EXTERNAL_SYSTEM_LIBRARIES
|
||||
);
|
||||
assert.deepEqual(manifest.runtimeAbi, {
|
||||
baseline: PORTABLE_ABI_BASELINE,
|
||||
files: abiRecords,
|
||||
});
|
||||
assert.deepEqual(
|
||||
manifest.runtimeExternalConfiguration,
|
||||
RUNTIME_EXTERNAL_CONFIGURATION
|
||||
);
|
||||
assert.equal(manifest.buildHost.glibcVersion, '2.35');
|
||||
assert.deepEqual(
|
||||
Object.keys(manifest.buildHost.systemPkgConfigPackages),
|
||||
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES
|
||||
@@ -814,6 +1308,7 @@ test('generates a hash-complete manifest accepted by the Linux validator', (t) =
|
||||
);
|
||||
assert.doesNotMatch(manifest.sourceDistribution, /TBD|TODO/i);
|
||||
assert.match(manifest.sourceDistribution, /source archives/i);
|
||||
assert.match(manifest.sourceDistribution, /libdisplay-info/i);
|
||||
});
|
||||
|
||||
test('guards the CLI to Linux x64 and requires exactly one output prefix', () => {
|
||||
@@ -883,7 +1378,7 @@ test('patches every materialized ELF runtime to ORIGIN before validating closure
|
||||
builderSource,
|
||||
/runCapture\('readelf', \['-d', libraryPath\]\)/
|
||||
);
|
||||
assert.match(builderSource, /materializeLibrarySymlinks\(libDir\)/);
|
||||
assert.match(builderSource, /retainRuntimeLibraries\(libDir,/);
|
||||
assert.match(builderSource, /validateRuntimeDependencyClosure\(\{/);
|
||||
assert.match(builderSource, /createRuntimeFileRecords\(libDir\)/);
|
||||
assert.match(builderSource, /runtime-manifest\.json/);
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('node:path');
|
||||
const { isDeepStrictEqual } = require('node:util');
|
||||
const {
|
||||
EXTERNAL_SYSTEM_LIBRARIES,
|
||||
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
|
||||
GLIBC_TOOLCHAIN_ALLOWLIST,
|
||||
MINIMUM_TOOL_VERSIONS,
|
||||
PORTABLE_ABI_BASELINE,
|
||||
REQUIRED_TOOLS,
|
||||
RUNTIME_EXTERNAL_CONFIGURATION,
|
||||
SOURCE_PACKAGES,
|
||||
compareVersions,
|
||||
parseVersion,
|
||||
} = require('./build-linux-runtime.cjs');
|
||||
|
||||
const LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION = 1;
|
||||
@@ -303,19 +309,31 @@ function validateMpv(errors, mpv) {
|
||||
});
|
||||
|
||||
if (Array.isArray(mpv.mesonFlags)) {
|
||||
for (const [option, requiredFlag] of [
|
||||
['-Dgpl', '-Dgpl=false'],
|
||||
['-Dlibmpv', '-Dlibmpv=true'],
|
||||
]) {
|
||||
const assignments = mpv.mesonFlags.filter(
|
||||
(flag) =>
|
||||
typeof flag === 'string' && flag.startsWith(`${option}=`)
|
||||
);
|
||||
const assignmentsByOption = new Map();
|
||||
for (const flag of mpv.mesonFlags) {
|
||||
const match =
|
||||
typeof flag === 'string' ? flag.match(/^(-D[^=]+)=/) : null;
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const option = match[1];
|
||||
const assignments = assignmentsByOption.get(option) ?? [];
|
||||
assignments.push(flag);
|
||||
assignmentsByOption.set(option, assignments);
|
||||
}
|
||||
for (const [option, assignments] of assignmentsByOption) {
|
||||
if (assignments.length > 1) {
|
||||
errors.push(
|
||||
`Linux runtime manifest mpv.mesonFlags must assign "${option}" exactly once.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [option, requiredFlag] of [
|
||||
['-Dgpl', '-Dgpl=false'],
|
||||
['-Dlibmpv', '-Dlibmpv=true'],
|
||||
]) {
|
||||
const assignments = assignmentsByOption.get(option) ?? [];
|
||||
for (const flag of assignments) {
|
||||
if (flag !== requiredFlag) {
|
||||
errors.push(
|
||||
@@ -336,6 +354,7 @@ function validateRuntimeFiles(errors, runtimeFiles) {
|
||||
}
|
||||
|
||||
const names = new Set();
|
||||
let hasLibMpvLinkerAlias = false;
|
||||
let hasVersionedLibMpv = false;
|
||||
|
||||
for (const [index, runtimeFile] of runtimeFiles.entries()) {
|
||||
@@ -380,6 +399,9 @@ function validateRuntimeFiles(errors, runtimeFiles) {
|
||||
} else {
|
||||
names.add(name);
|
||||
}
|
||||
if (name === 'libmpv.so') {
|
||||
hasLibMpvLinkerAlias = true;
|
||||
}
|
||||
if (VERSIONED_LIBMPV_PATTERN.test(name)) {
|
||||
hasVersionedLibMpv = true;
|
||||
}
|
||||
@@ -391,6 +413,11 @@ function validateRuntimeFiles(errors, runtimeFiles) {
|
||||
'Linux runtime manifest runtimeFiles must include a versioned libmpv.so.N entry.'
|
||||
);
|
||||
}
|
||||
if (!hasLibMpvLinkerAlias) {
|
||||
errors.push(
|
||||
'Linux runtime manifest runtimeFiles must include the libmpv.so linker alias.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRuntimeTotalBytes(errors, runtimeFiles, runtimeTotalBytes) {
|
||||
@@ -416,6 +443,110 @@ function validateRuntimeTotalBytes(errors, runtimeFiles, runtimeTotalBytes) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateRuntimeAbi(errors, runtimeAbi, runtimeFiles) {
|
||||
if (!isObject(runtimeAbi)) {
|
||||
errors.push('Linux runtime manifest runtimeAbi must be an object.');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!isObject(runtimeAbi.baseline) ||
|
||||
!isDeepStrictEqual(runtimeAbi.baseline, PORTABLE_ABI_BASELINE)
|
||||
) {
|
||||
errors.push(
|
||||
'Linux runtime manifest runtimeAbi.baseline must exactly match the portable ABI baseline.'
|
||||
);
|
||||
}
|
||||
|
||||
const runtimeNames = Array.isArray(runtimeFiles)
|
||||
? runtimeFiles
|
||||
.filter((runtimeFile) => isObject(runtimeFile))
|
||||
.map(({ name }) => name)
|
||||
.filter((name) => typeof name === 'string')
|
||||
: [];
|
||||
const runtimeNameSet = new Set(runtimeNames);
|
||||
if (!Array.isArray(runtimeAbi.files)) {
|
||||
errors.push(
|
||||
'Linux runtime manifest runtimeAbi.files must contain one record for every runtime file.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const abiNames = new Set();
|
||||
for (const [index, record] of runtimeAbi.files.entries()) {
|
||||
const label = `runtimeAbi.files[${index}]`;
|
||||
if (!isObject(record)) {
|
||||
errors.push(`Linux runtime manifest ${label} must be an object.`);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!isSafeBasename(record.name) ||
|
||||
!SHARED_LIBRARY_PATTERN.test(record.name)
|
||||
) {
|
||||
errors.push(
|
||||
`Linux runtime manifest ${label}.name must be a safe shared-library basename.`
|
||||
);
|
||||
}
|
||||
if (abiNames.has(record.name)) {
|
||||
errors.push(
|
||||
`Linux runtime manifest runtimeAbi.files contains duplicate name "${String(
|
||||
record.name
|
||||
)}".`
|
||||
);
|
||||
} else if (typeof record.name === 'string') {
|
||||
abiNames.add(record.name);
|
||||
}
|
||||
|
||||
for (const [field, maximum] of [
|
||||
['requiredGlibc', PORTABLE_ABI_BASELINE.glibcMaximum],
|
||||
['requiredGlibcxx', PORTABLE_ABI_BASELINE.glibcxxMaximum],
|
||||
]) {
|
||||
const version = record[field];
|
||||
if (
|
||||
version !== null &&
|
||||
(typeof version !== 'string' ||
|
||||
!/^\d+(?:\.\d+)+$/.test(version))
|
||||
) {
|
||||
errors.push(
|
||||
`Linux runtime manifest ${label}.${field} must be null or a dotted numeric symbol version.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (version && compareVersions(version, maximum) > 0) {
|
||||
errors.push(
|
||||
`Linux runtime manifest ${label}.${field} must not exceed portable ABI maximum ${maximum}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
runtimeAbi.files.length !== runtimeNames.length ||
|
||||
runtimeNames.some((name) => !abiNames.has(name)) ||
|
||||
[...abiNames].some((name) => !runtimeNameSet.has(name))
|
||||
) {
|
||||
errors.push(
|
||||
'Linux runtime manifest runtimeAbi.files must contain one record for every runtime file.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRuntimeExternalConfiguration(
|
||||
errors,
|
||||
runtimeExternalConfiguration
|
||||
) {
|
||||
if (
|
||||
!isObject(runtimeExternalConfiguration) ||
|
||||
!isDeepStrictEqual(
|
||||
runtimeExternalConfiguration,
|
||||
RUNTIME_EXTERNAL_CONFIGURATION
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
'Linux runtime manifest runtimeExternalConfiguration must exactly match the system-owned runtime paths.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function validateRuntimeDependencyClosure(
|
||||
errors,
|
||||
runtimeDependencyClosure,
|
||||
@@ -471,6 +602,16 @@ function validateRuntimeDependencyClosure(
|
||||
entryNames.add(entry.name);
|
||||
}
|
||||
|
||||
if (
|
||||
entry.soname !== null &&
|
||||
(!isSafeBasename(entry.soname) ||
|
||||
!SHARED_LIBRARY_PATTERN.test(entry.soname))
|
||||
) {
|
||||
errors.push(
|
||||
`Linux runtime manifest ${label}.soname must be null or a safe shared-library basename.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!Array.isArray(entry.needed) ||
|
||||
entry.needed.some(
|
||||
@@ -531,6 +672,20 @@ function validateRuntimeDependencyClosure(
|
||||
);
|
||||
}
|
||||
|
||||
const libMpvLinkerEntry = entries.find(
|
||||
(entry) => isObject(entry) && entry.name === 'libmpv.so'
|
||||
);
|
||||
if (
|
||||
!libMpvLinkerEntry ||
|
||||
typeof libMpvLinkerEntry.soname !== 'string' ||
|
||||
!VERSIONED_LIBMPV_PATTERN.test(libMpvLinkerEntry.soname) ||
|
||||
!runtimeNameSet.has(libMpvLinkerEntry.soname)
|
||||
) {
|
||||
errors.push(
|
||||
'Linux runtime manifest libmpv.so must declare a versioned SONAME present in runtimeFiles.'
|
||||
);
|
||||
}
|
||||
|
||||
const expectedExternalDependencies = [
|
||||
...computedExternalDependencies,
|
||||
].sort();
|
||||
@@ -598,6 +753,23 @@ function validateBuildHost(errors, buildHost) {
|
||||
'Linux runtime manifest buildHost.release must be a non-empty string.'
|
||||
);
|
||||
}
|
||||
if (
|
||||
typeof buildHost.glibcVersion !== 'string' ||
|
||||
!/^\d+(?:\.\d+)+$/.test(buildHost.glibcVersion)
|
||||
) {
|
||||
errors.push(
|
||||
'Linux runtime manifest buildHost.glibcVersion must be a dotted numeric version.'
|
||||
);
|
||||
} else if (
|
||||
compareVersions(
|
||||
buildHost.glibcVersion,
|
||||
PORTABLE_ABI_BASELINE.glibcMaximum
|
||||
) > 0
|
||||
) {
|
||||
errors.push(
|
||||
`Linux runtime manifest buildHost.glibcVersion ${buildHost.glibcVersion} exceeds portable ABI baseline maximum ${PORTABLE_ABI_BASELINE.glibcMaximum}.`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!Array.isArray(buildHost.systemPkgConfigDirs) ||
|
||||
@@ -648,10 +820,24 @@ function validateBuildHost(errors, buildHost) {
|
||||
return;
|
||||
}
|
||||
for (const tool of REQUIRED_TOOLS) {
|
||||
if (!isNonEmptyString(buildHost.tools[tool])) {
|
||||
const declaredVersion = buildHost.tools[tool];
|
||||
if (!isNonEmptyString(declaredVersion)) {
|
||||
errors.push(
|
||||
`Linux runtime manifest buildHost.tools.${tool} must be a non-empty version string.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const actualVersion = parseVersion(declaredVersion);
|
||||
if (!actualVersion) {
|
||||
errors.push(
|
||||
`Linux runtime manifest buildHost.tools.${tool} must contain a parseable dotted numeric version.`
|
||||
);
|
||||
} else if (
|
||||
compareVersions(actualVersion, MINIMUM_TOOL_VERSIONS[tool]) < 0
|
||||
) {
|
||||
errors.push(
|
||||
`Linux runtime manifest buildHost.tools.${tool} ${actualVersion} is unsupported; requires ${MINIMUM_TOOL_VERSIONS[tool]} or newer.`
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const tool of Object.keys(buildHost.tools).sort()) {
|
||||
@@ -694,6 +880,10 @@ function validateLinuxRuntimeManifest(manifest) {
|
||||
errors.push(
|
||||
'Linux runtime manifest sourceDistribution must be a non-empty string.'
|
||||
);
|
||||
} else if (!/\blibdisplay-info\b/i.test(manifest.sourceDistribution)) {
|
||||
errors.push(
|
||||
'Linux runtime manifest sourceDistribution must explicitly include libdisplay-info.'
|
||||
);
|
||||
}
|
||||
|
||||
validateRuntimeFiles(errors, manifest.runtimeFiles);
|
||||
@@ -702,6 +892,11 @@ function validateLinuxRuntimeManifest(manifest) {
|
||||
manifest.runtimeFiles,
|
||||
manifest.runtimeTotalBytes
|
||||
);
|
||||
validateRuntimeAbi(errors, manifest.runtimeAbi, manifest.runtimeFiles);
|
||||
validateRuntimeExternalConfiguration(
|
||||
errors,
|
||||
manifest.runtimeExternalConfiguration
|
||||
);
|
||||
validateRuntimeDependencyClosure(
|
||||
errors,
|
||||
manifest.runtimeDependencyClosure,
|
||||
|
||||
@@ -19,7 +19,10 @@ const {
|
||||
EXTERNAL_SYSTEM_LIBRARIES,
|
||||
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
|
||||
GLIBC_TOOLCHAIN_ALLOWLIST,
|
||||
MINIMUM_TOOL_VERSIONS,
|
||||
PORTABLE_ABI_BASELINE,
|
||||
REQUIRED_TOOLS,
|
||||
RUNTIME_EXTERNAL_CONFIGURATION,
|
||||
SOURCE_PACKAGES,
|
||||
} = require('./build-linux-runtime.cjs');
|
||||
|
||||
@@ -65,6 +68,7 @@ function createValidManifest(
|
||||
runtimeFiles = [
|
||||
runtimeFile('libmpv.so.2', 'libmpv-runtime'),
|
||||
runtimeFile('libavcodec.so.61', 'libavcodec-runtime'),
|
||||
runtimeFile('libmpv.so', 'libmpv-runtime'),
|
||||
]
|
||||
) {
|
||||
const packages = Object.fromEntries(
|
||||
@@ -85,6 +89,9 @@ function createValidManifest(
|
||||
)
|
||||
.map((runtimeEntry) => ({
|
||||
name: runtimeEntry.name,
|
||||
soname: runtimeEntry.name.startsWith('libmpv.so')
|
||||
? 'libmpv.so.2'
|
||||
: runtimeEntry.name,
|
||||
needed: runtimeEntry.name.startsWith('libmpv.so')
|
||||
? [
|
||||
...(runtimeNames.has('libavcodec.so.61')
|
||||
@@ -124,12 +131,28 @@ function createValidManifest(
|
||||
mesonFlags: ['-Dlibmpv=true', '-Dgpl=false'],
|
||||
},
|
||||
sourceDistribution:
|
||||
'https://downloads.example.test/iptvnator/linux-runtime-sources.tar.zst',
|
||||
'Publish the exact source archives, including the pinned MIT-licensed libdisplay-info source archive.',
|
||||
runtimeFiles,
|
||||
runtimeTotalBytes: runtimeFiles.reduce(
|
||||
(total, runtimeEntry) => total + (runtimeEntry?.size ?? 0),
|
||||
0
|
||||
),
|
||||
runtimeAbi: {
|
||||
baseline: { ...PORTABLE_ABI_BASELINE },
|
||||
files: runtimeFiles
|
||||
.filter(
|
||||
(runtimeEntry) =>
|
||||
runtimeEntry && typeof runtimeEntry.name === 'string'
|
||||
)
|
||||
.map(({ name }) => ({
|
||||
name,
|
||||
requiredGlibc: '2.34',
|
||||
requiredGlibcxx: null,
|
||||
})),
|
||||
},
|
||||
runtimeExternalConfiguration: structuredClone(
|
||||
RUNTIME_EXTERNAL_CONFIGURATION
|
||||
),
|
||||
runtimeDependencyClosure: {
|
||||
entries: closureEntries,
|
||||
externalDependencies,
|
||||
@@ -141,6 +164,7 @@ function createValidManifest(
|
||||
platform: 'linux',
|
||||
arch: 'x64',
|
||||
release: 'fixture-kernel',
|
||||
glibcVersion: '2.35',
|
||||
systemPkgConfigDirs: [...DEFAULT_SYSTEM_PKG_CONFIG_DIRS],
|
||||
systemPkgConfigPackages: Object.fromEntries(
|
||||
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES.map((packageName) => [
|
||||
@@ -149,7 +173,10 @@ function createValidManifest(
|
||||
])
|
||||
),
|
||||
tools: Object.fromEntries(
|
||||
REQUIRED_TOOLS.map((tool) => [tool, `${tool} fixture version`])
|
||||
REQUIRED_TOOLS.map((tool) => [
|
||||
tool,
|
||||
`${tool} ${MINIMUM_TOOL_VERSIONS[tool]}`,
|
||||
])
|
||||
),
|
||||
},
|
||||
};
|
||||
@@ -186,6 +213,7 @@ function createFixture(t, options = {}) {
|
||||
const contentsByName = new Map([
|
||||
['libmpv.so.2', 'libmpv-runtime'],
|
||||
['libavcodec.so.61', 'libavcodec-runtime'],
|
||||
['libmpv.so', 'libmpv-runtime'],
|
||||
]);
|
||||
|
||||
for (const [name, contents] of contentsByName) {
|
||||
@@ -377,6 +405,54 @@ test('requires a complete ORIGIN-only runtime dependency closure', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('requires safe SONAME metadata and a bundled versioned libmpv target', () => {
|
||||
const missingLinkerAlias = createValidManifest();
|
||||
const aliasFile = missingLinkerAlias.runtimeFiles.find(
|
||||
({ name }) => name === 'libmpv.so'
|
||||
);
|
||||
missingLinkerAlias.runtimeFiles = missingLinkerAlias.runtimeFiles.filter(
|
||||
({ name }) => name !== 'libmpv.so'
|
||||
);
|
||||
missingLinkerAlias.runtimeTotalBytes -= aliasFile.size;
|
||||
missingLinkerAlias.runtimeAbi.files =
|
||||
missingLinkerAlias.runtimeAbi.files.filter(
|
||||
({ name }) => name !== 'libmpv.so'
|
||||
);
|
||||
missingLinkerAlias.runtimeDependencyClosure.entries =
|
||||
missingLinkerAlias.runtimeDependencyClosure.entries.filter(
|
||||
({ name }) => name !== 'libmpv.so'
|
||||
);
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(missingLinkerAlias).join('\n'),
|
||||
/runtimeFiles must include the libmpv\.so linker alias/
|
||||
);
|
||||
|
||||
const missingLibmpvSoname = createValidManifest();
|
||||
missingLibmpvSoname.runtimeDependencyClosure.entries.find(
|
||||
({ name }) => name === 'libmpv.so'
|
||||
).soname = null;
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(missingLibmpvSoname).join('\n'),
|
||||
/libmpv\.so must declare a versioned SONAME present in runtimeFiles/
|
||||
);
|
||||
|
||||
const unsafeSoname = createValidManifest();
|
||||
unsafeSoname.runtimeDependencyClosure.entries[0].soname = '../libmpv.so.2';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(unsafeSoname).join('\n'),
|
||||
/runtimeDependencyClosure\.entries\[0\]\.soname must be null or a safe shared-library basename/
|
||||
);
|
||||
|
||||
const absentSonameTarget = createValidManifest();
|
||||
absentSonameTarget.runtimeDependencyClosure.entries.find(
|
||||
({ name }) => name === 'libmpv.so'
|
||||
).soname = 'libmpv.so.99';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(absentSonameTarget).join('\n'),
|
||||
/libmpv\.so must declare a versioned SONAME present in runtimeFiles/
|
||||
);
|
||||
});
|
||||
|
||||
test('requires the deterministic external-system-library records', () => {
|
||||
const manifest = createValidManifest();
|
||||
manifest.externalSystemLibraries.reverse();
|
||||
@@ -453,6 +529,68 @@ test('requires a Linux x64 build host and every required tool version', () => {
|
||||
validateLinuxRuntimeManifest(unexpectedPackage).join('\n'),
|
||||
/buildHost\.systemPkgConfigPackages contains unexpected package "unexpected"/
|
||||
);
|
||||
|
||||
const unsupportedGlibc = createValidManifest();
|
||||
unsupportedGlibc.buildHost.glibcVersion = '2.36';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(unsupportedGlibc).join('\n'),
|
||||
/buildHost\.glibcVersion.*portable ABI baseline.*2\.35/i
|
||||
);
|
||||
|
||||
const unsupportedMeson = createValidManifest();
|
||||
unsupportedMeson.buildHost.tools.meson = 'meson 1.5.9';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(unsupportedMeson).join('\n'),
|
||||
/buildHost\.tools\.meson.*requires 1\.6\.0 or newer/i
|
||||
);
|
||||
});
|
||||
|
||||
test('requires exact portable ABI and external configuration records', () => {
|
||||
const missingAbiFile = createValidManifest();
|
||||
missingAbiFile.runtimeAbi.files.pop();
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(missingAbiFile).join('\n'),
|
||||
/runtimeAbi\.files must contain one record for every runtime file/
|
||||
);
|
||||
|
||||
const newerGlibcSymbol = createValidManifest();
|
||||
newerGlibcSymbol.runtimeAbi.files[0].requiredGlibc = '2.36';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(newerGlibcSymbol).join('\n'),
|
||||
/runtimeAbi\.files\[0\]\.requiredGlibc.*maximum 2\.35/
|
||||
);
|
||||
|
||||
const newerGlibcxxSymbol = createValidManifest();
|
||||
newerGlibcxxSymbol.runtimeAbi.files[0].requiredGlibcxx = '3.4.31';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(newerGlibcxxSymbol).join('\n'),
|
||||
/runtimeAbi\.files\[0\]\.requiredGlibcxx.*maximum 3\.4\.30/
|
||||
);
|
||||
|
||||
const wrongBaseline = createValidManifest();
|
||||
wrongBaseline.runtimeAbi.baseline.distribution = 'current host';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(wrongBaseline).join('\n'),
|
||||
/runtimeAbi\.baseline must exactly match the portable ABI baseline/
|
||||
);
|
||||
|
||||
const buildPathConfiguration = createValidManifest();
|
||||
buildPathConfiguration.runtimeExternalConfiguration.fontconfig.configDirectory =
|
||||
'/tmp/build-prefix/etc/fonts';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(buildPathConfiguration).join('\n'),
|
||||
/runtimeExternalConfiguration must exactly match the system-owned runtime paths/
|
||||
);
|
||||
});
|
||||
|
||||
test('requires the source-distribution statement to name libdisplay-info', () => {
|
||||
const manifest = createValidManifest();
|
||||
manifest.sourceDistribution =
|
||||
'Publish all of the other source archives with the binary release.';
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(manifest).join('\n'),
|
||||
/sourceDistribution must explicitly include libdisplay-info/
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts and stages the current Linux CI system build-input manifest', (t) => {
|
||||
@@ -690,6 +828,15 @@ test('rejects duplicate or contradictory required mpv Meson assignments', () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects duplicate assignments for every mpv Meson option', () => {
|
||||
const manifest = createValidManifest();
|
||||
manifest.mpv.mesonFlags.push('-Ddrm=enabled', '-Ddrm=enabled');
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(manifest).join('\n'),
|
||||
/mpv\.mesonFlags must assign "-Ddrm" exactly once/
|
||||
);
|
||||
});
|
||||
|
||||
test('validates safe, unique shared-library metadata', () => {
|
||||
const manifest = createValidManifest([
|
||||
{
|
||||
@@ -729,32 +876,20 @@ test('rejects control characters in shared-library basenames', () => {
|
||||
|
||||
assert.match(
|
||||
validateLinuxRuntimeManifest(manifest).join('\n'),
|
||||
/runtimeFiles\[2\]\.name must be a safe shared-library basename/
|
||||
/runtimeFiles\[3\]\.name must be a safe shared-library basename/
|
||||
);
|
||||
});
|
||||
|
||||
test('stages only declared Linux libraries and materializes source symlinks', (t) => {
|
||||
const fixture = createFixture(t);
|
||||
const versionedMpvContents = fs.readFileSync(
|
||||
path.join(fixture.libDir, 'libmpv.so.2')
|
||||
);
|
||||
fs.renameSync(
|
||||
path.join(fixture.libDir, 'libmpv.so.2'),
|
||||
path.join(fixture.libDir, 'libmpv.so.2.1.0')
|
||||
);
|
||||
fs.rmSync(path.join(fixture.libDir, 'libmpv.so'));
|
||||
fs.symlinkSync('libmpv.so.2.1.0', path.join(fixture.libDir, 'libmpv.so.2'));
|
||||
fs.symlinkSync('libmpv.so.2', path.join(fixture.libDir, 'libmpv.so'));
|
||||
fs.writeFileSync(path.join(fixture.libDir, 'libundeclared.so.1'), 'extra');
|
||||
fixture.manifest.runtimeFiles.push(
|
||||
runtimeFile('libmpv.so', versionedMpvContents)
|
||||
);
|
||||
fixture.manifest.runtimeTotalBytes += versionedMpvContents.length;
|
||||
fixture.manifest.runtimeDependencyClosure.entries.push({
|
||||
name: 'libmpv.so',
|
||||
needed: ['libmpv.so.2'],
|
||||
rpath: [],
|
||||
runpath: ['$ORIGIN'],
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(fixture.prefix, 'runtime-manifest.json'),
|
||||
`${JSON.stringify(fixture.manifest, null, 2)}\n`
|
||||
@@ -809,6 +944,8 @@ test('stages only declared Linux libraries and materializes source symlinks', (t
|
||||
for (const field of [
|
||||
'packages',
|
||||
'runtimeTotalBytes',
|
||||
'runtimeAbi',
|
||||
'runtimeExternalConfiguration',
|
||||
'runtimeDependencyClosure',
|
||||
'externalSystemLibraries',
|
||||
'buildHost',
|
||||
|
||||
Reference in new issue
Block a user