From 2cd9e1777b47f001acb6a5d4ec5848677b331c31 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 8 Aug 2026 21:23:20 -0500 Subject: [PATCH] fix(desktop): rebuild get-windows when its win32 binding is missing Fixing the allowlist only helps a fresh install. npm will not re-run an install script for a package already on disk, so every checkout that installed while get-windows was blocked stays bricked: `hermes update` pulls the fix, `npm install` skips the script, and the build fails on the same missing binding. Run `npm rebuild get-windows` from the staging step when the binding is absent, and if that still yields nothing, print the two commands that recover the checkout by hand instead of the previous advice to reinstall dependencies, which is exactly what the user already tried. Gated to a win32 host building for win32, since no other host can produce the binding. Co-authored-by: JoaoMarcos44 --- apps/desktop/scripts/stage-native-deps.mjs | 57 +++++++++++++++---- .../scripts/stage-native-deps.test.mjs | 47 +++++++++++++++ 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/apps/desktop/scripts/stage-native-deps.mjs b/apps/desktop/scripts/stage-native-deps.mjs index 987d37946c..95fea7d13b 100644 --- a/apps/desktop/scripts/stage-native-deps.mjs +++ b/apps/desktop/scripts/stage-native-deps.mjs @@ -426,7 +426,11 @@ function resolveGetWindowsRoot() { */ const GET_WINDOWS_VERSION = '9.3.0' -export function stageGetWindowsInto(srcRoot, destRoot, { platform = process.platform } = {}) { +export function stageGetWindowsInto( + srcRoot, + destRoot, + { platform = process.platform, rebuild } = {} +) { // The STAGED_WINDOWS_JS rewrite mirrors this exact version's export surface. // A version bump must fail the build here until the rewrite is re-verified — // otherwise it ships stale and fails soft as a generic "unavailable". @@ -473,17 +477,32 @@ export function stageGetWindowsInto(srcRoot, destRoot, { platform = process.plat // the target platform; the classify gate below still catches a dir that // claims win32 but holds a foreign binary. const bindingRoot = join(srcRoot, 'lib', 'binding') - const bindingDirs = existsSync(bindingRoot) - ? readdirSync(bindingRoot).filter( - (dir) => - dir.includes(`-${platform}-`) && - existsSync(join(bindingRoot, dir, 'node-get-windows.node')) - ) - : [] + const scanBindingDirs = () => + existsSync(bindingRoot) + ? readdirSync(bindingRoot).filter( + (dir) => + dir.includes(`-${platform}-`) && + existsSync(join(bindingRoot, dir, 'node-get-windows.node')) + ) + : [] + let bindingDirs = scanBindingDirs() + if (bindingDirs.length === 0 && typeof rebuild === 'function') { + // A plain `npm install` won't re-run an install script for a package + // that is already on disk, so every checkout that installed while + // get-windows was missing from allowScripts stays bricked even after + // the allowlist is fixed. `npm rebuild` re-runs it. + console.log( + '[stage-native-deps] get-windows has no win32 binding; running `npm rebuild get-windows`...' + ) + rebuild() + bindingDirs = scanBindingDirs() + } if (bindingDirs.length === 0) { throw new Error( - '[stage-native-deps] get-windows has no win32 prebuilt binding under lib/binding; ' + - 'reinstall dependencies on the Windows build host.' + '[stage-native-deps] get-windows has no win32 prebuilt binding under lib/binding. ' + + 'Recover from the checkout root with:\n' + + ' npm install-scripts approve get-windows\n' + + ' npm rebuild get-windows' ) } for (const dir of bindingDirs) { @@ -506,10 +525,26 @@ export function stageGetWindowsInto(srcRoot, destRoot, { platform = process.plat return destRoot } +function rebuildGetWindowsViaNpm() { + const result = spawnSync('npm', ['rebuild', 'get-windows'], { + cwd: resolve(projectRoot, '..', '..'), + stdio: 'inherit', + // npm resolves to npm.cmd on Windows, which needs a shell. + shell: process.platform === 'win32' + }) + if (result.status !== 0) { + console.warn(`[stage-native-deps] npm rebuild get-windows exited with ${result.status}`) + } +} + export function stageGetWindows({ platform = process.platform } = {}) { const srcRoot = resolveGetWindowsRoot() const destRoot = resolve(projectRoot, 'dist/node_modules/get-windows') - return stageGetWindowsInto(srcRoot, destRoot, { platform }) + // Only a win32 host can produce the win32 binding, so a cross-platform pack + // has nothing to gain from the rebuild. + const rebuild = + platform === 'win32' && process.platform === 'win32' ? rebuildGetWindowsViaNpm : undefined + return stageGetWindowsInto(srcRoot, destRoot, { platform, rebuild }) } // Allow direct CLI invocation: node scripts/stage-native-deps.mjs [platform] [arch] diff --git a/apps/desktop/scripts/stage-native-deps.test.mjs b/apps/desktop/scripts/stage-native-deps.test.mjs index dfd06bb25c..48a4101175 100644 --- a/apps/desktop/scripts/stage-native-deps.test.mjs +++ b/apps/desktop/scripts/stage-native-deps.test.mjs @@ -437,6 +437,53 @@ test('win32 staging fails when only foreign bindings exist', () => { } }) +test('win32 staging self-heals through the rebuild hook when the binding is missing', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) + try { + const srcRoot = join(tmp, 'get-windows') + const destRoot = join(tmp, 'dest') + + // The bricked state a blocked install script leaves behind: the package is + // present, lib/binding was never populated by node-pre-gyp. + makeFakeGetWindows(srcRoot, { bindings: [] }) + + let calls = 0 + const rebuild = () => { + calls += 1 + makeFakeNode( + join(srcRoot, 'lib', 'binding', 'napi-9-win32-unknown-x64', 'node-get-windows.node'), + 'win32' + ) + } + + stageGetWindowsInto(srcRoot, destRoot, { platform: 'win32', rebuild }) + + assert.equal(calls, 1) + assert.ok( + existsSync(join(destRoot, 'lib', 'binding', 'napi-9-win32-unknown-x64', 'node-get-windows.node')) + ) + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + +test('win32 staging reports the recovery steps when the rebuild hook produces nothing', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) + try { + const srcRoot = join(tmp, 'get-windows') + const destRoot = join(tmp, 'dest') + + makeFakeGetWindows(srcRoot, { bindings: [] }) + + assert.throws( + () => stageGetWindowsInto(srcRoot, destRoot, { platform: 'win32', rebuild: () => {} }), + /npm rebuild get-windows/ + ) + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) + test('staging refuses a get-windows version the lib/windows.js rewrite was not verified against', () => { const tmp = fs.mkdtempSync(join(os.tmpdir(), 'hermes-stage-')) try {