Files
iptvnator/tools/packaging/electron-after-pack.cjs
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

294 lines
8.7 KiB
JavaScript

const linuxAfterPack = require('./linux-after-pack.cjs');
const {
isForeignLinuxEmbeddedMpvArch,
resolveElectronBuilderArchName,
validatePackagedEmbeddedMpv,
} = require('./embedded-mpv-packaging.cjs');
const {
resolveLinuxFrameCopyProfile,
validateLinuxProfileTargets,
} = require('./linux-frame-copy-profile.cjs');
const fs = require('fs');
const path = require('path');
const {
preparePackagedFrameCopyArtifacts,
} = require('./embedded-mpv-frame-copy-files.cjs');
function log(message) {
console.log(` - ${message}`);
}
function isTruthy(value) {
return ['1', 'true', 'yes', 'on'].includes(
String(value ?? '')
.trim()
.toLowerCase()
);
}
function copyEmbeddedMpvNativeOutput(
resourceDir,
projectDir,
platform,
preparationOptions
) {
const sourceDir = path.join(
projectDir,
'dist',
'apps',
'electron-backend',
'native'
);
if (!fs.existsSync(sourceDir)) {
return;
}
const destinationDir = path.join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.rmSync(destinationDir, { recursive: true, force: true });
fs.cpSync(sourceDir, destinationDir, { recursive: true });
const resolvedPreparationOptions =
platform === 'linux' && preparationOptions
? {
...preparationOptions,
noticeSourceDir: path.join(
projectDir,
'vendor',
'embedded-mpv',
'linux-x64',
'notices'
),
}
: preparationOptions;
return preparePackagedFrameCopyArtifacts(
destinationDir,
platform,
resolvedPreparationOptions
);
}
function writeEmbeddedMpvUnavailableMarker(resourceDir, targetArch) {
const destinationDir = path.join(
resourceDir,
'app.asar.unpacked',
'electron-backend',
'native'
);
fs.rmSync(destinationDir, { recursive: true, force: true });
fs.mkdirSync(destinationDir, { recursive: true });
fs.writeFileSync(
path.join(destinationDir, 'embedded-mpv-unavailable.txt'),
`Embedded MPV is not bundled for ${targetArch} Linux builds yet. The built-in player and external MPV/VLC remain available.\n`
);
}
function resolveLinuxFrameCopyPackagingContext(
params,
{
required = isTruthy(process.env.IPTVNATOR_REQUIRE_EMBEDDED_MPV),
environment = process.env,
} = {}
) {
if (params.electronPlatformName !== 'linux') {
return null;
}
const targetArch = resolveElectronBuilderArchName(params.arch);
if (!targetArch) {
throw new Error(
`Unknown Electron Builder architecture: ${String(params.arch)}.`
);
}
const targetNames = [];
for (const target of params.targets ?? []) {
const targetName = String(target?.name ?? '')
.trim()
.toLowerCase();
if (!targetName) {
throw new Error(
'Linux Electron Builder targets must expose a non-empty name.'
);
}
if (targetNames.includes(targetName)) {
throw new Error(
`Linux Electron Builder target "${targetName}" is duplicated.`
);
}
targetNames.push(targetName);
}
if (targetNames.length === 0) {
throw new Error(
'Linux Electron Builder must provide at least one packaging target.'
);
}
// Official Linux frame-copy artifacts are intentionally x64-only. Do not
// let a caller-provided build-arch environment value promote an ARM
// package to a supported layout: every non-x64 target must remain the
// marker-only native-view fallback.
const foreignArch = targetArch !== 'x64';
const profileValue =
environment.IPTVNATOR_LINUX_FRAME_COPY_PROFILE?.trim() ?? '';
if (!profileValue) {
if (required && targetArch === 'x64') {
resolveLinuxFrameCopyProfile(profileValue);
}
return {
targetArch,
foreignArch,
targetNames,
profile: null,
};
}
const profile = resolveLinuxFrameCopyProfile(profileValue);
const targetErrors = validateLinuxProfileTargets(profile.name, targetNames);
if (targetErrors.length > 0) {
throw new Error(targetErrors.join('\n'));
}
return {
targetArch,
foreignArch,
targetNames,
profile,
};
}
function ensureSnapGraphicsContentMount(appOutDir, targetNames) {
if (!targetNames.includes('snap')) {
return null;
}
const mountPath = path.join(appOutDir, 'graphics');
if (!fs.existsSync(mountPath)) {
fs.mkdirSync(mountPath, { mode: 0o755 });
}
const stat = fs.lstatSync(mountPath);
if (!stat.isDirectory() || stat.isSymbolicLink()) {
throw new Error(
`Snap graphics content mount must be a real empty directory: ${mountPath}`
);
}
if (fs.readdirSync(mountPath).length > 0) {
throw new Error(
`Snap graphics content mount directory must be empty: ${mountPath}`
);
}
fs.chmodSync(mountPath, 0o755);
return mountPath;
}
async function afterPackHook(params) {
const requireEmbeddedMpv = isTruthy(
process.env.IPTVNATOR_REQUIRE_EMBEDDED_MPV
);
const linuxPackagingContext = resolveLinuxFrameCopyPackagingContext(
params,
{
required: requireEmbeddedMpv,
environment: process.env,
}
);
await linuxAfterPack(params);
if (linuxPackagingContext) {
const graphicsMountPath = ensureSnapGraphicsContentMount(
params.appOutDir,
linuxPackagingContext.targetNames
);
if (graphicsMountPath) {
log(
`prepared empty Snap graphics content mount at ${graphicsMountPath}`
);
}
}
log(
requireEmbeddedMpv
? `validating required embedded MPV ${params.electronPlatformName} runtime`
: `validating optional embedded MPV ${params.electronPlatformName} runtime`
);
const resourceDir = getResourceDir(params);
const foreignArch =
linuxPackagingContext?.foreignArch ??
isForeignLinuxEmbeddedMpvArch(params.electronPlatformName, params.arch);
if (foreignArch) {
const targetArch = linuxPackagingContext.targetArch;
log(
`embedded MPV addon is not built for ${targetArch}; packaging an unavailable marker instead`
);
writeEmbeddedMpvUnavailableMarker(resourceDir, targetArch);
} else {
copyEmbeddedMpvNativeOutput(
resourceDir,
params.packager.projectDir ?? process.cwd(),
params.electronPlatformName,
linuxPackagingContext
? {
profile: linuxPackagingContext.profile?.name,
targetNames: linuxPackagingContext.targetNames,
}
: undefined
);
}
const errors = validatePackagedEmbeddedMpv(resourceDir, {
platform: params.electronPlatformName,
required: requireEmbeddedMpv,
foreignArch,
targetArch: linuxPackagingContext?.targetArch,
profile: linuxPackagingContext?.profile?.name,
targetNames: linuxPackagingContext?.targetNames,
executableName: params.packager.executableName,
});
if (errors.length > 0) {
throw new Error(
[
`Embedded MPV ${params.electronPlatformName} package validation failed.`,
...errors.map((error) => `- ${error}`),
].join('\n')
);
}
log(`embedded MPV ${params.electronPlatformName} runtime validated`);
}
function getResourceDir(params) {
if (params.electronPlatformName !== 'darwin') {
return path.join(params.appOutDir, 'resources');
}
const appPath = params.appOutDir.endsWith('.app')
? params.appOutDir
: fs
.readdirSync(params.appOutDir)
.find((entry) => entry.endsWith('.app'));
return appPath
? path.join(
params.appOutDir.endsWith('.app')
? params.appOutDir
: path.join(params.appOutDir, appPath),
'Contents',
'Resources'
)
: params.appOutDir;
}
module.exports = afterPackHook;
module.exports.ensureSnapGraphicsContentMount = ensureSnapGraphicsContentMount;
module.exports.resolveLinuxFrameCopyPackagingContext =
resolveLinuxFrameCopyPackagingContext;
module.exports.writeEmbeddedMpvUnavailableMarker =
writeEmbeddedMpvUnavailableMarker;