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.
This commit is contained in:
Neil
2026-08-29 21:15:40 -07:00
parent 7e7f241ec6
commit 6a24e2bf97
6 changed files with 139 additions and 22 deletions
@@ -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.')
}
+9
View File
@@ -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,
@@ -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')
@@ -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', () => {
@@ -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
}
@@ -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 })
}
})
})