fix(packaging): restore Snap desktop runtime (#1406)

* fix(packaging): restore Snap desktop runtime

* docs(packaging): publish Snap launch repair note

* fix(packaging): declare Node 22.12 floor

* docs(architecture): update SQLite pin rationale

* fix(tooling): align Node engine floor

* fix(tooling): constrain supported Node releases

* docs(architecture): correct node-abi consumer
This commit is contained in:
4gray authored and GitHub committed 2026-08-11 02:08:30 +02:00
1 parent 2a7d7c315e
commit 728df1a68c
11 files changed
+404 -660

No files matched your search

@@ -0,0 +1,6 @@
---
type: fix
area: packaging
---
Snap packages now launch correctly with the desktop runtime scripts required by their generated startup command.
+4 -1
View File
@@ -496,7 +496,10 @@ Key files:
`$SNAP/graphics/drirc.d`. The provider is external shared content, not part
of IPTVnator's package size, source archive, or notices. Installed-Snap CI
must prove controlled unavailable exit after disconnect, then reconnect and
prove success. The helper links `libGL.so.1` rather than `libOpenGL.so.0`.
prove success. Static artifact verification requires regular
`desktop-init.sh`, `desktop-common.sh`, and `desktop-gnome-specific.sh`
files at the Snap root, with `desktop-init.sh` executable. The helper links
`libGL.so.1` rather than `libOpenGL.so.0`.
- The probe and playback helper share one sanitized loader environment:
ambient audit, preload, library, graphics-driver, and shell-startup overrides
are removed; the validated private closure wins; trusted Snap GL,
+5 -2
View File
@@ -974,8 +974,11 @@ engine` (restart required) or
default provider. Its only provider-data layouts bind `/usr/share/libdrm`
from `$SNAP/graphics/libdrm` and symlink `/usr/share/drirc.d` to
`$SNAP/graphics/drirc.d`. Installed-Snap CI requires controlled unavailable
status after disconnect, then reconnects and requires success. The helper
links `libGL.so.1`, and probe/playback share a sanitized loader environment
status after disconnect, then reconnects and requires success. Static
artifact verification requires regular `desktop-init.sh`,
`desktop-common.sh`, and `desktop-gnome-specific.sh` files at the Snap root,
with `desktop-init.sh` executable. The helper links `libGL.so.1`, and
probe/playback share a sanitized loader environment
in which ambient audit, preload, library, graphics-driver, and shell-startup
overrides are removed; the validated private closure plus trusted host GL,
graphics-content, core22 base x64, and exact GNOME-platform roots have
+1 -1
View File
@@ -266,7 +266,7 @@ needed.
Requirements:
- Node.js with pnpm (via Corepack)
- Node.js 22.13–22.x or 24 and newer with pnpm (via Corepack)
1. Clone this repository and install project dependencies:
+5 -1
View File
@@ -365,7 +365,11 @@ missing, non-directory, symlinked, non-empty, or incorrectly permissioned
target. It also requires exactly the canonical provider-data layouts:
`/usr/share/libdrm` binds from `$SNAP/graphics/libdrm`, and
`/usr/share/drirc.d` symlinks to `$SNAP/graphics/drirc.d`. No additional or
duplicate layout entry is accepted.
duplicate layout entry is accepted. Static extraction verification also
requires regular `desktop-init.sh`, `desktop-common.sh`, and
`desktop-gnome-specific.sh` files at the Snap root, with `desktop-init.sh`
executable, because Electron Builder's generated `command.sh` invokes that
runtime before the application binary.
Private shared memory gives the app a confined, snap-specific POSIX shm
namespace rather than global cross-snap access. The packaging-only
+18 -11
View File
@@ -305,18 +305,25 @@ Window decorations on Linux (shadows, corners):
Toolchain notes for the Electron 41 upgrade:
1. `better-sqlite3` is pinned to exactly `12.9.0` — the last release that
ships prebuilt binaries for BOTH Node 20 (ABI 115, used by Jest) and
Electron 41 (ABI 145, used at runtime). `12.10.0` dropped the Node 20
prebuilds, which forces a from-source build that fails on machines
without a C++ toolchain.
1. `better-sqlite3` remains pinned to exactly `12.9.0` so native dependency
updates happen deliberately with database-worker, packaging, and Electron
E2E validation. The former Node 20 prebuild constraint no longer applies
now that the supported development floor is Node 22.13; revisit this pin
in a dedicated native-dependency update.
2. The pnpm override `node-abi@3.85.0 -> 3.92.0` is required so
`@electron/rebuild` (via `electron-builder install-app-deps`) can map
Electron 41 to its ABI.
3. Local development needs **Node >= 22.12**, declared in `engines`.
`electron-builder` 26.15.3 pulls `@electron/rebuild` 4, which sets that
floor, and the root `postinstall` runs `install-app-deps` on every
`pnpm install`. CI already runs Node 22.
`better-sqlite3`'s `prebuild-install` can map Electron 41 to ABI 145 when
`electron-builder install-app-deps` rebuilds native modules. The 3.85
registry stops at Electron 40, while 3.92 includes Electron 41;
`@electron/rebuild` itself now resolves `node-abi` 4.x.
3. Local development supports **Node 22.13–22.x or Node >= 24**, declared in
`engines` as `^22.13.0 || >=24.0.0`.
The direct `@faker-js/faker` dependency and current lint tooling require
that floor. `electron-builder` 26.15.7 also pulls `@electron/rebuild` 4,
which requires Node 22.12 or newer, and the root `postinstall` runs
`install-app-deps` on every `pnpm install`. The 26.15.7 minimum also
carries the v26 backport that fully extracts the Snap template's `.tar.7z`
payload; 26.15.0–26.15.6 can
produce a Snap that is missing `desktop-init.sh`. CI already runs Node 22.
Known caveats:
+4 -1
View File
@@ -1,6 +1,9 @@
{
"name": "iptvnator",
"version": "0.23.0",
"engines": {
"node": "^22.13.0 || >=24.0.0"
},
"license": "MIT",
"description": "IPTV player application.",
"homepage": "https://github.com/4gray/iptvnator",
@@ -189,7 +192,7 @@
"cors": "2.8.6",
"drizzle-kit": "0.31.10",
"electron": "^41.10.3",
"electron-builder": "^26.0.12",
"electron-builder": "^26.15.7",
"electron-playwright-helpers": "1.8.2",
"esbuild": "0.28.1",
"eslint": "^9.8.0",
+284 -643
View File
File diff suppressed because it is too large. Load diff
@@ -1235,6 +1235,15 @@ test('hashes the final source archive bytes and reads the exact packaged Snap bi
const graphicsRoot = path.join(snapSourceRoot, 'graphics');
fs.mkdirSync(graphicsRoot, { mode: 0o755 });
fs.chmodSync(graphicsRoot, 0o755);
for (const script of [
'desktop-init.sh',
'desktop-common.sh',
'desktop-gnome-specific.sh',
]) {
const desktopScriptPath = path.join(snapSourceRoot, script);
fs.writeFileSync(desktopScriptPath, '#!/bin/sh\n');
fs.chmodSync(desktopScriptPath, 0o755);
}
const asarSourceRoot = path.join(temporaryRoot, 'asar-source');
const appAsarPath = path.join(appRoot, 'resources', 'app.asar');
fs.mkdirSync(asarSourceRoot);
@@ -34,6 +34,11 @@ const {
const scriptPath = fileURLToPath(import.meta.url);
const LIBMPV_DEPENDENCY_PATTERN = /^libmpv\.so(?:\.|$)/;
const SNAP_METADATA_MAX_BYTES = 256 * 1024;
const SNAP_DESKTOP_RUNTIME_SCRIPTS = [
'desktop-init.sh',
'desktop-common.sh',
'desktop-gnome-specific.sh',
];
// Keep this set identical to the packaged helper sanitizer in
// embedded-mpv-frame-copy-runtime/helper-environment.ts. Feature/debug
// selectors such as LIBGL_ALWAYS_SOFTWARE remain intentionally available.
@@ -715,6 +720,35 @@ export function validateExtractedSnapMetadata(extractionRoot) {
];
}
const errors = [];
for (const script of SNAP_DESKTOP_RUNTIME_SCRIPTS) {
const desktopScriptPath = path.join(extractionRoot, script);
let desktopScriptStat;
try {
desktopScriptStat = fs.lstatSync(desktopScriptPath);
} catch {
errors.push(
`Missing required desktop runtime script: ${desktopScriptPath}`
);
continue;
}
if (
!desktopScriptStat.isFile() ||
desktopScriptStat.isSymbolicLink()
) {
errors.push(
`Extracted Snap desktop runtime script must be a regular file: ${desktopScriptPath}`
);
continue;
}
if (
script === 'desktop-init.sh' &&
(desktopScriptStat.mode & 0o111) === 0
) {
errors.push(
`Extracted Snap desktop-init.sh must be executable: ${desktopScriptPath}`
);
}
}
const graphicsMountPath = path.join(extractionRoot, 'graphics');
let graphicsMountStat;
try {
@@ -96,6 +96,19 @@ const DEB_SYSTEM_PACKAGE_DEPENDENCIES = [
'libgl1',
'libgbm1',
];
const SNAP_DESKTOP_RUNTIME_SCRIPTS = [
'desktop-init.sh',
'desktop-common.sh',
'desktop-gnome-specific.sh',
];
function writeSnapDesktopRuntimeScripts(root) {
for (const script of SNAP_DESKTOP_RUNTIME_SCRIPTS) {
const scriptPath = path.join(root, script);
fs.writeFileSync(scriptPath, '#!/bin/sh\n');
fs.chmodSync(scriptPath, 0o755);
}
}
test('uses the shared frozen runtime probe resource contract', async () => {
assert.equal(
@@ -1239,6 +1252,7 @@ test('requires exact Snap graphics layouts and plugs used by the app', () => {
const snapYamlPath = path.join(root, 'meta', 'snap.yaml');
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
fs.mkdirSync(path.join(root, 'graphics'));
writeSnapDesktopRuntimeScripts(root);
const validSnapYaml = [
'name: iptvnator',
@@ -1275,6 +1289,24 @@ test('requires exact Snap graphics layouts and plugs used by the app', () => {
fs.writeFileSync(snapYamlPath, validSnapYaml);
assert.deepEqual(validateExtractedSnapMetadata(root), []);
for (const script of SNAP_DESKTOP_RUNTIME_SCRIPTS) {
const scriptPath = path.join(root, script);
fs.rmSync(scriptPath);
assert.match(
validateExtractedSnapMetadata(root).join('\n'),
new RegExp(`required desktop runtime script.*${script}`, 'i')
);
fs.writeFileSync(scriptPath, '#!/bin/sh\n');
fs.chmodSync(scriptPath, 0o755);
}
fs.chmodSync(path.join(root, 'desktop-init.sh'), 0o644);
assert.match(
validateExtractedSnapMetadata(root).join('\n'),
/desktop-init\.sh.*executable/i
);
fs.chmodSync(path.join(root, 'desktop-init.sh'), 0o755);
fs.truncateSync(snapYamlPath, 256 * 1024 + 1);
assert.match(
validateExtractedSnapMetadata(root).join('\n'),
@@ -1675,6 +1707,7 @@ for (const [kind, mutate, expected] of [
const snapYamlPath = path.join(root, 'meta', 'snap.yaml');
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
fs.mkdirSync(path.join(root, 'graphics'));
writeSnapDesktopRuntimeScripts(root);
try {
fs.writeFileSync(snapYamlPath, SNAP_METADATA_WITH_LITERAL_HASHES);
@@ -1702,6 +1735,7 @@ test('artifact verification enforces Snap metadata for x64 and ARM payloads', ()
fs.writeFileSync(artifactPath, 'fixture');
fs.mkdirSync(path.dirname(snapYamlPath), { recursive: true });
fs.mkdirSync(path.join(fixture.root, 'graphics'));
writeSnapDesktopRuntimeScripts(fixture.root);
fs.writeFileSync(
snapYamlPath,
[