Files
iptvnator/tools/embedded-mpv/build-linux-runtime.mjs
T
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

866 lines
26 KiB
JavaScript

#!/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';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
const require = createRequire(import.meta.url);
const {
BUILD_RECIPES,
BUILD_ORDER,
EXPECTED_SYSTEM_PKG_CONFIG_PACKAGES,
MPV_MESON_FLAGS,
PORTABLE_ABI_BASELINE,
REQUIRED_TOOLS,
SOURCE_PACKAGES,
assertArchiveMatchesPin,
assertGitCommitMatchesPin,
assertMinimumToolVersions,
assertOwnedOutputDestination,
assertPortableAbiRecords,
assertPortableBuildHostGlibc,
assertUniqueMesonOptionAssignments,
canonicalizeGitSubmoduleStatus,
createBuildEnvironment,
createLinuxRuntimeManifest,
createOwnedStagingPrefix,
createRuntimeFileRecords,
ownedStagingPrefixPath,
parseCliInvocation,
parseReadelfDynamic,
parseReadelfVersionInfo,
preparePinnedHwdataBuildInput,
retainRuntimeLibraries,
resolveLinuxPackageBuildEnvironment,
resolveSystemPkgConfigDirs,
runtimeLibraryNames,
selectReachableRuntimeLibraryNames,
sha256Buffer,
publishOwnedOutput,
validateRuntimeDependencyClosure,
} = require('./build-linux-runtime.cjs');
const {
validateLinuxRuntimeManifest,
} = require('./linux-runtime-manifest.cjs');
const scriptPath = fileURLToPath(import.meta.url);
const workspaceRoot = path.resolve(path.dirname(scriptPath), '..', '..');
const sourcePackageById = new Map(
SOURCE_PACKAGES.map((sourcePackage) => [sourcePackage.id, sourcePackage])
);
function log(message) {
process.stdout.write(`[embedded-mpv-linux-runtime] ${message}\n`);
}
function commandLine(command, args) {
return [command, ...args]
.map((value) =>
/^[A-Za-z0-9_./:=+,-]+$/.test(value) ? value : JSON.stringify(value)
)
.join(' ');
}
function spawn(command, args, options, capture) {
log(commandLine(command, args));
const result = spawnSync(command, args, {
cwd: options.cwd,
env: options.env,
encoding: capture ? 'utf8' : undefined,
stdio: capture ? 'pipe' : 'inherit',
});
if (result.error) {
throw new Error(
`Unable to run ${commandLine(command, args)}: ${result.error.message}`
);
}
if (result.status !== 0) {
const details = capture
? [result.stdout, result.stderr].filter(Boolean).join('\n').trim()
: '';
throw new Error(
`${commandLine(command, args)} failed with status ${
result.status ?? 1
}.${details ? `\n${details}` : ''}`
);
}
return result;
}
function createCommandRunner({ buildEnvironment }) {
return {
run(command, args, options = {}) {
spawn(
command,
args,
{
cwd: options.cwd ?? workspaceRoot,
env: options.env ?? buildEnvironment,
},
false
);
},
runCapture(command, args, options = {}) {
const result = spawn(
command,
args,
{
cwd: options.cwd ?? workspaceRoot,
env: options.env ?? buildEnvironment,
},
true
);
return [result.stdout, result.stderr]
.filter(Boolean)
.join('\n')
.trim();
},
};
}
function commandExists(command) {
const result = spawnSync(
'sh',
['-c', 'command -v "$1" >/dev/null 2>&1', 'sh', command],
{ stdio: 'ignore' }
);
return result.status === 0;
}
function ensureTools() {
const missingTools = REQUIRED_TOOLS.filter(
(command) => !commandExists(command)
);
if (missingTools.length > 0) {
throw new Error(
`Missing required Linux runtime build tools: ${missingTools.join(
', '
)}.`
);
}
}
function resolveParallelism(environment) {
const explicitJobs = environment.IPTVNATOR_EMBEDDED_MPV_JOBS;
const makeJobs = environment.MAKEFLAGS?.match(
/(?:^|\s)-j\s*(\d+)(?:\s|$)/
)?.[1];
const value =
explicitJobs ??
makeJobs ??
String(os.availableParallelism?.() ?? os.cpus().length);
if (!/^[1-9]\d*$/.test(value)) {
throw new Error(
`IPTVNATOR_EMBEDDED_MPV_JOBS must be a positive integer; received ${value}.`
);
}
return value;
}
function containsPath(parentPath, candidatePath) {
const relativePath = path.relative(parentPath, candidatePath);
return (
relativePath === '' ||
(!relativePath.startsWith(`..${path.sep}`) &&
relativePath !== '..' &&
!path.isAbsolute(relativePath))
);
}
function assertSafeOutputPrefix(prefix, buildRoot) {
const filesystemRoot = path.parse(prefix).root;
if (
prefix === filesystemRoot ||
containsPath(prefix, workspaceRoot) ||
containsPath(prefix, buildRoot)
) {
throw new Error(
`Refusing unsafe output prefix ${prefix}; choose a dedicated directory that does not contain the repository or build cache.`
);
}
}
function archiveExtension(sourceUrl) {
for (const extension of ['.tar.xz', '.tar.gz', '.tar.bz2', '.tgz']) {
if (new URL(sourceUrl).pathname.endsWith(extension)) {
return extension;
}
}
throw new Error(`Unsupported source archive URL: ${sourceUrl}`);
}
function archivePathFor(sourcePackage, archiveRoot) {
return path.join(
archiveRoot,
`${sourcePackage.id}-${sourcePackage.version}${archiveExtension(
sourcePackage.sourceUrl
)}`
);
}
function sourcePathFor(packageId, sourceRoot) {
return path.join(sourceRoot, packageId);
}
function sha256File(filePath) {
return sha256Buffer(fs.readFileSync(filePath));
}
function downloadArchive(sourcePackage, context) {
const archivePath = archivePathFor(sourcePackage, context.archiveRoot);
if (!fs.existsSync(archivePath)) {
const temporaryArchivePath = `${archivePath}.partial`;
fs.rmSync(temporaryArchivePath, { force: true });
context.run('curl', [
'--fail',
'--location',
'--retry',
'3',
'--retry-all-errors',
'--connect-timeout',
'30',
'--proto',
'=https',
'--tlsv1.2',
'--output',
temporaryArchivePath,
sourcePackage.sourceUrl,
]);
fs.renameSync(temporaryArchivePath, archivePath);
}
const sourceSha256 = sha256File(archivePath);
assertArchiveMatchesPin(sourcePackage, sourceSha256);
const packageSourcePath = sourcePathFor(
sourcePackage.id,
context.sourceRoot
);
fs.rmSync(packageSourcePath, { recursive: true, force: true });
fs.mkdirSync(packageSourcePath, { recursive: true });
context.run('tar', [
'--extract',
'--file',
archivePath,
'--directory',
packageSourcePath,
'--strip-components',
'1',
'--no-same-owner',
]);
return {
...sourcePackage,
sourceSha256,
};
}
function cloneGitSource(sourcePackage, context) {
const packageSourcePath = sourcePathFor(
sourcePackage.id,
context.sourceRoot
);
fs.rmSync(packageSourcePath, { recursive: true, force: true });
context.run('git', [
'clone',
'--depth',
'1',
'--branch',
sourcePackage.sourceTag,
sourcePackage.sourceUrl,
packageSourcePath,
]);
const sourceGitCommit = context.runCapture('git', ['rev-parse', 'HEAD'], {
cwd: packageSourcePath,
});
assertGitCommitMatchesPin(sourcePackage, sourceGitCommit);
context.run(
'git',
['submodule', 'update', '--init', '--recursive', '--depth', '1'],
{ cwd: packageSourcePath }
);
const submoduleOutput = context.runCapture(
'git',
['submodule', 'status', '--recursive'],
{ cwd: packageSourcePath }
);
return {
...sourcePackage,
sourceGitCommit,
sourceSubmodules: canonicalizeGitSubmoduleStatus(submoduleOutput),
};
}
function acquireSources(context) {
fs.mkdirSync(context.archiveRoot, { recursive: true });
fs.mkdirSync(context.sourceRoot, { recursive: true });
const sourceRecords = {};
for (const packageId of BUILD_ORDER) {
const sourcePackage = sourcePackageById.get(packageId);
log(
`Acquiring ${sourcePackage.id} ${sourcePackage.version} from ${sourcePackage.sourceUrl}`
);
sourceRecords[packageId] =
sourcePackage.sourceKind === 'git'
? cloneGitSource(sourcePackage, context)
: downloadArchive(sourcePackage, context);
}
return sourceRecords;
}
function configureInstall(packageId, recipe, context) {
const sourcePath = sourcePathFor(packageId, context.sourceRoot);
context.run('./configure', [`--prefix=${context.prefix}`, ...recipe.args], {
cwd: sourcePath,
});
context.run('make', [`-j${context.parallelism}`], { cwd: sourcePath });
context.run('make', ['install'], { cwd: sourcePath });
}
function mesonSetupArgs(prefix, recipeArgs) {
return [
`--prefix=${prefix}`,
'--libdir=lib',
'--buildtype=release',
'--default-library=shared',
'--wrap-mode=nodownload',
'--auto-features=disabled',
'-Db_ndebug=true',
...recipeArgs,
];
}
function mesonInstall(packageId, recipe, context) {
const sourcePath = sourcePathFor(packageId, context.sourceRoot);
const buildPath = path.join(sourcePath, 'build-iptvnator');
const buildEnvironment = resolveLinuxPackageBuildEnvironment(
packageId,
context
);
fs.rmSync(buildPath, { recursive: true, force: true });
context.run(
'meson',
['setup', buildPath, ...mesonSetupArgs(context.prefix, recipe.args)],
{ cwd: sourcePath, env: buildEnvironment }
);
context.run(
'meson',
['compile', '--jobs', context.parallelism, '-C', buildPath],
{ cwd: sourcePath, env: buildEnvironment }
);
context.run('meson', ['install', '-C', buildPath], {
cwd: sourcePath,
env: buildEnvironment,
});
}
function prepareHwdata(context) {
context.hwdataBuildEnvironment = preparePinnedHwdataBuildInput({
buildEnvironment: context.buildEnvironment,
prefix: context.prefix,
runCapture: (command, args, options) =>
context.runCapture(command, args, options),
sourcePath: sourcePathFor('hwdata', context.sourceRoot),
});
}
function pathInsideDestdir(destdir, absolutePath) {
return path.join(
destdir,
absolutePath.slice(path.parse(absolutePath).root.length)
);
}
export 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,
verbatimSymlinks: 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');
fs.rmSync(buildPath, { recursive: true, force: true });
context.run('cmake', [
'-S',
sourcePath,
'-B',
buildPath,
'-G',
'Ninja',
`-DCMAKE_INSTALL_PREFIX=${context.prefix}`,
'-DCMAKE_INSTALL_LIBDIR=lib',
'-DCMAKE_BUILD_TYPE=Release',
'-DBUILD_SHARED_LIBS=ON',
...recipe.args,
]);
context.run('cmake', [
'--build',
buildPath,
'--parallel',
context.parallelism,
]);
context.run('cmake', ['--install', buildPath]);
}
function opensslInstall(recipe, context) {
const sourcePath = sourcePathFor('openssl', context.sourceRoot);
context.run(
'perl',
[
'./Configure',
'linux-x86_64',
`--prefix=${context.prefix}`,
'--libdir=lib',
...recipe.args,
],
{ cwd: sourcePath }
);
context.run('make', [`-j${context.parallelism}`], { cwd: sourcePath });
installWithDestdir('openssl', context, (destdir) =>
context.run('make', ['install_sw', `DESTDIR=${destdir}`], {
cwd: sourcePath,
})
);
}
function ffmpegInstall(recipe, context) {
const sourcePath = sourcePathFor('ffmpeg', context.sourceRoot);
const configureFlags = [`--prefix=${context.prefix}`, ...recipe.args];
context.run('./configure', configureFlags, { cwd: sourcePath });
context.run('make', [`-j${context.parallelism}`], { cwd: sourcePath });
context.run('make', ['install'], { cwd: sourcePath });
context.ffmpegConfigureFlags = configureFlags;
}
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 'data':
prepareHwdata(context);
break;
case 'configure':
configureInstall(packageId, recipe, context);
break;
case 'meson':
mesonInstall(packageId, recipe, context);
break;
case 'cmake':
cmakeInstall(packageId, recipe, context);
break;
case 'openssl':
opensslInstall(recipe, context);
break;
case 'ffmpeg':
ffmpegInstall(recipe, context);
break;
default:
throw new Error(
`Unsupported build system ${recipe.buildSystem} for ${packageId}.`
);
}
}
}
function removeFilesMatching(root, pattern) {
if (!fs.existsSync(root)) {
return;
}
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) {
removeFilesMatching(entryPath, pattern);
} else if (entry.isFile() && pattern.test(entry.name)) {
fs.rmSync(entryPath);
}
}
}
function removeNonRuntimeBuildOutputs(prefix) {
removeFilesMatching(path.join(prefix, 'lib'), /\.(?:a|la)$/);
for (const relativePath of [
'bin',
path.join('share', 'doc'),
path.join('share', 'gtk-doc'),
path.join('share', 'man'),
]) {
fs.rmSync(path.join(prefix, relativePath), {
recursive: true,
force: true,
});
}
}
function assertElfLibrary(libraryPath) {
const descriptor = fs.openSync(libraryPath, 'r');
try {
const header = Buffer.alloc(4);
const bytesRead = fs.readSync(descriptor, header, 0, header.length, 0);
if (
bytesRead !== 4 ||
!header.equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
) {
throw new Error(
`Runtime shared library is not an ELF file: ${libraryPath}.`
);
}
} finally {
fs.closeSync(descriptor);
}
}
function postProcessRuntime(context) {
const libDir = path.join(context.prefix, 'lib');
if (!fs.existsSync(libDir)) {
throw new Error(`Runtime library directory was not built: ${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);
assertElfLibrary(libraryPath);
context.run('patchelf', ['--set-rpath', '$ORIGIN', libraryPath]);
const dynamic = parseReadelfDynamic(
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) {
throw new Error(
'The Linux runtime build produced no shared libraries.'
);
}
const dependencyClosure = validateRuntimeDependencyClosure({
entries: dynamicEntries,
runtimeFileNames: runtimeFiles.map(({ name }) => name),
buildPrefix: context.prefix,
});
return {
abiBaseline: PORTABLE_ABI_BASELINE,
abiRecords,
dependencyClosure,
runtimeFiles,
};
}
function firstLine(value) {
return (
value
.split(/\r?\n/)
.find((line) => line.trim())
?.trim() ?? ''
);
}
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'],
curl: ['--version'],
git: ['--version'],
gperf: ['--version'],
make: ['--version'],
meson: ['--version'],
nasm: ['-v'],
ninja: ['--version'],
patchelf: ['--version'],
perl: ['-e', 'printf "%vd\\n", $^V'],
'pkg-config': ['--version'],
python3: ['--version'],
readelf: ['--version'],
tar: ['--version'],
};
const toolVersions = {};
for (const tool of REQUIRED_TOOLS) {
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 systemPkgConfigPackages;
}
function writeManifest(context, sourceRecords, runtimeMetadata) {
const mpvMesonFlags = mesonSetupArgs(context.prefix, MPV_MESON_FLAGS);
const manifest = createLinuxRuntimeManifest({
sourceRecords,
runtimeFiles: runtimeMetadata.runtimeFiles,
abiRecords: runtimeMetadata.abiRecords,
dependencyClosure: runtimeMetadata.dependencyClosure,
buildHost: collectBuildHost(context),
ffmpegConfigureFlags: context.ffmpegConfigureFlags,
mpvMesonFlags,
});
const manifestErrors = validateLinuxRuntimeManifest(manifest);
if (manifestErrors.length > 0) {
throw new Error(
[
'Generated Linux runtime manifest is invalid:',
...manifestErrors.map((error) => `- ${error}`),
].join('\n')
);
}
const manifestPath = path.join(context.prefix, 'runtime-manifest.json');
const temporaryManifestPath = `${manifestPath}.tmp`;
fs.writeFileSync(
temporaryManifestPath,
`${JSON.stringify(manifest, null, 2)}\n`,
{ mode: 0o644 }
);
fs.renameSync(temporaryManifestPath, manifestPath);
return manifest;
}
function createBuildContext(prefix, environment) {
const buildRoot = path.resolve(
environment.IPTVNATOR_EMBEDDED_MPV_LINUX_BUILD_ROOT ??
path.join(
os.tmpdir(),
'iptvnator-embedded-mpv-runtime',
'linux-x64'
)
);
assertSafeOutputPrefix(prefix, buildRoot);
const systemPkgConfigDirs = resolveSystemPkgConfigDirs(environment);
const buildEnvironment = createBuildEnvironment({
prefix,
baseEnv: environment,
systemPkgConfigDirs,
});
const runner = createCommandRunner({ buildEnvironment });
return {
...runner,
prefix,
buildRoot,
archiveRoot: path.join(buildRoot, 'archives'),
sourceRoot: path.join(buildRoot, 'sources'),
parallelism: resolveParallelism(environment),
systemPkgConfigDirs,
buildEnvironment,
ffmpegConfigureFlags: null,
};
}
export function main({
argv = process.argv.slice(2),
platform = process.platform,
arch = process.arch,
cwd = process.cwd(),
environment = process.env,
} = {}) {
const { prefix: outputPrefix } = parseCliInvocation({
platform,
arch,
argv,
cwd,
});
assertUniqueMesonOptionAssignments(BUILD_RECIPES);
ensureTools();
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 });
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]
? path.resolve(process.argv[1])
: undefined;
if (invokedScriptPath === scriptPath) {
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exitCode = 1;
}
}