From 6a24e2bf97bd211883be8e0823667cd7f93e5565 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:15:25 -0700 Subject: [PATCH] fix(windows): stage node-addon-api headers before process-tree rebuild Patched windows-process-tree binding.gyp includes deps/node-addon-api, but those headers were only copied by the later relay-addon script. Postinstall electron-rebuild then failed CI Windows installs with C1083 napi.h. --- ...build-windows-process-tree-relay-addon.mjs | 13 ++--- config/scripts/rebuild-native-deps.mjs | 9 ++++ config/scripts/rebuild-native-deps.test.mjs | 54 +++++++++++++++++++ .../windows-process-tree-gyp-path.test.mjs | 23 +++++--- .../windows-process-tree-gyp-rebuild.mjs | 26 ++++++++- .../windows-process-tree-gyp-rebuild.test.mjs | 36 ++++++++++++- 6 files changed, 139 insertions(+), 22 deletions(-) diff --git a/config/scripts/build-windows-process-tree-relay-addon.mjs b/config/scripts/build-windows-process-tree-relay-addon.mjs index 364335e537c..d3b9db939cd 100644 --- a/config/scripts/build-windows-process-tree-relay-addon.mjs +++ b/config/scripts/build-windows-process-tree-relay-addon.mjs @@ -29,11 +29,11 @@ import { readSync, writeFileSync } from 'node:fs' -import { createRequire } from 'node:module' -import { dirname, join, resolve } from 'node:path' +import { join, resolve } from 'node:path' import { RELAY_WINDOWS_PROCESS_TREE_FILENAME } from '../../src/shared/relay-artifacts.ts' import { nodeGypRebuildInvocation, + stageWindowsProcessTreeNodeAddonApiHeaders, WINDOWS_PROCESS_TREE_PACKAGE_DIR as PACKAGE_DIR } from './windows-process-tree-gyp-rebuild.mjs' @@ -96,10 +96,6 @@ function assertPatchApplied() { function applyWindowsProcessTreeBuildFixes() { const bindingPath = join(PACKAGE_DIR, 'binding.gyp') const processPath = join(PACKAGE_DIR, 'src', 'process.cc') - const nodeAddonApiDir = dirname( - createRequire(join(PACKAGE_DIR, 'package.json')).resolve('node-addon-api/package.json') - ) - const stagedHeaderDir = join(PACKAGE_DIR, 'deps', 'node-addon-api') let bindingGyp = readFileSync(bindingPath, 'utf8') let processCc = readFileSync(processPath, 'utf8') const originalBinding = bindingGyp @@ -134,10 +130,7 @@ function applyWindowsProcessTreeBuildFixes() { if (processCc !== originalProcess) { writeFileSync(processPath, processCc) } - mkdirSync(stagedHeaderDir, { recursive: true }) - for (const header of ['napi.h', 'napi-inl.h', 'napi-inl.deprecated.h']) { - copyFileSync(join(nodeAddonApiDir, header), join(stagedHeaderDir, header)) - } + stageWindowsProcessTreeNodeAddonApiHeaders(PACKAGE_DIR) if (bindingGyp !== originalBinding || processCc !== originalProcess) { console.warn('[windows-process-tree] Repaired un-applied pnpm patch hunks before build.') } diff --git a/config/scripts/rebuild-native-deps.mjs b/config/scripts/rebuild-native-deps.mjs index 4eca4bac33a..9bf61b679ed 100644 --- a/config/scripts/rebuild-native-deps.mjs +++ b/config/scripts/rebuild-native-deps.mjs @@ -20,6 +20,7 @@ import { rebuild } from '@electron/rebuild' import { execFileSync, spawnSync } from 'node:child_process' +import { stageWindowsProcessTreeNodeAddonApiHeaders } from './windows-process-tree-gyp-rebuild.mjs' import { copyFileSync, existsSync, @@ -132,6 +133,14 @@ if (!ignoreModules.includes('cpu-features')) { } } +if ( + rebuildPlatform === 'win32' && + modulesToRebuild.includes('@vscode/windows-process-tree') && + existsSync(join(projectDir, 'node_modules', '@vscode', 'windows-process-tree', 'package.json')) +) { + stageWindowsProcessTreeNodeAddonApiHeaders() +} + try { await rebuild({ buildPath: projectDir, diff --git a/config/scripts/rebuild-native-deps.test.mjs b/config/scripts/rebuild-native-deps.test.mjs index 5ebc39dfcaa..31676b92e55 100644 --- a/config/scripts/rebuild-native-deps.test.mjs +++ b/config/scripts/rebuild-native-deps.test.mjs @@ -21,6 +21,9 @@ const sourceInstallScriptPath = fileURLToPath( const sourceNodePtyJobOwnershipPath = fileURLToPath( new URL('./node-pty-job-ownership.cjs', import.meta.url) ) +const sourceWindowsProcessTreeGypRebuildPath = fileURLToPath( + new URL('./windows-process-tree-gyp-rebuild.mjs', import.meta.url) +) describe('rebuild-native-deps Electron install fallback', () => { it('continues non-strict postinstall when Electron retry download fails', () => { @@ -159,6 +162,41 @@ describe('rebuild-native-deps patched node-pty rebuild', () => { } ) + it('stages windows-process-tree node-addon-api headers before a Windows rebuild', () => { + const projectDir = mkTempProject() + + try { + writeFakeUsableElectronPackage(projectDir, { platform: 'win32' }) + writeFakeElectronRebuild(projectDir) + writeFakeNodePtyConptyPayload(projectDir, 'x64') + writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) + + const result = runRebuildScript( + projectDir, + { npm_config_platform: 'win32', npm_config_arch: 'x64' }, + ['--platform=win32', '--arch=x64', '--force'] + ) + + expect(result.status, result.stderr).toBe(0) + expect( + readFileSync( + join( + projectDir, + 'node_modules', + '@vscode', + 'windows-process-tree', + 'deps', + 'node-addon-api', + 'napi.h' + ), + 'utf8' + ) + ).toBe('// napi.h\n') + } finally { + rmSync(projectDir, { recursive: true, force: true }) + } + }) + it('restores the ConPTY runtime payload after a Windows Electron rebuild', () => { const projectDir = mkTempProject() @@ -344,6 +382,10 @@ function mkTempProject() { sourceNodePtyJobOwnershipPath, join(projectDir, 'config', 'scripts', 'node-pty-job-ownership.cjs') ) + copyFileSync( + sourceWindowsProcessTreeGypRebuildPath, + join(projectDir, 'config', 'scripts', 'windows-process-tree-gyp-rebuild.mjs') + ) return projectDir } @@ -578,6 +620,18 @@ function writeFakeWindowsProcessTree(projectDir) { writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n') } +function writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir) { + const processTreeDir = join(projectDir, 'node_modules', '@vscode', 'windows-process-tree') + const nodeAddonApiDir = join(processTreeDir, 'node_modules', 'node-addon-api') + mkdirSync(nodeAddonApiDir, { recursive: true }) + writeFileSync(join(processTreeDir, 'package.json'), '{"dependencies":{"node-addon-api":"*"}}\n') + writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n') + writeFileSync(join(nodeAddonApiDir, 'package.json'), '{"name":"node-addon-api"}\n') + writeFileSync(join(nodeAddonApiDir, 'napi.h'), '// napi.h\n') + writeFileSync(join(nodeAddonApiDir, 'napi-inl.h'), '// napi-inl.h\n') + writeFileSync(join(nodeAddonApiDir, 'napi-inl.deprecated.h'), '// napi-inl.deprecated.h\n') +} + function writeNodePtyPatchFile(projectDir) { mkdirSync(join(projectDir, 'config', 'patches'), { recursive: true }) writeFileSync(join(projectDir, 'config', 'patches', 'node-pty@1.1.0.patch'), 'patch marker\n') diff --git a/config/scripts/windows-process-tree-gyp-path.test.mjs b/config/scripts/windows-process-tree-gyp-path.test.mjs index e2e650384ec..30770500f42 100644 --- a/config/scripts/windows-process-tree-gyp-path.test.mjs +++ b/config/scripts/windows-process-tree-gyp-path.test.mjs @@ -20,15 +20,22 @@ describe('windows-process-tree node-addon-api gyp path', () => { join(projectDir, 'config/scripts/build-windows-process-tree-relay-addon.mjs'), 'utf8' ) - expect(buildScript).toContain( - "for (const header of ['napi.h', 'napi-inl.h', 'napi-inl.deprecated.h'])" - ) - expect(buildScript).toContain("import { createRequire } from 'node:module'") - expect(buildScript).toContain("import { dirname, join, resolve } from 'node:path'") - expect(buildScript).toContain( - "createRequire(join(PACKAGE_DIR, 'package.json')).resolve('node-addon-api/package.json')" - ) + expect(buildScript).toContain('stageWindowsProcessTreeNodeAddonApiHeaders(PACKAGE_DIR)') expect(buildScript).toContain('Repaired un-applied pnpm patch hunks before build.') + const rebuildHelper = readFileSync( + join(projectDir, 'config/scripts/windows-process-tree-gyp-rebuild.mjs'), + 'utf8' + ) + expect(rebuildHelper).toContain("createRequire(join(packageDir, 'package.json'))") + expect(rebuildHelper).toContain("resolve('node-addon-api/package.json')") + expect(rebuildHelper).toContain("'napi.h'") + expect(rebuildHelper).toContain("'napi-inl.h'") + expect(rebuildHelper).toContain("'napi-inl.deprecated.h'") + const rebuildScript = readFileSync( + join(projectDir, 'config/scripts/rebuild-native-deps.mjs'), + 'utf8' + ) + expect(rebuildScript).toContain('stageWindowsProcessTreeNodeAddonApiHeaders()') }) it('resolves node_addon_api.gyp to a real file from the package directory', () => { diff --git a/config/scripts/windows-process-tree-gyp-rebuild.mjs b/config/scripts/windows-process-tree-gyp-rebuild.mjs index 06ffcb3dfb1..c815407d6d0 100644 --- a/config/scripts/windows-process-tree-gyp-rebuild.mjs +++ b/config/scripts/windows-process-tree-gyp-rebuild.mjs @@ -9,8 +9,9 @@ * hop escapes the store and configure fails with "node_addon_api.gyp not * found" (run 32999886072). */ -import { realpathSync } from 'node:fs' -import { join, resolve } from 'node:path' +import { copyFileSync, mkdirSync, realpathSync } from 'node:fs' +import { createRequire } from 'node:module' +import { dirname, join, resolve } from 'node:path' const ROOT = resolve(import.meta.dirname, '..', '..') @@ -21,6 +22,12 @@ export const WINDOWS_PROCESS_TREE_PACKAGE_DIR = join( 'windows-process-tree' ) +export const WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS = [ + 'napi.h', + 'napi-inl.h', + 'napi-inl.deprecated.h' +] + export function nodeGypRebuildInvocation(arch, packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR) { return { args: [ @@ -31,3 +38,18 @@ export function nodeGypRebuildInvocation(arch, packageDir = WINDOWS_PROCESS_TREE cwd: realpathSync(packageDir) } } + +// Patched binding.gyp includes deps/node-addon-api; the tarball does not ship those headers. +export function stageWindowsProcessTreeNodeAddonApiHeaders( + packageDir = WINDOWS_PROCESS_TREE_PACKAGE_DIR +) { + const nodeAddonApiDir = dirname( + createRequire(join(packageDir, 'package.json')).resolve('node-addon-api/package.json') + ) + const stagedHeaderDir = join(packageDir, 'deps', 'node-addon-api') + mkdirSync(stagedHeaderDir, { recursive: true }) + for (const header of WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS) { + copyFileSync(join(nodeAddonApiDir, header), join(stagedHeaderDir, header)) + } + return stagedHeaderDir +} diff --git a/config/scripts/windows-process-tree-gyp-rebuild.test.mjs b/config/scripts/windows-process-tree-gyp-rebuild.test.mjs index a90a92f42bf..f4820e9430a 100644 --- a/config/scripts/windows-process-tree-gyp-rebuild.test.mjs +++ b/config/scripts/windows-process-tree-gyp-rebuild.test.mjs @@ -1,9 +1,20 @@ import { execFileSync } from 'node:child_process' -import { existsSync, realpathSync } from 'node:fs' -import { resolve } from 'node:path' +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { nodeGypRebuildInvocation, + stageWindowsProcessTreeNodeAddonApiHeaders, + WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS, WINDOWS_PROCESS_TREE_PACKAGE_DIR } from './windows-process-tree-gyp-rebuild.mjs' @@ -26,4 +37,25 @@ describe('windows-process-tree node-gyp rebuild', () => { expect(args).toContain('rebuild') expect(args).toContain('--arch=arm64') }) + + it('copies node-addon-api headers into the patched include dir', () => { + const packageDir = mkdtempSync(join(tmpdir(), 'orca-windows-process-tree-headers-')) + try { + const nodeAddonApiDir = join(packageDir, 'node_modules', 'node-addon-api') + mkdirSync(nodeAddonApiDir, { recursive: true }) + writeFileSync(join(packageDir, 'package.json'), '{"dependencies":{"node-addon-api":"*"}}\n') + writeFileSync(join(nodeAddonApiDir, 'package.json'), '{"name":"node-addon-api"}\n') + for (const header of WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS) { + writeFileSync(join(nodeAddonApiDir, header), `// ${header}\n`) + } + + const stagedDir = stageWindowsProcessTreeNodeAddonApiHeaders(packageDir) + expect(stagedDir).toBe(join(packageDir, 'deps', 'node-addon-api')) + for (const header of WINDOWS_PROCESS_TREE_NODE_ADDON_API_HEADERS) { + expect(readFileSync(join(stagedDir, header), 'utf8')).toBe(`// ${header}\n`) + } + } finally { + rmSync(packageDir, { recursive: true, force: true }) + } + }) })