feat(packaging): define Linux frame-copy profiles

This commit is contained in:
4gray committed 2026-07-17 22:42:36 +02:00
1 parent a3fbfb1bdc
commit 81f970d281
4 files changed
+254 -1

No files matched your search

+9
View File
@@ -98,6 +98,15 @@
"artifactName": "${name}-${version}-${os}-${arch}.${ext}",
"icon": "apps/web/src/assets/icons"
},
"deb": {
"fpm": ["--depends=libmpv2"]
},
"rpm": {
"fpm": ["--depends=mpv-libs"]
},
"pacman": {
"fpm": ["--depends=mpv"]
},
"flatpak": {
"branch": "stable",
"runtime": "org.freedesktop.Platform",
@@ -0,0 +1,96 @@
'use strict';
function createProfile(name, runtimeMode, targets, manifestOrigin) {
return Object.freeze({
name,
runtimeMode,
targets: Object.freeze([...targets]),
manifestOrigin,
});
}
const LINUX_FRAME_COPY_PROFILES = Object.freeze({
system: createProfile(
'system',
'system',
['deb', 'rpm', 'pacman'],
'system-libmpv-frame-copy'
),
portable: createProfile(
'portable',
'bundled',
['appimage', 'snap'],
'bundled-lgpl-frame-copy'
),
flatpak: createProfile(
'flatpak',
'bundled',
['flatpak'],
'bundled-lgpl-frame-copy'
),
});
const SUPPORTED_PROFILE_NAMES = Object.freeze(
Object.keys(LINUX_FRAME_COPY_PROFILES)
);
const LINUX_SYSTEM_PACKAGE_DEPENDENCIES = Object.freeze({
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
});
function expectedProfileNames() {
return SUPPORTED_PROFILE_NAMES.map((name) => `"${name}"`).join(', ');
}
function findLinuxFrameCopyProfile(value) {
if (value == null || (typeof value === 'string' && value.trim() === '')) {
throw new Error(
`Linux frame-copy profile is required. Expected one of: ${expectedProfileNames()}.`
);
}
const name = typeof value === 'string' ? value.trim() : String(value);
const profile = LINUX_FRAME_COPY_PROFILES[name];
if (!profile) {
throw new Error(
`Unsupported Linux frame-copy profile "${name}". Expected one of: ${expectedProfileNames()}.`
);
}
return profile;
}
function resolveLinuxFrameCopyProfile(value) {
const profile = findLinuxFrameCopyProfile(value);
return createProfile(
profile.name,
profile.runtimeMode,
profile.targets,
profile.manifestOrigin
);
}
function validateLinuxProfileTargets(profileName, targetNames) {
const profile = findLinuxFrameCopyProfile(profileName);
if (!Array.isArray(targetNames)) {
throw new TypeError('Linux frame-copy targets must be an array.');
}
const allowedTargets = new Set(profile.targets);
return targetNames.flatMap((targetName) => {
const normalizedTarget = String(targetName).trim().toLowerCase();
if (allowedTargets.has(normalizedTarget)) {
return [];
}
return [
`Linux frame-copy profile "${profile.name}" cannot build target "${normalizedTarget}".`,
];
});
}
module.exports = {
LINUX_FRAME_COPY_PROFILES,
LINUX_SYSTEM_PACKAGE_DEPENDENCIES,
resolveLinuxFrameCopyProfile,
validateLinuxProfileTargets,
};
@@ -0,0 +1,146 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { dirname, join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const currentDir = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const {
LINUX_FRAME_COPY_PROFILES,
LINUX_SYSTEM_PACKAGE_DEPENDENCIES,
resolveLinuxFrameCopyProfile,
validateLinuxProfileTargets,
} = require('./linux-frame-copy-profile.cjs');
const electronBuilderConfig = JSON.parse(
fs.readFileSync(
join(currentDir, '..', '..', 'electron-builder.json'),
'utf8'
)
);
test('defines the exact immutable Linux frame-copy profile matrix', () => {
assert.deepEqual(LINUX_FRAME_COPY_PROFILES, {
system: {
name: 'system',
runtimeMode: 'system',
targets: ['deb', 'rpm', 'pacman'],
manifestOrigin: 'system-libmpv-frame-copy',
},
portable: {
name: 'portable',
runtimeMode: 'bundled',
targets: ['appimage', 'snap'],
manifestOrigin: 'bundled-lgpl-frame-copy',
},
flatpak: {
name: 'flatpak',
runtimeMode: 'bundled',
targets: ['flatpak'],
manifestOrigin: 'bundled-lgpl-frame-copy',
},
});
assert.equal(Object.isFrozen(LINUX_FRAME_COPY_PROFILES), true);
for (const profile of Object.values(LINUX_FRAME_COPY_PROFILES)) {
assert.equal(Object.isFrozen(profile), true);
assert.equal(Object.isFrozen(profile.targets), true);
}
});
test('defines immutable system-package libmpv dependencies', () => {
assert.deepEqual(LINUX_SYSTEM_PACKAGE_DEPENDENCIES, {
deb: 'libmpv2',
rpm: 'mpv-libs',
pacman: 'mpv',
});
assert.equal(Object.isFrozen(LINUX_SYSTEM_PACKAGE_DEPENDENCIES), true);
});
test('resolves each supported profile as an immutable defensive value', () => {
const firstSystemProfile = resolveLinuxFrameCopyProfile('system');
const secondSystemProfile = resolveLinuxFrameCopyProfile('system');
assert.deepEqual(firstSystemProfile, {
name: 'system',
runtimeMode: 'system',
targets: ['deb', 'rpm', 'pacman'],
manifestOrigin: 'system-libmpv-frame-copy',
});
assert.deepEqual(resolveLinuxFrameCopyProfile('portable'), {
name: 'portable',
runtimeMode: 'bundled',
targets: ['appimage', 'snap'],
manifestOrigin: 'bundled-lgpl-frame-copy',
});
assert.deepEqual(resolveLinuxFrameCopyProfile('flatpak'), {
name: 'flatpak',
runtimeMode: 'bundled',
targets: ['flatpak'],
manifestOrigin: 'bundled-lgpl-frame-copy',
});
assert.notEqual(firstSystemProfile, LINUX_FRAME_COPY_PROFILES.system);
assert.notEqual(firstSystemProfile, secondSystemProfile);
assert.notEqual(
firstSystemProfile.targets,
LINUX_FRAME_COPY_PROFILES.system.targets
);
assert.notEqual(firstSystemProfile.targets, secondSystemProfile.targets);
assert.equal(Object.isFrozen(firstSystemProfile), true);
assert.equal(Object.isFrozen(firstSystemProfile.targets), true);
assert.throws(() => firstSystemProfile.targets.push('appimage'), TypeError);
assert.deepEqual(resolveLinuxFrameCopyProfile('system').targets, [
'deb',
'rpm',
'pacman',
]);
});
test('rejects missing and unsupported profile names with clear errors', () => {
for (const value of [undefined, null, '', ' ']) {
assert.throws(
() => resolveLinuxFrameCopyProfile(value),
/Linux frame-copy profile is required/
);
}
assert.throws(
() => resolveLinuxFrameCopyProfile('standard'),
/Unsupported Linux frame-copy profile "standard"/
);
});
test('validates profile targets case-insensitively with deterministic errors', () => {
const targets = ['DEB', 'AppImage', 'RPM'];
assert.deepEqual(validateLinuxProfileTargets('system', targets), [
'Linux frame-copy profile "system" cannot build target "appimage".',
]);
assert.deepEqual(targets, ['DEB', 'AppImage', 'RPM']);
assert.deepEqual(
validateLinuxProfileTargets('portable', ['APPIMAGE', 'sNaP']),
[]
);
assert.deepEqual(validateLinuxProfileTargets('flatpak', ['FlatPak']), []);
});
test('rejects a non-array profile target list', () => {
assert.throws(
() => validateLinuxProfileTargets('system', 'deb'),
/Linux frame-copy targets must be an array/
);
});
test('adds only libmpv-specific package dependencies without replacing electron-builder defaults', () => {
for (const [target, dependency] of Object.entries(
LINUX_SYSTEM_PACKAGE_DEPENDENCIES
)) {
assert.equal(
electronBuilderConfig[target]?.depends,
undefined,
`${target}.depends must remain unset so electron-builder keeps its defaults`
);
assert.deepEqual(electronBuilderConfig[target]?.fpm, [
`--depends=${dependency}`,
]);
}
});
+3 -1
View File
@@ -21,11 +21,13 @@
"{workspaceRoot}/tools/packaging/asar-dependency-closure.test.mjs",
"{workspaceRoot}/tools/packaging/embedded-mpv-packaging.cjs",
"{workspaceRoot}/tools/packaging/embedded-mpv-arch.test.mjs",
"{workspaceRoot}/tools/packaging/linux-frame-copy-profile.cjs",
"{workspaceRoot}/tools/packaging/linux-frame-copy-profile.test.mjs",
"{workspaceRoot}/tools/embedded-mpv/stage-runtime.mjs",
"{workspaceRoot}/tools/embedded-mpv/stage-windows-runtime-archive.mjs"
],
"options": {
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs",
"command": "node --test tools/packaging/electron-package-identity.test.mjs tools/packaging/asar-dependency-closure.test.mjs tools/packaging/embedded-mpv-arch.test.mjs tools/packaging/linux-frame-copy-profile.test.mjs",
"cwd": "{workspaceRoot}"
}
},