From 48a19dedd808a29a8c7ae084557565fc1f0f4cbf Mon Sep 17 00:00:00 2001 From: Hampus Date: Fri, 9 Oct 2026 05:39:56 +0200 Subject: [PATCH] fix(desktop): stop requiring the removed win-toast at startup (#3345) --- fluxer_desktop/electron-builder.config.cjs | 80 ++++++++++++ .../src/main/NativeModulePreflight.test.mjs | 13 ++ .../src/main/NativeModulePreflight.ts | 117 ++---------------- .../main/NativeModulePreflightModules.json | 102 +++++++++++++++ 4 files changed, 203 insertions(+), 109 deletions(-) create mode 100644 fluxer_desktop/src/main/NativeModulePreflightModules.json diff --git a/fluxer_desktop/electron-builder.config.cjs b/fluxer_desktop/electron-builder.config.cjs index dda59ceb6..0cbdd8279 100644 --- a/fluxer_desktop/electron-builder.config.cjs +++ b/fluxer_desktop/electron-builder.config.cjs @@ -1074,11 +1074,91 @@ async function verifyLinuxGlibcCompatibility(context) { ); } +const NATIVE_MODULE_PREFLIGHT_MODULES = require('./src/main/NativeModulePreflightModules.json'); + +function packagedResourcesDir(context) { + if (context.electronPlatformName === 'darwin' || context.electronPlatformName === 'mas') { + return path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`, 'Contents', 'Resources'); + } + return path.join(context.appOutDir, 'resources'); +} + +async function readAsarFileList(asarPath) { + const handle = await fs.open(asarPath, 'r'); + try { + const prefix = Buffer.alloc(16); + await handle.read(prefix, 0, 16, 0); + const header = Buffer.alloc(prefix.readUInt32LE(12)); + await handle.read(header, 0, header.length, 16); + const files = new Set(); + const walk = (node, prefixPath) => { + for (const [name, child] of Object.entries(node.files ?? {})) { + const childPath = prefixPath ? `${prefixPath}/${name}` : name; + if (child.files) walk(child, childPath); + else files.add(childPath); + } + }; + walk(JSON.parse(header.toString('utf8')), ''); + return files; + } finally { + await handle.close(); + } +} + +async function verifyPreflightModulesPackaged(context) { + const platform = context.electronPlatformName === 'mas' ? 'darwin' : context.electronPlatformName; + const asarPath = path.join(packagedResourcesDir(context), 'app.asar'); + const files = await readAsarFileList(asarPath); + const missing = []; + for (const spec of NATIVE_MODULE_PREFLIGHT_MODULES) { + if (!spec.platforms.includes(platform)) continue; + const manifestPath = `node_modules/${spec.name}/package.json`; + if (!files.has(manifestPath)) { + missing.push(spec.name); + continue; + } + const manifest = JSON.parse((await readAsarEntry(asarPath, manifestPath)).toString('utf8')); + const entry = path.posix.normalize(`node_modules/${spec.name}/${manifest.main ?? 'index.js'}`); + if (!files.has(entry)) { + missing.push(`${spec.name} (${entry})`); + } + } + if (missing.length > 0) { + throw new Error( + [ + `The packaged app for ${platform} is missing native module(s) that the startup preflight requires:`, + ...missing.map((entry) => ` - ${entry}`), + 'Package them or remove them from src/main/NativeModulePreflightModules.json.', + ].join('\n'), + ); + } +} + +async function readAsarEntry(asarPath, entryPath) { + const handle = await fs.open(asarPath, 'r'); + try { + const prefix = Buffer.alloc(16); + await handle.read(prefix, 0, 16, 0); + const headerSize = prefix.readUInt32LE(4); + const header = Buffer.alloc(prefix.readUInt32LE(12)); + await handle.read(header, 0, header.length, 16); + let node = JSON.parse(header.toString('utf8')); + for (const part of entryPath.split('/')) node = node.files[part]; + if (node.unpacked) return await fs.readFile(path.join(`${asarPath}.unpacked`, ...entryPath.split('/'))); + const data = Buffer.alloc(node.size); + await handle.read(data, 0, node.size, 8 + headerSize + Number(node.offset)); + return data; + } finally { + await handle.close(); + } +} + async function afterPack(context) { await copyMissingPackagedNativeArtifacts(context); await cleanupNativeBuildIntermediates(context); await addLinuxLegacyBinarySymlink(context); await verifyPackagedNativeArtifacts(context); + await verifyPreflightModulesPackaged(context); await verifyLinuxGlibcCompatibility(context); } diff --git a/fluxer_desktop/src/main/NativeModulePreflight.test.mjs b/fluxer_desktop/src/main/NativeModulePreflight.test.mjs index 425a926cc..1dfff87d2 100644 --- a/fluxer_desktop/src/main/NativeModulePreflight.test.mjs +++ b/fluxer_desktop/src/main/NativeModulePreflight.test.mjs @@ -73,6 +73,9 @@ function loadPreflight({ }), }; } + if (specifier === '@electron/main/NativeModulePreflightModules.json') { + return JSON.parse(readFileSync(new URL('./NativeModulePreflightModules.json', import.meta.url), 'utf8')); + } if (specifier === '@electron/main/AppStoreNativeBoundary') return {APP_STORE_ADDON_PACKAGE: '@fluxer/app-store'}; if (specifier === '@electron/main/GatewaySocketNativeBoundary') { return {GATEWAY_SOCKET_ADDON_PACKAGE: '@fluxer/gateway-socket'}; @@ -166,3 +169,13 @@ describe('NativeModulePreflight', () => { assert.equal(preflight.spawns.length, 1); }); }); + +test('every module the preflight requires is a dependency of the desktop app', () => { + const modules = JSON.parse(readFileSync(new URL('./NativeModulePreflightModules.json', import.meta.url), 'utf8')); + const manifest = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')); + const dependencies = {...manifest.dependencies, ...manifest.optionalDependencies}; + assert.deepEqual( + modules.map((spec) => spec.name).filter((name) => !Object.hasOwn(dependencies, name)), + [], + ); +}); diff --git a/fluxer_desktop/src/main/NativeModulePreflight.ts b/fluxer_desktop/src/main/NativeModulePreflight.ts index 1b923997d..e1690c730 100644 --- a/fluxer_desktop/src/main/NativeModulePreflight.ts +++ b/fluxer_desktop/src/main/NativeModulePreflight.ts @@ -5,8 +5,7 @@ import {createHash} from 'node:crypto'; import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs'; import {createRequire} from 'node:module'; import path from 'node:path'; -import {APP_STORE_ADDON_PACKAGE} from '@electron/main/AppStoreNativeBoundary'; -import {GATEWAY_SOCKET_ADDON_PACKAGE} from '@electron/main/GatewaySocketNativeBoundary'; +import NATIVE_MODULE_PREFLIGHT_MODULES from '@electron/main/NativeModulePreflightModules.json'; import {app} from 'electron'; import log from 'electron-log'; @@ -43,113 +42,13 @@ export interface NativeModulePreflightResult { readonly degraded: ReadonlyArray; } -const NATIVE_MODULE_PREFLIGHT_SPECS: ReadonlyArray = [ - { - name: '@fluxer/webauthn', - platforms: new Set(['darwin', 'linux', 'win32']), - severity: 'fatal', - }, - { - name: '@fluxer/platform-info', - platforms: new Set(['darwin', 'linux', 'win32']), - severity: 'fatal', - }, - { - name: '@fluxer/mac-app-audio', - platforms: new Set(['darwin']), - severity: 'fatal', - }, - { - name: '@fluxer/mac-clipboard', - platforms: new Set(['darwin']), - severity: 'fatal', - }, - { - name: '@fluxer/mac-sysctl', - platforms: new Set(['darwin']), - severity: 'fatal', - }, - { - name: '@fluxer/mac-tcc', - platforms: new Set(['darwin']), - severity: 'fatal', - }, - { - name: '@fluxer/macos-input-hook', - platforms: new Set(['darwin']), - severity: 'fatal', - }, - { - name: '@fluxer/linux-audio-capture', - platforms: new Set(['linux']), - severity: 'fatal', - }, - { - name: '@fluxer/linux-evdev', - platforms: new Set(['linux']), - severity: 'fatal', - }, - { - name: '@fluxer/linux-input-hook', - platforms: new Set(['linux']), - severity: 'fatal', - }, - { - name: '@fluxer/linux-notifications', - platforms: new Set(['linux']), - severity: 'fatal', - }, - { - name: '@fluxer/linux-portals', - platforms: new Set(['linux']), - severity: 'fatal', - }, - { - name: '@fluxer/linux-screen-capture', - platforms: new Set(['linux']), - severity: 'fatal', - }, - { - name: '@fluxer/system-hunspell', - platforms: new Set(['linux']), - severity: 'fatal', - }, - { - name: '@fluxer/win-clipboard', - platforms: new Set(['win32']), - severity: 'fatal', - }, - { - name: '@fluxer/win-process-loopback', - platforms: new Set(['win32']), - severity: 'fatal', - }, - { - name: '@fluxer/win-shell', - platforms: new Set(['win32']), - severity: 'fatal', - }, - { - name: '@fluxer/win-toast', - platforms: new Set(['win32']), - severity: 'fatal', - }, - { - name: '@fluxer/windows-input-hook', - platforms: new Set(['win32']), - severity: 'fatal', - }, - { - name: APP_STORE_ADDON_PACKAGE, - platforms: new Set(['darwin', 'linux', 'win32']), - severity: 'degraded', - }, - { - name: GATEWAY_SOCKET_ADDON_PACKAGE, - platforms: new Set(['darwin', 'linux', 'win32']), - severity: 'degraded', - }, -]; +const NATIVE_MODULE_PREFLIGHT_SPECS: ReadonlyArray = NATIVE_MODULE_PREFLIGHT_MODULES.map( + (spec) => ({ + name: spec.name, + platforms: new Set(spec.platforms as ReadonlyArray), + severity: spec.severity as NativeModuleSeverity, + }), +); const PROBE_SCRIPT = ` const modulePaths = process.argv.slice(1); function write(obj) { diff --git a/fluxer_desktop/src/main/NativeModulePreflightModules.json b/fluxer_desktop/src/main/NativeModulePreflightModules.json new file mode 100644 index 000000000..e634d3e78 --- /dev/null +++ b/fluxer_desktop/src/main/NativeModulePreflightModules.json @@ -0,0 +1,102 @@ +[ + { + "name": "@fluxer/webauthn", + "platforms": ["darwin", "linux", "win32"], + "severity": "fatal" + }, + { + "name": "@fluxer/platform-info", + "platforms": ["darwin", "linux", "win32"], + "severity": "fatal" + }, + { + "name": "@fluxer/mac-app-audio", + "platforms": ["darwin"], + "severity": "fatal" + }, + { + "name": "@fluxer/mac-clipboard", + "platforms": ["darwin"], + "severity": "fatal" + }, + { + "name": "@fluxer/mac-sysctl", + "platforms": ["darwin"], + "severity": "fatal" + }, + { + "name": "@fluxer/mac-tcc", + "platforms": ["darwin"], + "severity": "fatal" + }, + { + "name": "@fluxer/macos-input-hook", + "platforms": ["darwin"], + "severity": "fatal" + }, + { + "name": "@fluxer/linux-audio-capture", + "platforms": ["linux"], + "severity": "fatal" + }, + { + "name": "@fluxer/linux-evdev", + "platforms": ["linux"], + "severity": "fatal" + }, + { + "name": "@fluxer/linux-input-hook", + "platforms": ["linux"], + "severity": "fatal" + }, + { + "name": "@fluxer/linux-notifications", + "platforms": ["linux"], + "severity": "fatal" + }, + { + "name": "@fluxer/linux-portals", + "platforms": ["linux"], + "severity": "fatal" + }, + { + "name": "@fluxer/linux-screen-capture", + "platforms": ["linux"], + "severity": "fatal" + }, + { + "name": "@fluxer/system-hunspell", + "platforms": ["linux"], + "severity": "fatal" + }, + { + "name": "@fluxer/win-clipboard", + "platforms": ["win32"], + "severity": "fatal" + }, + { + "name": "@fluxer/win-process-loopback", + "platforms": ["win32"], + "severity": "fatal" + }, + { + "name": "@fluxer/win-shell", + "platforms": ["win32"], + "severity": "fatal" + }, + { + "name": "@fluxer/windows-input-hook", + "platforms": ["win32"], + "severity": "fatal" + }, + { + "name": "@fluxer/app-store", + "platforms": ["darwin", "linux", "win32"], + "severity": "degraded" + }, + { + "name": "@fluxer/gateway-socket", + "platforms": ["darwin", "linux", "win32"], + "severity": "degraded" + } +]