Files
iptvnator/tools/packaging/configure-linux-frame-copy-build.mjs
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

264 lines
8.5 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
const require = createRequire(import.meta.url);
const {
LINUX_SYSTEM_PACKAGE_DEPENDENCIES,
resolveLinuxFrameCopyProfile,
} = require('./linux-frame-copy-profile.cjs');
const scriptPath = fileURLToPath(import.meta.url);
function cloneJson(value) {
return JSON.parse(JSON.stringify(value));
}
function targetName(target) {
const value =
typeof target === 'string'
? target
: target && typeof target === 'object'
? target.target
: null;
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(
'Electron Builder Linux targets must have a non-empty target name.'
);
}
return value.trim().toLowerCase();
}
function targetObject(target) {
return typeof target === 'string' ? { target } : { ...target };
}
function fpmDependencyName(option) {
return String(option).match(
/^--depends(?:=|\s+)([A-Za-z0-9+_.-]+)(?:$|\s|[<>=])/
)?.[1];
}
function configureSystemDependencies(config) {
for (const [format, dependencies] of Object.entries(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES
)) {
const frameCopyDependencies = new Set(dependencies);
const formatConfig = { ...(config[format] ?? {}) };
const otherFpmOptions = (formatConfig.fpm ?? []).filter(
(option) => !frameCopyDependencies.has(fpmDependencyName(option))
);
formatConfig.fpm = [
...otherFpmOptions,
...dependencies.map((dependency) => `--depends=${dependency}`),
];
config[format] = formatConfig;
}
}
function removeForeignFrameCopyDependency(config, format) {
const dependencies = new Set(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES[format] ?? []
);
const formatConfig = { ...(config[format] ?? {}) };
const retainedFpmOptions = (formatConfig.fpm ?? []).filter(
(option) => !dependencies.has(fpmDependencyName(option))
);
if (retainedFpmOptions.length > 0) {
formatConfig.fpm = retainedFpmOptions;
} else {
delete formatConfig.fpm;
}
config[format] = formatConfig;
}
export function configureLinuxFrameCopyBuild(
electronBuilderConfig,
{ profileName, foreignDeb = false, foreignArch } = {}
) {
const hasForeignArch = foreignArch !== undefined;
if (
!electronBuilderConfig ||
typeof electronBuilderConfig !== 'object' ||
Array.isArray(electronBuilderConfig)
) {
throw new TypeError(
'Electron Builder configuration must be an object.'
);
}
if (foreignDeb && profileName) {
throw new Error(
'The marker-only foreign DEB pass must not select a frame-copy profile.'
);
}
if (hasForeignArch && !foreignDeb) {
throw new Error(
'A marker-only foreign architecture requires --foreign-deb.'
);
}
const configured = cloneJson(electronBuilderConfig);
const targets = configured.linux?.target;
if (!Array.isArray(targets)) {
throw new Error('Electron Builder linux.target must be an array.');
}
if (foreignDeb) {
const debTarget = targets.find(
(target) => targetName(target) === 'deb'
);
if (!debTarget) {
throw new Error(
'Electron Builder has no DEB target for the foreign-architecture pass.'
);
}
const configuredDebTarget = targetObject(debTarget);
const configuredArches = Array.isArray(configuredDebTarget.arch)
? configuredDebTarget.arch
: [configuredDebTarget.arch].filter(Boolean);
const foreignArches = configuredArches.filter(
(architecture) => architecture !== 'x64'
);
if (foreignArches.length === 0) {
throw new Error(
'Electron Builder DEB target has no foreign architectures.'
);
}
if (
hasForeignArch &&
(typeof foreignArch !== 'string' ||
!foreignArches.includes(foreignArch))
) {
throw new Error(
`Unsupported marker-only foreign DEB architecture "${foreignArch}". Expected one of: ${foreignArches.join(
', '
)}.`
);
}
configuredDebTarget.arch = hasForeignArch
? [foreignArch]
: foreignArches;
configured.linux.target = [configuredDebTarget];
configured.directories = {
...(configured.directories ?? {}),
output: 'dist/executables-linux-foreign',
};
removeForeignFrameCopyDependency(configured, 'deb');
return configured;
}
const profile = resolveLinuxFrameCopyProfile(profileName);
const allowedTargets = new Set(profile.targets);
const targetsByName = new Map(
profile.targets.map((profileTarget) => [profileTarget, []])
);
for (const target of targets) {
const name = targetName(target);
if (allowedTargets.has(name)) {
targetsByName.get(name).push(target);
}
}
const duplicateTargets = profile.targets.filter(
(profileTarget) => targetsByName.get(profileTarget).length > 1
);
const missingTargets = profile.targets.filter(
(profileTarget) => targetsByName.get(profileTarget).length === 0
);
if (duplicateTargets.length > 0 || missingTargets.length > 0) {
throw new Error(
[
`Invalid Electron Builder targets for Linux profile "${profile.name}".`,
...(duplicateTargets.length > 0
? [
`Found duplicate target "${duplicateTargets.join(
'", "'
)}".`,
]
: []),
...(missingTargets.length > 0
? [`Missing targets: ${missingTargets.join(', ')}.`]
: []),
].join(' ')
);
}
configured.linux.target = profile.targets.map((profileTarget) => {
const target = targetsByName.get(profileTarget)[0];
const configuredTarget = targetObject(target);
if (profile.name === 'system') {
configuredTarget.arch = ['x64'];
}
return configuredTarget;
});
if (profile.name === 'system') {
configureSystemDependencies(configured);
}
return configured;
}
function parseArguments(argv) {
const normalized = argv[0] === '--' ? argv.slice(1) : argv;
let configPath = 'electron-builder.json';
let profileName;
let foreignDeb = false;
let foreignArch;
const nextValue = (flag, index) => {
const value = normalized[index + 1];
if (
typeof value !== 'string' ||
value.trim() === '' ||
value.startsWith('--')
) {
throw new Error(`${flag} requires a value.`);
}
return value;
};
for (let index = 0; index < normalized.length; index += 1) {
const argument = normalized[index];
if (argument === '--config') {
configPath = nextValue(argument, index);
index += 1;
} else if (argument === '--profile') {
profileName = nextValue(argument, index);
index += 1;
} else if (argument === '--foreign-deb') {
foreignDeb = true;
} else if (argument === '--foreign-arch') {
foreignArch = nextValue(argument, index);
index += 1;
} else {
throw new Error(`Unsupported configurator argument: ${argument}`);
}
}
if (!foreignDeb && !profileName) {
throw new Error('--profile or --foreign-deb is required.');
}
return {
configPath: path.resolve(configPath),
profileName,
foreignDeb,
foreignArch,
};
}
function main() {
const options = parseArguments(process.argv.slice(2));
const config = JSON.parse(fs.readFileSync(options.configPath, 'utf8'));
const configured = configureLinuxFrameCopyBuild(config, options);
fs.writeFileSync(
options.configPath,
`${JSON.stringify(configured, null, 4)}\n`
);
}
if (process.argv[1] && path.resolve(process.argv[1]) === scriptPath) {
try {
main();
} catch (error) {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exitCode = 1;
}
}