Files
iptvnator/tools/embedded-mpv/linux-runtime-manifest.cjs
4gray 8fdac824fd feat(packaging): ship Linux embedded MPV frame-copy runtime (#1200)
* docs: design Linux frame-copy packaging

* docs: plan Linux frame-copy packaging

* feat(packaging): define Linux frame-copy profiles

* fix(packaging): reject inherited profile names

* feat(embedded-mpv): validate staged Linux runtime

* fix(embedded-mpv): require Linux source packages

* fix(embedded-mpv): harden Linux runtime staging

* feat(embedded-mpv): build LGPL Linux runtime

* fix(embedded-mpv): pin Linux runtime inputs

* feat(embedded-mpv): build relocatable Linux helper

* fix(embedded-mpv): require bundled Linux runtime

* fix(embedded-mpv): make Linux runtime portable

* feat(packaging): ship Linux frame-copy artifacts

* fix(embedded-mpv): verify Linux helper linkage

* fix(packaging): enforce Linux frame-copy isolation

* fix(embedded-mpv): pin Linux display data

* docs(embedded-mpv): document Linux frame-copy packaging

* feat(embedded-mpv): probe Linux frame-copy runtime

* test(embedded-mpv): smoke packaged Linux frame-copy

* docs(embedded-mpv): clarify Linux system runtime baseline

* fix(embedded-mpv): harden Linux runtime capability gate

* ci: verify Linux frame-copy packages

* test(embedded-mpv): harden packaged Linux smoke

* test(embedded-mpv): preserve packaged GL mode

* test(packaging): harden Linux package probes

* fix(embedded-mpv): enable private Snap shared memory

* fix(embedded-mpv): sanitize Linux helper environment

* fix(packaging): enforce private Snap memory semantics

* fix(packaging): reject ambiguous Snap memory metadata

* fix(embedded-mpv): prioritize trusted Snap GL

* fix(packaging): reject advanced Snap YAML semantics

* fix(packaging): reject arbitrary Snap YAML aliases

* feat(packaging): ship Linux runtime license notices

* docs(embedded-mpv): document Linux runtime distribution

* fix(packaging): parse Snap trailing comments safely

* fix(release): gate Snap publish on public source release

* fix(packaging): strip VCS metadata from source bundle

* docs(packaging): clarify Linux source release gate

* test(embedded-mpv): smoke missing bundled libmpv

* style(embedded-mpv): format final validation inputs

* fix(e2e): satisfy fixture index signature typing

* fix(ci): declare fontconfig gperf generator

* fix(embedded-mpv): hash runtime cache identities

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): tighten runtime delivery gates

* fix(ci): decouple Linux runtime matrix

* fix(packaging): harden Linux frame-copy delivery

* fix(packaging): validate Linux frame-copy runtimes

* fix(packaging): scope Snap Electron library checks

* feat(packaging): ship Linux frame-copy runtimes

* fix(packaging): improve Linux runtime smoke diagnostics

* fix(packaging): expose bounded helper probe details

* test(packaging): trace Snap EGL probe failures

* fix(packaging): prefer core22 ABI in Snap helper

* fix(packaging): bound helper probe capture

* fix(packaging): harden Linux frame-copy releases

* fix(packaging): canonicalize libplacebo submodule identity

* fix(packaging): make source archive inspection portable

* fix(packaging): harden Snap release verification
2026-07-18 17:28:22 +02:00

1000 lines
33 KiB
JavaScript

'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;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const GIT_COMMIT_PATTERN = /^[a-f0-9]{40,64}$/;
const SAFE_BASENAME_PATTERN = /^[A-Za-z0-9_+.-]+$/;
const SHARED_LIBRARY_PATTERN = /\.so(?:\.\d+)*$/;
const VERSIONED_LIBMPV_PATTERN = /^libmpv\.so\.\d+(?:\.\d+)*$/;
const REQUIRED_SOURCE_PACKAGES = SOURCE_PACKAGES.map(({ id }) => id);
const SOURCE_PACKAGE_BY_ID = new Map(
SOURCE_PACKAGES.map((sourcePackage) => [sourcePackage.id, sourcePackage])
);
const ALLOWED_EXTERNAL_LIBRARY_NAMES = new Set([
...GLIBC_TOOLCHAIN_ALLOWLIST,
...EXTERNAL_SYSTEM_LIBRARIES.map(({ name }) => name),
]);
const SUBMODULE_RECORD_PATTERN = /^[a-f0-9]{40,64}\s+([A-Za-z0-9_+./-]+)$/;
const LINUX_SYSTEM_BACKEND = 'process-isolated mpv --wid';
function isObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
function isSafeBasename(value) {
return (
isNonEmptyString(value) &&
path.basename(value) === value &&
!value.includes('/') &&
!value.includes('\\') &&
value !== '.' &&
value !== '..' &&
SAFE_BASENAME_PATTERN.test(value)
);
}
function isValidSubmoduleRecord(value) {
if (typeof value !== 'string') {
return false;
}
const match = value.match(SUBMODULE_RECORD_PATTERN);
if (!match) {
return false;
}
const submodulePath = match[1];
return (
!path.posix.isAbsolute(submodulePath) &&
!submodulePath
.split('/')
.some((segment) => segment === '.' || segment === '..')
);
}
function validateSourceMetadata(errors, value, label) {
if (!isNonEmptyString(value.version)) {
errors.push(
`Linux runtime manifest ${label}.version must be a non-empty string.`
);
}
if (!isNonEmptyString(value.sourceUrl)) {
errors.push(
`Linux runtime manifest ${label}.sourceUrl must be a non-empty string.`
);
}
const hasSourceSha256 = value.sourceSha256 !== undefined;
const hasSourceGitCommit = value.sourceGitCommit !== undefined;
if (!hasSourceSha256 && !hasSourceGitCommit) {
errors.push(
`Linux runtime manifest ${label} must include sourceSha256 or sourceGitCommit.`
);
return;
}
if (
hasSourceSha256 &&
(typeof value.sourceSha256 !== 'string' ||
!SHA256_PATTERN.test(value.sourceSha256))
) {
errors.push(
`Linux runtime manifest ${label}.sourceSha256 must be a lowercase 64-character hexadecimal digest.`
);
}
if (
hasSourceGitCommit &&
(typeof value.sourceGitCommit !== 'string' ||
!GIT_COMMIT_PATTERN.test(value.sourceGitCommit))
) {
errors.push(
`Linux runtime manifest ${label}.sourceGitCommit must be a lowercase hexadecimal commit digest.`
);
}
}
function validatePackages(errors, packages) {
if (!isObject(packages)) {
errors.push(
'Linux runtime manifest packages must contain source package metadata.'
);
return;
}
if (Object.keys(packages).length === 0) {
errors.push(
'Linux runtime manifest packages must contain source package metadata.'
);
}
const invalidRequiredPackages = new Set();
for (const packageName of REQUIRED_SOURCE_PACKAGES) {
if (
!Object.prototype.hasOwnProperty.call(packages, packageName) ||
!isObject(packages[packageName])
) {
errors.push(
`Linux runtime manifest packages.${packageName} must be an object.`
);
invalidRequiredPackages.add(packageName);
}
}
for (const packageName of Object.keys(packages).sort()) {
if (!SOURCE_PACKAGE_BY_ID.has(packageName)) {
errors.push(
`Linux runtime manifest packages contains unexpected source package "${packageName}".`
);
}
}
for (const packageName of Object.keys(packages).sort()) {
const packageMetadata = packages[packageName];
const label = `packages.${packageName}`;
if (!isObject(packageMetadata)) {
if (!invalidRequiredPackages.has(packageName)) {
errors.push(
`Linux runtime manifest ${label} must be an object.`
);
}
continue;
}
validateSourceMetadata(errors, packageMetadata, label);
if (!isNonEmptyString(packageMetadata.license)) {
errors.push(
`Linux runtime manifest ${label}.license must be a non-empty string.`
);
}
const pinnedPackage = SOURCE_PACKAGE_BY_ID.get(packageName);
if (!pinnedPackage) {
continue;
}
for (const field of ['version', 'sourceUrl', 'license']) {
if (
isNonEmptyString(packageMetadata[field]) &&
packageMetadata[field] !== pinnedPackage[field]
) {
errors.push(
`Linux runtime manifest ${label}.${field} must equal the pinned value.`
);
}
}
if (pinnedPackage.sourceTag) {
if (packageMetadata.sourceTag !== pinnedPackage.sourceTag) {
errors.push(
`Linux runtime manifest ${label}.sourceTag must equal the pinned tag.`
);
}
} else if (packageMetadata.sourceTag !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include sourceTag.`
);
}
if (pinnedPackage.buildInput) {
if (
!isObject(packageMetadata.buildInput) ||
!isDeepStrictEqual(
packageMetadata.buildInput,
pinnedPackage.buildInput
)
) {
errors.push(
`Linux runtime manifest ${label}.buildInput must equal the pinned build input.`
);
}
} else if (packageMetadata.buildInput !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include buildInput.`
);
}
if (pinnedPackage.sourceKind === 'archive') {
if (
typeof packageMetadata.sourceSha256 === 'string' &&
SHA256_PATTERN.test(packageMetadata.sourceSha256) &&
packageMetadata.sourceSha256 !== pinnedPackage.expectedSha256
) {
errors.push(
`Linux runtime manifest ${label}.sourceSha256 must equal the pinned digest.`
);
}
if (packageMetadata.sourceGitCommit !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include sourceGitCommit.`
);
}
if (packageMetadata.sourceSubmodules !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include sourceSubmodules.`
);
}
continue;
}
if (
typeof packageMetadata.sourceGitCommit === 'string' &&
GIT_COMMIT_PATTERN.test(packageMetadata.sourceGitCommit) &&
packageMetadata.sourceGitCommit !== pinnedPackage.expectedGitCommit
) {
errors.push(
`Linux runtime manifest ${label}.sourceGitCommit must equal the pinned commit.`
);
}
if (packageMetadata.sourceSha256 !== undefined) {
errors.push(
`Linux runtime manifest ${label} must not include sourceSha256.`
);
}
if (
!Array.isArray(packageMetadata.sourceSubmodules) ||
packageMetadata.sourceSubmodules.length === 0 ||
packageMetadata.sourceSubmodules.some(
(record) => !isValidSubmoduleRecord(record)
) ||
new Set(packageMetadata.sourceSubmodules).size !==
packageMetadata.sourceSubmodules.length
) {
errors.push(
`Linux runtime manifest ${label}.sourceSubmodules must be a non-empty array of commit-and-path records.`
);
} else if (
!isDeepStrictEqual(
packageMetadata.sourceSubmodules,
pinnedPackage.expectedSubmodules
)
) {
errors.push(
`Linux runtime manifest ${label}.sourceSubmodules must equal the pinned records.`
);
}
}
}
function validateFlags(errors, value, label, options) {
if (
!Array.isArray(value) ||
value.some((flag) => typeof flag !== 'string')
) {
errors.push(
`Linux runtime manifest ${label} must be an array of strings.`
);
return;
}
const addForbiddenErrors = () => {
for (const forbiddenFlag of options.forbidden) {
if (value.includes(forbiddenFlag)) {
errors.push(
`Linux runtime manifest ${label} must not include "${forbiddenFlag}".`
);
}
}
};
const addRequiredErrors = () => {
for (const requiredFlag of options.required) {
if (!value.includes(requiredFlag)) {
errors.push(
`Linux runtime manifest ${label} must include "${requiredFlag}".`
);
}
}
};
if (options.requiredFirst) {
addRequiredErrors();
addForbiddenErrors();
} else {
addForbiddenErrors();
addRequiredErrors();
}
}
function validateFfmpeg(errors, ffmpeg) {
if (!isObject(ffmpeg)) {
errors.push('Linux runtime manifest ffmpeg must be an object.');
return;
}
validateFlags(errors, ffmpeg.configureFlags, 'ffmpeg.configureFlags', {
forbidden: ['--enable-gpl', '--enable-nonfree'],
required: ['--disable-gpl', '--disable-nonfree'],
});
}
function validateMpv(errors, mpv) {
if (!isObject(mpv)) {
errors.push('Linux runtime manifest mpv must be an object.');
return;
}
validateFlags(errors, mpv.mesonFlags, 'mpv.mesonFlags', {
forbidden: [],
required: ['-Dgpl=false', '-Dlibmpv=true'],
requiredFirst: true,
});
if (Array.isArray(mpv.mesonFlags)) {
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(
`Linux runtime manifest mpv.mesonFlags must not include "${flag}".`
);
}
}
}
}
}
function validateRuntimeFiles(errors, runtimeFiles) {
if (!Array.isArray(runtimeFiles) || runtimeFiles.length === 0) {
errors.push(
'Linux runtime manifest runtimeFiles must be a non-empty array.'
);
return;
}
const names = new Set();
let hasLibMpvLinkerAlias = false;
let hasVersionedLibMpv = false;
for (const [index, runtimeFile] of runtimeFiles.entries()) {
const label = `runtimeFiles[${index}]`;
if (!isObject(runtimeFile)) {
errors.push(`Linux runtime manifest ${label} must be an object.`);
continue;
}
const { name, sha256, size } = runtimeFile;
const safeName = isSafeBasename(name);
if (!safeName) {
errors.push(
`Linux runtime manifest ${label}.name must be a safe shared-library basename.`
);
}
if (
typeof name === 'string' &&
safeName &&
!SHARED_LIBRARY_PATTERN.test(name)
) {
errors.push(
`Linux runtime manifest ${label}.name must end in ".so" or a numeric ".so.N" suffix.`
);
}
if (!Number.isInteger(size) || size <= 0) {
errors.push(
`Linux runtime manifest ${label}.size must be a positive integer.`
);
}
if (typeof sha256 !== 'string' || !SHA256_PATTERN.test(sha256)) {
errors.push(
`Linux runtime manifest ${label}.sha256 must be a lowercase 64-character hexadecimal digest.`
);
}
if (typeof name === 'string') {
if (names.has(name)) {
errors.push(
`Linux runtime manifest runtimeFiles contains duplicate name "${name}".`
);
} else {
names.add(name);
}
if (name === 'libmpv.so') {
hasLibMpvLinkerAlias = true;
}
if (VERSIONED_LIBMPV_PATTERN.test(name)) {
hasVersionedLibMpv = true;
}
}
}
if (!hasVersionedLibMpv) {
errors.push(
'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) {
const expectedTotal = Array.isArray(runtimeFiles)
? runtimeFiles.reduce(
(total, runtimeFile) =>
total +
(isObject(runtimeFile) &&
Number.isInteger(runtimeFile.size) &&
runtimeFile.size > 0
? runtimeFile.size
: 0),
0
)
: 0;
if (
!Number.isInteger(runtimeTotalBytes) ||
runtimeTotalBytes !== expectedTotal
) {
errors.push(
`Linux runtime manifest runtimeTotalBytes must equal the sum of runtimeFiles sizes (${expectedTotal}).`
);
}
}
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,
runtimeFiles
) {
if (!isObject(runtimeDependencyClosure)) {
errors.push(
'Linux runtime manifest runtimeDependencyClosure must be an object.'
);
return;
}
const runtimeNames = Array.isArray(runtimeFiles)
? runtimeFiles
.filter((runtimeFile) => isObject(runtimeFile))
.map(({ name }) => name)
.filter((name) => typeof name === 'string')
: [];
const runtimeNameSet = new Set(runtimeNames);
const { entries, externalDependencies } = runtimeDependencyClosure;
if (!Array.isArray(entries)) {
errors.push(
'Linux runtime manifest runtimeDependencyClosure.entries must contain one record for every runtime file.'
);
return;
}
const entryNames = new Set();
const computedExternalDependencies = new Set();
for (const [index, entry] of entries.entries()) {
const label = `runtimeDependencyClosure.entries[${index}]`;
if (!isObject(entry)) {
errors.push(`Linux runtime manifest ${label} must be an object.`);
continue;
}
if (
!isSafeBasename(entry.name) ||
!SHARED_LIBRARY_PATTERN.test(entry.name)
) {
errors.push(
`Linux runtime manifest ${label}.name must be a safe shared-library basename.`
);
}
if (entryNames.has(entry.name)) {
errors.push(
`Linux runtime manifest runtimeDependencyClosure.entries contains duplicate name "${String(
entry.name
)}".`
);
} else if (typeof entry.name === 'string') {
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(
(dependencyName) =>
!isSafeBasename(dependencyName) ||
!SHARED_LIBRARY_PATTERN.test(dependencyName)
)
) {
errors.push(
`Linux runtime manifest ${label}.needed must be an array of safe shared-library names.`
);
} else {
const neededNames = new Set();
for (const dependencyName of entry.needed) {
if (neededNames.has(dependencyName)) {
errors.push(
`Linux runtime manifest ${label}.needed contains duplicate name "${dependencyName}".`
);
continue;
}
neededNames.add(dependencyName);
if (runtimeNameSet.has(dependencyName)) {
continue;
}
if (!ALLOWED_EXTERNAL_LIBRARY_NAMES.has(dependencyName)) {
errors.push(
`Linux runtime manifest dependency ${dependencyName} is not in the deterministic system-library allowlist.`
);
continue;
}
computedExternalDependencies.add(dependencyName);
}
}
if (!Array.isArray(entry.rpath) || entry.rpath.length !== 0) {
errors.push(
`Linux runtime manifest ${label}.rpath must be an empty array.`
);
}
if (
!Array.isArray(entry.runpath) ||
entry.runpath.length !== 1 ||
entry.runpath[0] !== '$ORIGIN'
) {
errors.push(
`Linux runtime manifest ${label}.runpath must contain only "$ORIGIN".`
);
}
}
if (
entries.length !== runtimeNames.length ||
runtimeNames.some((name) => !entryNames.has(name)) ||
[...entryNames].some((name) => !runtimeNameSet.has(name))
) {
errors.push(
'Linux runtime manifest runtimeDependencyClosure.entries must contain one record for every runtime file.'
);
}
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();
if (
!Array.isArray(externalDependencies) ||
externalDependencies.some(
(dependencyName) =>
!isSafeBasename(dependencyName) ||
!SHARED_LIBRARY_PATTERN.test(dependencyName)
) ||
JSON.stringify(externalDependencies) !==
JSON.stringify(expectedExternalDependencies)
) {
errors.push(
'Linux runtime manifest runtimeDependencyClosure.externalDependencies must exactly match the sorted allowlisted external dependency set.'
);
}
}
function validateExternalSystemLibraries(errors, externalSystemLibraries) {
const expectedKeys = ['interface', 'name', 'reason'];
const matchesDeterministicAllowlist =
Array.isArray(externalSystemLibraries) &&
externalSystemLibraries.length === EXTERNAL_SYSTEM_LIBRARIES.length &&
externalSystemLibraries.every((externalLibrary, index) => {
if (!isObject(externalLibrary)) {
return false;
}
const actualKeys = Object.keys(externalLibrary).sort();
if (
actualKeys.length !== expectedKeys.length ||
actualKeys.some(
(key, keyIndex) => key !== expectedKeys[keyIndex]
)
) {
return false;
}
const expectedLibrary = EXTERNAL_SYSTEM_LIBRARIES[index];
return expectedKeys.every(
(key) => externalLibrary[key] === expectedLibrary[key]
);
});
if (!matchesDeterministicAllowlist) {
errors.push(
'Linux runtime manifest externalSystemLibraries must exactly match the deterministic allowlist.'
);
}
}
function validateBuildHost(errors, buildHost) {
if (!isObject(buildHost)) {
errors.push('Linux runtime manifest buildHost must be an object.');
return;
}
if (buildHost.platform !== 'linux') {
errors.push(
'Linux runtime manifest buildHost.platform must be "linux".'
);
}
if (buildHost.arch !== 'x64') {
errors.push('Linux runtime manifest buildHost.arch must be "x64".');
}
if (!isNonEmptyString(buildHost.release)) {
errors.push(
'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) ||
buildHost.systemPkgConfigDirs.length === 0 ||
buildHost.systemPkgConfigDirs.some(
(directory) =>
!isNonEmptyString(directory) || !path.isAbsolute(directory)
) ||
new Set(buildHost.systemPkgConfigDirs).size !==
buildHost.systemPkgConfigDirs.length
) {
errors.push(
'Linux runtime manifest buildHost.systemPkgConfigDirs must be a non-empty array of unique absolute paths.'
);
}
if (!isObject(buildHost.systemPkgConfigPackages)) {
errors.push(
'Linux runtime manifest buildHost.systemPkgConfigPackages must be an object.'
);
} else {
for (const packageName of EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES) {
if (
!isNonEmptyString(
buildHost.systemPkgConfigPackages[packageName]
)
) {
errors.push(
`Linux runtime manifest buildHost.systemPkgConfigPackages.${packageName} must be a non-empty version string.`
);
}
}
for (const packageName of Object.keys(
buildHost.systemPkgConfigPackages
).sort()) {
if (!EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES.includes(packageName)) {
errors.push(
`Linux runtime manifest buildHost.systemPkgConfigPackages contains unexpected package "${packageName}".`
);
}
}
}
if (!isObject(buildHost.tools)) {
errors.push(
'Linux runtime manifest buildHost.tools must be an object.'
);
return;
}
for (const tool of REQUIRED_TOOLS) {
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()) {
if (!REQUIRED_TOOLS.includes(tool)) {
errors.push(
`Linux runtime manifest buildHost.tools contains unexpected tool "${tool}".`
);
}
}
}
function validateLinuxRuntimeManifest(manifest) {
if (!isObject(manifest)) {
return ['Linux runtime manifest must be an object.'];
}
const errors = [];
if (manifest.schemaVersion !== LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION) {
errors.push(
`Linux runtime manifest schemaVersion must be ${LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION}.`
);
}
if (manifest.origin !== 'vendored-lgpl-source-build') {
errors.push(
'Linux runtime manifest origin must be "vendored-lgpl-source-build".'
);
}
if (manifest.platform !== 'linux') {
errors.push('Linux runtime manifest platform must be "linux".');
}
if (manifest.arch !== 'x64') {
errors.push('Linux runtime manifest arch must be "x64".');
}
validatePackages(errors, manifest.packages);
validateFfmpeg(errors, manifest.ffmpeg);
validateMpv(errors, manifest.mpv);
if (!isNonEmptyString(manifest.sourceDistribution)) {
errors.push(
'Linux runtime manifest sourceDistribution must be a non-empty string.'
);
} else {
for (const [sourceName, sourcePattern] of [
['hwdata', /\bhwdata\b/i],
['pnp.ids', /\bpnp\.ids\b/i],
['libdisplay-info', /\blibdisplay-info\b/i],
]) {
if (!sourcePattern.test(manifest.sourceDistribution)) {
errors.push(
`Linux runtime manifest sourceDistribution must explicitly include ${sourceName}.`
);
}
}
}
validateRuntimeFiles(errors, manifest.runtimeFiles);
validateRuntimeTotalBytes(
errors,
manifest.runtimeFiles,
manifest.runtimeTotalBytes
);
validateRuntimeAbi(errors, manifest.runtimeAbi, manifest.runtimeFiles);
validateRuntimeExternalConfiguration(
errors,
manifest.runtimeExternalConfiguration
);
validateRuntimeDependencyClosure(
errors,
manifest.runtimeDependencyClosure,
manifest.runtimeFiles
);
validateExternalSystemLibraries(errors, manifest.externalSystemLibraries);
validateBuildHost(errors, manifest.buildHost);
return errors;
}
function isLinuxSystemBuildInputManifest(manifest) {
return isObject(manifest) && manifest.linuxBackend === LINUX_SYSTEM_BACKEND;
}
function validateLinuxSystemBuildInputManifest(manifest) {
if (!isObject(manifest)) {
return ['Linux system build-input manifest must be an object.'];
}
const errors = [];
if (manifest.linuxBackend !== LINUX_SYSTEM_BACKEND) {
errors.push(
`Linux system build-input manifest linuxBackend must be "${LINUX_SYSTEM_BACKEND}".`
);
}
if (!isObject(manifest.buildInputs)) {
errors.push(
'Linux system build-input manifest buildInputs must be an object.'
);
} else {
for (const packageName of ['libmpvDevPackage', 'mpvPackage']) {
if (!isNonEmptyString(manifest.buildInputs[packageName])) {
errors.push(
`Linux system build-input manifest buildInputs.${packageName} must be a non-empty string.`
);
}
}
}
if (!isNonEmptyString(manifest.sourceDistribution)) {
errors.push(
'Linux system build-input manifest sourceDistribution must be a non-empty string.'
);
}
if (Object.prototype.hasOwnProperty.call(manifest, 'origin')) {
errors.push(
'Linux system build-input manifest must not include origin.'
);
}
if (Object.prototype.hasOwnProperty.call(manifest, 'runtimeFiles')) {
errors.push(
'Linux system build-input manifest must not include runtimeFiles.'
);
}
return errors;
}
module.exports = {
LINUX_SYSTEM_BACKEND,
LINUX_RUNTIME_MANIFEST_SCHEMA_VERSION,
isLinuxSystemBuildInputManifest,
validateLinuxRuntimeManifest,
validateLinuxSystemBuildInputManifest,
};