diff --git a/apps/desktop/electron/get-windows.d.ts b/apps/desktop/electron/get-windows.d.ts index a52c2feafe..444e7f9dd2 100644 --- a/apps/desktop/electron/get-windows.d.ts +++ b/apps/desktop/electron/get-windows.d.ts @@ -1,12 +1,11 @@ // Type declarations for the get-windows optionalDependency. // -// get-windows ships no bundled types and is an optionalDependency: on Linux -// `npm ci` skips it when its node-pre-gyp install script fails (no Linux -// prebuilt; the node-gyp fallback needs `gyp` in the active Python), so the -// package is legitimately absent from node_modules on Linux builds. Declaring -// the module here keeps the typecheck independent of whether the package -// installed — the runtime import in window-below.ts degrades to null when it -// is absent. +// get-windows ships no bundled types and is an optionalDependency. `npm ci` +// can skip it when its native install fails, including Linux and Windows ARM64 +// where 9.3.0 has no prebuilt, so it can legitimately be absent from +// node_modules. Declaring the module here keeps typecheck independent of +// whether the package installed. The runtime import in window-below.ts +// degrades to null when it is absent. declare module 'get-windows' { export interface GetWindowsWindow { diff --git a/apps/desktop/electron/window-below.ts b/apps/desktop/electron/window-below.ts index 979734be42..d5fa9e2384 100644 --- a/apps/desktop/electron/window-below.ts +++ b/apps/desktop/electron/window-below.ts @@ -125,11 +125,11 @@ type GetWindowsModule = { let getWindowsModule: Promise | null = null const loadGetWindows = (): Promise => { - // get-windows is an optionalDependency: on Linux `npm ci` skips it when its - // node-pre-gyp install script fails (no Linux prebuilt, and the node-gyp - // fallback needs `gyp` in the active Python). A missing module is therefore - // a normal state, so the lazy import resolves to null instead of rejecting; - // enumeration then degrades to the failure note instead of an uncaught error. + // get-windows is an optionalDependency: `npm ci` can skip it when its native + // install fails, including Linux and Windows ARM64 where 9.3.0 has no + // prebuilt. A missing module is therefore a normal state on those targets, + // so the lazy import resolves to null instead of rejecting; enumeration then + // degrades to the failure note instead of an uncaught error. getWindowsModule ??= import('get-windows').catch(() => null) return getWindowsModule diff --git a/apps/desktop/scripts/stage-native-deps.mjs b/apps/desktop/scripts/stage-native-deps.mjs index 4a59aee17e..c55a8ed3ab 100644 --- a/apps/desktop/scripts/stage-native-deps.mjs +++ b/apps/desktop/scripts/stage-native-deps.mjs @@ -408,9 +408,9 @@ export function openWindowsSync() { function resolveGetWindowsRoot() { // get-windows is an optionalDependency (its node-pre-gyp install script has - // no Linux prebuilt and the node-gyp fallback needs `gyp` in the active - // Python, so `npm ci` may skip it entirely on Linux). Return null when it is - // absent; the caller decides whether that is fatal per platform. + // no Linux or Windows ARM64 prebuilt and its node-gyp fallback may fail, so + // `npm ci` can skip it entirely on those targets). Return null when it is + // absent; the caller decides whether that is fatal per platform and arch. try { // get-windows' exports map doesn't expose ./package.json; resolve the entry // (index.js sits at the package root) and take its directory. @@ -565,22 +565,22 @@ export function stageGetWindows( const destRoot = resolve(projectRoot, 'dist/node_modules/get-windows') if (!srcRoot) { - // get-windows is an optionalDependency: `npm ci` on Linux skips it when the - // install script fails (no Linux prebuilt, node-gyp needs `gyp` in the - // active Python), so absence here is a normal state, not a broken checkout. - // On Linux it only backs read_window_below over X11 (lib/linux.js shells - // out to xprop); skipping it degrades that tool to Hyprland IPC or an - // "unavailable" note instead of failing the whole desktop build. On - // darwin/win32 the native payload is required, so fail loudly. - if (platform === 'linux') { + // npm may omit an optional dependency whose install script fails. That is + // expected on Linux and win32-arm64 because get-windows 9.3.0 publishes no + // native prebuilt for either target. The runtime import already fails soft, + // so disable only window enumeration instead of failing the Desktop build. + // Other Windows architectures and macOS have supported native payloads and + // remain fail-closed so a broken package cannot ship silently. + const canDegrade = platform === 'linux' || (platform === 'win32' && arch === 'arm64') + if (canDegrade) { console.warn( - '[stage-native-deps] get-windows not installed (optional dep skipped on ' + - 'Linux); read_window_below will be unavailable in this build' + `[stage-native-deps] get-windows not installed (optional dep skipped for ${platform}-${arch}); ` + + 'read_window_below will be unavailable in this build' ) return undefined } throw new Error( - `[stage-native-deps] get-windows is not installed; cannot stage its ${platform} native payload` + `[stage-native-deps] get-windows is not installed; cannot stage its ${platform}-${arch} native payload` ) } diff --git a/apps/desktop/scripts/stage-native-deps.test.mjs b/apps/desktop/scripts/stage-native-deps.test.mjs index 3266158fcc..6392b52a80 100644 --- a/apps/desktop/scripts/stage-native-deps.test.mjs +++ b/apps/desktop/scripts/stage-native-deps.test.mjs @@ -551,10 +551,10 @@ test('darwin staging ships the Swift helper executable and the rewritten windows // ─── stageGetWindows (optionalDependency gate) ────────────────────── // // get-windows is an optionalDependency: on Linux its node-pre-gyp install -// script fails (no Linux prebuilt; the node-gyp fallback needs `gyp` in the -// active Python), so npm skips the package and `npm ci` must still succeed. -// The staging step mirrors that: absent package on Linux is a skip, on -// darwin/win32 it is a hard failure because the native payload is required. +// script fails because no prebuilt exists. Windows ARM64 has the same package +// state: its prebuilt URL returns 404 and npm may omit the optional dependency. +// Staging skips those unsupported targets, but supported native targets remain +// a hard failure when the package is missing. test('linux staging skips when get-windows is absent (optional dep skipped by npm)', () => { assert.equal(stageGetWindows({ platform: 'linux', resolveRoot: () => null }), undefined) @@ -562,14 +562,21 @@ test('linux staging skips when get-windows is absent (optional dep skipped by np test('darwin staging fails when get-windows is absent', () => { assert.throws( - () => stageGetWindows({ platform: 'darwin', resolveRoot: () => null }), + () => stageGetWindows({ platform: 'darwin', arch: 'arm64', resolveRoot: () => null }), /get-windows is not installed/ ) }) -test('win32 staging fails when get-windows is absent', () => { +test('win32-arm64 staging skips when get-windows is absent after its optional install fails', () => { + assert.equal( + stageGetWindows({ platform: 'win32', arch: 'arm64', resolveRoot: () => null }), + undefined + ) +}) + +test('win32-x64 staging fails when get-windows is absent', () => { assert.throws( - () => stageGetWindows({ platform: 'win32', resolveRoot: () => null }), + () => stageGetWindows({ platform: 'win32', arch: 'x64', resolveRoot: () => null }), /get-windows is not installed/ ) })