mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* 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
1000 lines
33 KiB
JavaScript
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,
|
|
};
|