fix(build): preserve Electron during binary repair (#17334)

* fix(build): preserve Electron during binary repair

* refactor(build): split native dependency fixtures

* fix(build): resolve one Electron install target for child and check

runElectronPackageBinaryInstall forced ELECTRON_INSTALL_PLATFORM/ARCH to the
host-derived rebuild target, clobbering inherited installer env, while the
parent usability check still honored the inherited value. A bare
`node config/scripts/rebuild-native-deps.mjs` under ELECTRON_INSTALL_PLATFORM=win32
on Linux therefore installed the Linux binary and then rejected it as
unavailable. Resolve the target once (CLI, ELECTRON_INSTALL_*, npm config, host)
and use it for both the child env and getElectronPlatformPath.

* fix(build): keep Electron install transaction cleanup best-effort

The finally-block rmSync could throw after a fully successful publish (Windows
EPERM when another process still holds the discarded old electron.exe open),
turning a correct install into exit 1. On the rollback path it could also
replace the in-flight publishError with an unrelated temp-dir error. Retry the
removal and downgrade a persistent failure to a warning.
This commit is contained in:
Neil
2026-08-29 23:12:25 -07:00
committed by GitHub
parent 07df4bf0be
commit b261f4005c
6 changed files with 917 additions and 566 deletions
@@ -53,6 +53,7 @@ try {
}
async function main() {
repairElectronPathFile()
if (electronPackageIsUsable()) {
return
}
@@ -61,7 +62,6 @@ async function main() {
// Node. Install only Electron's npm package binary here; do not run the full
// Electron native-module rebuild path, which would undo the Node ABI rebuild.
console.log('[electron-package] Electron package binary is missing; running Electron install.')
resetPartialElectronInstall()
await installElectronPackageBinary()
repairElectronPathFile()
@@ -74,16 +74,23 @@ async function main() {
}
function electronPackageIsUsable() {
try {
const installedPlatformPath = readFileSync(resolve(electronPackageDir, 'path.txt'), 'utf8')
return (
electronDistMatchesPackage(getElectronExecutablePath()) &&
installedPlatformPath === platformPath
)
} catch {
return false
}
}
function electronDistMatchesPackage(electronExecutable) {
try {
const installedVersion = readFileSync(resolve(electronPackageDir, 'dist', 'version'), 'utf8')
.trim()
.replace(/^v/, '')
const installedPlatformPath = readFileSync(resolve(electronPackageDir, 'path.txt'), 'utf8')
return (
installedVersion === electronVersion &&
installedPlatformPath === platformPath &&
existsSync(getElectronExecutablePath())
)
return installedVersion === electronVersion && existsSync(electronExecutable)
} catch {
return false
}
@@ -95,14 +102,9 @@ function getElectronExecutablePath() {
: resolve(electronPackageDir, 'dist', platformPath)
}
function resetPartialElectronInstall() {
rmSync(resolve(electronPackageDir, 'dist'), { recursive: true, force: true })
rmSync(resolve(electronPackageDir, 'path.txt'), { force: true })
}
function repairElectronPathFile() {
const electronExecutable = resolve(electronPackageDir, 'dist', platformPath)
if (!existsSync(electronExecutable)) {
if (!electronDistMatchesPackage(electronExecutable)) {
return
}
@@ -152,11 +154,6 @@ async function installElectronPackageBinary() {
}
moveExtractedElectronDist(extractDir, electronDistDir)
const srcTypeDefPath = resolve(electronDistDir, 'electron.d.ts')
if (existsSync(srcTypeDefPath)) {
renameSync(srcTypeDefPath, resolve(electronPackageDir, 'electron.d.ts'))
}
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
@@ -266,16 +263,85 @@ function extractElectronArchive(zipPath, extractDir) {
}
function moveExtractedElectronDist(extractDir, electronDistDir) {
rmSync(electronDistDir, { recursive: true, force: true })
const transactionDir = mkdtempSync(resolve(electronPackageDir, '.dist-install-'))
const nextDistDir = join(transactionDir, 'next')
const previousDistDir = join(transactionDir, 'previous')
const packageTypeDefPath = resolve(electronPackageDir, 'electron.d.ts')
const previousTypeDefPath = join(transactionDir, 'previous-electron.d.ts')
let previousMoved = false
let previousTypeDefMoved = false
let nextPublished = false
let cleanupTransaction = true
try {
stageExtractedElectronDist(extractDir, nextDistDir)
const hasNextTypeDef = existsSync(resolve(nextDistDir, 'electron.d.ts'))
try {
if (existsSync(electronDistDir)) {
renameSync(electronDistDir, previousDistDir)
previousMoved = true
}
if (hasNextTypeDef && existsSync(packageTypeDefPath)) {
renameSync(packageTypeDefPath, previousTypeDefPath)
previousTypeDefMoved = true
}
renameSync(nextDistDir, electronDistDir)
nextPublished = true
if (hasNextTypeDef) {
renameSync(resolve(electronDistDir, 'electron.d.ts'), packageTypeDefPath)
}
} catch (publishError) {
const rollbackErrors = []
for (const [shouldMove, source, target] of [
[nextPublished, electronDistDir, nextDistDir],
[previousMoved, previousDistDir, electronDistDir],
[previousTypeDefMoved, previousTypeDefPath, packageTypeDefPath]
]) {
if (!shouldMove) {
continue
}
try {
renameSync(source, target)
} catch (rollbackError) {
rollbackErrors.push(rollbackError)
}
}
if (rollbackErrors.length > 0) {
cleanupTransaction = false
throw new AggregateError(
[publishError, ...rollbackErrors],
`Electron install publish failed; previous files remain at ${transactionDir}`
)
}
throw publishError
}
} finally {
if (cleanupTransaction) {
// Why: the discarded tree can hold an executable another process still has
// open on Windows. Never fail a published install, or mask a publish error,
// on leftover-temp cleanup.
try {
rmSync(transactionDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 })
} catch (cleanupError) {
console.warn(
`[electron-package] Could not remove install transaction dir ${transactionDir}: ` +
`${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
)
}
}
}
}
function stageExtractedElectronDist(extractDir, nextDistDir) {
try {
// Why: macOS Electron archives rely on framework symlinks. Moving the
// verified tree preserves them exactly; copying has broken them in CI.
renameSync(extractDir, electronDistDir)
renameSync(extractDir, nextDistDir)
} catch (/** @type {any} */ err) {
if (err?.code !== 'EXDEV') {
throw err
}
cpSync(extractDir, electronDistDir, {
cpSync(extractDir, nextDistDir, {
recursive: true,
dereference: false,
verbatimSymlinks: true
@@ -26,6 +26,11 @@ describe('install-electron-package-binary', () => {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeExtractor(projectDir, { createExecutable: true })
writeFakeElectronDist(projectDir, {
version: 'v40.0.0',
executableContents: 'old executable',
pathContents: 'stale-path'
})
const result = runInstallScript(projectDir)
@@ -36,6 +41,10 @@ describe('install-electron-package-binary', () => {
expect(readFileSync(join(projectDir, 'node_modules', 'electron', 'path.txt'), 'utf8')).toBe(
'electron'
)
expect(readFileSync(join(projectDir, 'node_modules/electron/electron.d.ts'), 'utf8')).toBe(
'replacement types'
)
expect(existsSync(join(projectDir, 'node_modules/electron/dist/electron.d.ts'))).toBe(false)
if (process.platform !== 'win32') {
expect(
lstatSync(
@@ -49,6 +58,84 @@ describe('install-electron-package-binary', () => {
}
})
it('repairs existing Electron path metadata without downloading', () => {
const projectDir = mkTempProject()
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeElectronDist(projectDir)
const result = runInstallScript(projectDir)
expect(result.status, result.stderr).toBe(0)
expect(readFileSync(join(projectDir, 'node_modules/electron/path.txt'), 'utf8')).toBe(
'electron'
)
expect(result.stdout).toContain('Repaired Electron path.txt -> electron')
expect(existsSync(join(projectDir, 'electron-get.log'))).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('preserves an existing Electron distribution when replacement download fails', () => {
const projectDir = mkTempProject()
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { downloadFailures: 1, downloadErrorCode: 'EACCES' })
writeFakeElectronDist(projectDir, {
version: 'v40.0.0',
executableContents: 'existing executable',
pathContents: 'electron'
})
const result = runInstallScript(projectDir)
const electronDir = join(projectDir, 'node_modules/electron')
expect(result.status).toBe(1)
expect(readFileSync(join(electronDir, 'dist/version'), 'utf8')).toBe('v40.0.0')
expect(readFileSync(join(electronDir, 'dist/electron'), 'utf8')).toBe('existing executable')
expect(readFileSync(join(electronDir, 'path.txt'), 'utf8')).toBe('electron')
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('restores an existing Electron distribution when publishing its type definitions fails', () => {
const projectDir = mkTempProject()
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir)
writeFakeExtractor(projectDir, { createExecutable: true })
writeFakeElectronDist(projectDir, {
version: 'v40.0.0',
executableContents: 'existing executable',
pathContents: 'electron'
})
writeFileSync(join(projectDir, 'node_modules/electron/electron.d.ts'), 'existing types')
const preloadPath = writeTypeDefPublishFailurePreload(projectDir)
const result = runInstallScript(projectDir, {
NODE_OPTIONS: [process.env.NODE_OPTIONS, `--require=${preloadPath}`]
.filter(Boolean)
.join(' ')
})
const electronDir = join(projectDir, 'node_modules/electron')
expect(result.status).toBe(1)
expect(result.stderr).toContain('injected Electron type definition publish failure')
expect(readFileSync(join(electronDir, 'dist/version'), 'utf8')).toBe('v40.0.0')
expect(readFileSync(join(electronDir, 'dist/electron'), 'utf8')).toBe('existing executable')
expect(readFileSync(join(electronDir, 'path.txt'), 'utf8')).toBe('electron')
expect(readFileSync(join(electronDir, 'electron.d.ts'), 'utf8')).toBe('existing types')
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('uses Electron 42 install env vars before npm config platform flags', () => {
const projectDir = mkTempProject()
@@ -329,6 +416,19 @@ module.exports = path.join(__dirname, 'dist', fs.readFileSync(pathFile, 'utf8'))
)
}
function writeFakeElectronDist(
projectDir,
{ version = 'v41.5.0', executableContents = '', pathContents } = {}
) {
const electronDir = join(projectDir, 'node_modules', 'electron')
mkdirSync(join(electronDir, 'dist'), { recursive: true })
writeFileSync(join(electronDir, 'dist/version'), version)
writeFileSync(join(electronDir, 'dist/electron'), executableContents)
if (pathContents !== undefined) {
writeFileSync(join(electronDir, 'path.txt'), pathContents)
}
}
function writeFakeElectronGet(
projectDir,
{
@@ -386,6 +486,7 @@ mkdirSync(join(extractDir, 'locales'), { recursive: true })
if (${JSON.stringify(createExecutable)}) {
writeFileSync(join(extractDir, 'electron'), '')
writeFileSync(join(extractDir, 'electron.exe'), '')
writeFileSync(join(extractDir, 'electron.d.ts'), 'replacement types')
writeFileSync(join(extractDir, 'version'), 'v41.5.0')
if (process.platform !== 'win32') {
symlinkSync('version', join(extractDir, 'version-link'))
@@ -394,3 +495,26 @@ if (${JSON.stringify(createExecutable)}) {
`
)
}
function writeTypeDefPublishFailurePreload(projectDir) {
const preloadPath = join(projectDir, 'type-def-publish-failure.cjs')
writeFileSync(
preloadPath,
`
const fs = require('node:fs')
const { syncBuiltinESMExports } = require('node:module')
const { basename, dirname } = require('node:path')
const renameSync = fs.renameSync
fs.renameSync = (source, target) => {
if (basename(source) === 'electron.d.ts' && basename(dirname(source)) === 'dist') {
const error = new Error('injected Electron type definition publish failure')
error.code = 'EACCES'
throw error
}
return renameSync(source, target)
}
syncBuiltinESMExports()
`
)
return preloadPath
}
@@ -0,0 +1,258 @@
import { existsSync, readFileSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
mkTempProject,
runRebuildScript,
writeFakeElectronRebuild,
writeFakeLoadableNodePty,
writeFakeNodePtyConptyPayload,
writeFakeUsableElectronPackage,
writeFakeWindowsProcessTree,
writeFakeWindowsProcessTreeWithNodeAddonApi,
writeFakeWindowsRegistry,
writeNodePtyPatchFile,
writePatchedNodePtyBuildArtifacts
} from './rebuild-native-deps-test-fixtures.mjs'
describe('rebuild-native-deps patched node-pty rebuild', () => {
it.skipIf(process.platform !== 'win32')(
'repairs a missing ConPTY runtime before probing without recompiling node-pty',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../build/Release/' })
writeFakeWindowsRegistry(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Restored node-pty ConPTY runtime files')
expect(result.stdout).toContain(
'Native modules already load in Electron; skipping rebuild.'
)
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
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()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, 'x64')
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(result.stdout).toContain('Restored node-pty ConPTY runtime files for win10-x64')
const runtimeDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release', 'conpty')
expect(readFileSync(join(runtimeDir, 'conpty.dll'), 'utf8')).toBe('conpty.dll x64')
expect(readFileSync(join(runtimeDir, 'OpenConsole.exe'), 'utf8')).toBe('OpenConsole.exe x64')
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it.skipIf(process.platform !== 'win32')(
'does not rebuild a healthy node-pty when another Windows addon fails its probe',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: windows-native-registry')
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['windows-native-registry'])
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform !== 'win32')(
'rebuilds a loadable ConPTY native that lacks Orca job ownership',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { ownsPtyJob: false })
writeFakeWindowsRegistry(projectDir)
writeFakeWindowsProcessTree(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: node-pty')
expect(result.stdout).toContain('missing listJobProcessIds')
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'rebuilds when Electron can load node-pty but patched build artifacts are missing',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir)
writeNodePtyPatchFile(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain(
'Patched node-pty build artifacts are missing; rebuilding from source.'
)
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
expect(rebuildCall.ignoreModules).toEqual(['cpu-features'])
expect(rebuildCall.force).toBe(true)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'keeps the Electron load-probe fast path once patched node-pty artifacts exist',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../build/Release/' })
writeNodePtyPatchFile(projectDir)
writePatchedNodePtyBuildArtifacts(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain(
'Native modules already load in Electron; skipping rebuild.'
)
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'rebuilds when patched artifacts exist but Electron falls back to node-pty prebuilds',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../prebuilds/darwin-arm64/' })
writeNodePtyPatchFile(projectDir)
writePatchedNodePtyBuildArtifacts(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: node-pty')
expect(result.stdout).toContain("expected build/Release so Orca's node-pty patch is active")
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
expect(rebuildCall.force).toBe(true)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
})
@@ -0,0 +1,307 @@
import { spawnSync } from 'node:child_process'
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
const sourceScriptPath = fileURLToPath(new URL('./rebuild-native-deps.mjs', import.meta.url))
const sourceInstallScriptPath = fileURLToPath(
new URL('./install-electron-package-binary.mjs', import.meta.url)
)
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)
)
export function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-rebuild-native-deps-'))
mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true })
copyFileSync(sourceScriptPath, join(projectDir, 'config', 'scripts', 'rebuild-native-deps.mjs'))
copyFileSync(
sourceInstallScriptPath,
join(projectDir, 'config', 'scripts', 'install-electron-package-binary.mjs')
)
copyFileSync(
sourceNodePtyJobOwnershipPath,
join(projectDir, 'config', 'scripts', 'node-pty-job-ownership.cjs')
)
copyFileSync(
sourceWindowsProcessTreeGypRebuildPath,
join(projectDir, 'config', 'scripts', 'windows-process-tree-gyp-rebuild.mjs')
)
return projectDir
}
export function runRebuildScript(projectDir, extraEnv = {}, args = []) {
const env = {
...process.env,
npm_config_platform: 'linux',
npm_config_arch: 'x64',
ORCA_ELECTRON_PACKAGE_EXTRACTOR: join(projectDir, 'fake-extractor.cjs')
}
for (const key of Object.keys(env)) {
if (
key.toLowerCase() === 'orca_strict_electron_install' ||
key.toLowerCase() === 'npm_lifecycle_event'
) {
delete env[key]
}
}
return spawnSync(process.execPath, ['config/scripts/rebuild-native-deps.mjs', ...args], {
cwd: projectDir,
encoding: 'utf8',
env: {
...env,
...extraEnv
}
})
}
export function writeFakeElectronPackage(projectDir) {
const electronDir = join(projectDir, 'node_modules', 'electron')
mkdirSync(electronDir, { recursive: true })
writeFileSync(
join(electronDir, 'package.json'),
JSON.stringify({ name: 'electron', version: '41.5.0' })
)
writeFileSync(join(electronDir, 'checksums.json'), '{}')
writeFileSync(
join(electronDir, 'index.js'),
`
const fs = require('node:fs')
const path = require('node:path')
const pathFile = path.join(__dirname, 'path.txt')
if (!fs.existsSync(pathFile)) {
throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')
}
const electronPath = path.join(__dirname, 'dist', fs.readFileSync(pathFile, 'utf8'))
if (!fs.existsSync(electronPath)) {
throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')
}
module.exports = electronPath
`
)
}
export function writeFakeElectronGet(
projectDir,
{
downloadRejects = false,
logPartialStateBeforeInstall = false,
logTargetBeforeInstall = false
} = {}
) {
const getDir = join(projectDir, 'node_modules', 'electron', 'node_modules', '@electron', 'get')
mkdirSync(getDir, { recursive: true })
writeFileSync(
join(getDir, 'index.js'),
`
const { appendFileSync, existsSync, mkdirSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
exports.downloadArtifact = async function downloadArtifact(details) {
if (${JSON.stringify(logTargetBeforeInstall)}) {
appendFileSync(
'electron-get.log',
'platform=' + details.platform + ' arch=' + details.arch + '\\n'
)
}
if (${JSON.stringify(logPartialStateBeforeInstall)}) {
appendFileSync(
'electron-get.log',
existsSync('node_modules/electron/dist') || existsSync('node_modules/electron/path.txt')
? 'partial still present\\n'
: 'partial cleared\\n'
)
}
appendFileSync('electron-get.log', 'download attempted\\n')
if (${JSON.stringify(downloadRejects)}) {
throw new Error('download failed')
}
mkdirSync(details.cacheRoot, { recursive: true })
const artifactPath = join(details.cacheRoot, 'electron.zip')
writeFileSync(artifactPath, 'fake zip')
return artifactPath
}
`
)
}
export function writeFakeElectronExtractor(projectDir, { createExecutable }) {
writeFileSync(
join(projectDir, 'fake-extractor.cjs'),
`
const { mkdirSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
const extractDir = process.argv[3]
mkdirSync(join(extractDir, 'locales'), { recursive: true })
if (${JSON.stringify(createExecutable)}) {
writeFileSync(join(extractDir, 'electron'), '')
writeFileSync(join(extractDir, 'electron.exe'), '')
writeFileSync(join(extractDir, 'version'), 'v41.5.0')
}
`
)
}
export function writeFakeElectronRebuild(projectDir, { logPathEnv = null } = {}) {
const rebuildDir = join(projectDir, 'node_modules', '@electron', 'rebuild')
mkdirSync(rebuildDir, { recursive: true })
writeFileSync(join(rebuildDir, 'package.json'), JSON.stringify({ type: 'module' }))
writeFileSync(
join(rebuildDir, 'index.js'),
logPathEnv
? `
import { appendFileSync } from 'node:fs'
export async function rebuild(options) {
const logPath = process.env[${JSON.stringify(logPathEnv)}]
if (!logPath) {
return
}
appendFileSync(
logPath,
JSON.stringify({
arch: options.arch,
electronVersion: options.electronVersion,
force: options.force,
ignoreModules: options.ignoreModules,
onlyModules: options.onlyModules,
platform: options.platform
}) + '\\n'
)
}
`
: 'export async function rebuild() {}\n'
)
}
export function writeFakeUsableElectronPackage(projectDir, { platform = 'linux' } = {}) {
writeFakeElectronPackage(projectDir)
const electronDir = join(projectDir, 'node_modules', 'electron')
const platformExecutable = platform === 'win32' ? 'electron.exe' : 'electron'
const electronPath = join(electronDir, 'dist', platformExecutable)
mkdirSync(join(electronDir, 'dist'), { recursive: true })
writeFileSync(join(electronDir, 'path.txt'), platformExecutable)
writeFileSync(join(electronDir, 'dist', 'version'), 'v41.5.0')
if (platform === 'win32') {
copyFileSync(process.execPath, electronPath)
} else {
writeFileSync(
electronPath,
`#!/usr/bin/env node
const { spawnSync } = require('node:child_process')
const result = spawnSync(process.execPath, process.argv.slice(2), {
cwd: process.cwd(),
env: process.env,
stdio: 'inherit'
})
if (result.error) {
console.error(result.error.message)
process.exit(1)
}
process.exit(result.status ?? 0)
`
)
chmodSync(electronPath, 0o755)
}
}
export function writeFakeNodePtyConptyPayload(projectDir, arch) {
const releaseDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release')
mkdirSync(releaseDir, { recursive: true })
writeFileSync(join(releaseDir, 'conpty.node'), 'native addon')
const sourceDir = join(
projectDir,
'node_modules',
'node-pty',
'third_party',
'conpty',
'0.1.0',
`win10-${arch}`
)
mkdirSync(sourceDir, { recursive: true })
writeFileSync(join(sourceDir, 'conpty.dll'), `conpty.dll ${arch}`)
writeFileSync(join(sourceDir, 'OpenConsole.exe'), `OpenConsole.exe ${arch}`)
}
export function writeFakeLoadableNodePty(
projectDir,
{ nativeDir = 'prebuilds/pty', ownsPtyJob = true } = {}
) {
const nodePtyDir = join(projectDir, 'node_modules', 'node-pty')
mkdirSync(join(nodePtyDir, 'lib'), { recursive: true })
writeFileSync(join(nodePtyDir, 'index.js'), 'module.exports = {}\n')
writeFileSync(
join(nodePtyDir, 'lib', 'utils.js'),
`
exports.loadNativeModule = function loadNativeModule(nativeName) {
return {
dir: ${JSON.stringify(nativeDir)},
module: {
nativeName,
...(${JSON.stringify(ownsPtyJob)}
? {
listJobProcessIds() {},
terminateJob() {},
assignCurrentProcessToJob() {}
}
: {})
}
}
}
`
)
}
export function writeFakeWindowsRegistry(projectDir) {
const registryDir = join(projectDir, 'node_modules', 'windows-native-registry')
mkdirSync(registryDir, { recursive: true })
writeFileSync(
join(registryDir, 'index.js'),
'exports.HK = { CU: 0x80000001 }; exports.getRegistryKey = () => ({})\n'
)
}
export function writeFakeWindowsProcessTree(projectDir) {
const processTreeDir = join(projectDir, 'node_modules', '@vscode', 'windows-process-tree')
mkdirSync(processTreeDir, { recursive: true })
writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n')
}
export 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')
}
export function writeNodePtyPatchFile(projectDir) {
mkdirSync(join(projectDir, 'config', 'patches'), { recursive: true })
writeFileSync(join(projectDir, 'config', 'patches', 'node-pty@1.1.0.patch'), 'patch marker\n')
}
export function writePatchedNodePtyBuildArtifacts(projectDir) {
const buildDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release')
mkdirSync(buildDir, { recursive: true })
if (process.platform === 'win32') {
writeFileSync(join(buildDir, 'conpty.node'), '')
mkdirSync(join(buildDir, 'conpty'), { recursive: true })
writeFileSync(join(buildDir, 'conpty', 'conpty.dll'), '')
writeFileSync(join(buildDir, 'conpty', 'OpenConsole.exe'), '')
return
}
writeFileSync(join(buildDir, 'pty.node'), '')
if (process.platform === 'darwin') {
writeFileSync(join(buildDir, 'spawn-helper'), '')
}
}
+50 -36
View File
@@ -28,7 +28,6 @@ import {
mkdirSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { platform as osPlatform } from 'node:os'
@@ -44,6 +43,15 @@ try {
}
const rebuildPlatform = cliOptions.platform ?? osPlatform()
const rebuildArch = cliOptions.arch ?? process.arch
// Why: resolve the Electron download target once so the child installer and the
// usability check can never disagree about which binary should be on disk.
const electronInstallPlatform =
cliOptions.platform ||
process.env.ELECTRON_INSTALL_PLATFORM ||
process.env.npm_config_platform ||
rebuildPlatform
const electronInstallArch =
cliOptions.arch || process.env.ELECTRON_INSTALL_ARCH || process.env.npm_config_arch || rebuildArch
const electronPackageDir = resolve(projectDir, 'node_modules/electron')
const electronVersion = JSON.parse(
readFileSync(resolve(electronPackageDir, 'package.json'), 'utf8')
@@ -208,6 +216,7 @@ function restoreNodePtyWindowsConptyRuntime() {
}
function ensureElectronPackageInstalled() {
repairElectronPathFile()
if (electronPackageIsUsable()) {
return
}
@@ -216,7 +225,6 @@ function ensureElectronPackageInstalled() {
// writing path.txt. Electron 42's lazy require() would run install.js here,
// so inspect dist/ directly and keep using our strict partial-extract checks.
console.log('[rebuild] Electron package binary is missing; installing Electron package binary.')
resetPartialElectronInstall()
try {
runElectronPackageBinaryInstall()
} catch (/** @type {any} */ err) {
@@ -228,37 +236,46 @@ function ensureElectronPackageInstalled() {
process.exit(1)
}
repairElectronPathFile()
if (!electronPackageIsUsable()) {
const repaired = repairElectronPathFile()
if (!repaired || !electronPackageIsUsable()) {
logElectronInstallDiagnostics()
if (continuePostinstallWithoutElectron()) {
process.exit(0)
}
console.error('[rebuild] Electron package is still unavailable after retry.')
process.exit(1)
logElectronInstallDiagnostics()
if (continuePostinstallWithoutElectron()) {
process.exit(0)
}
console.error('[rebuild] Electron package is still unavailable after retry.')
process.exit(1)
}
}
function electronPackageIsUsable() {
try {
const installedVersion = readFileSync(resolve(electronPackageDir, 'dist', 'version'), 'utf8')
.trim()
.replace(/^v/, '')
const installedPlatformPath = readFileSync(resolve(electronPackageDir, 'path.txt'), 'utf8')
return (
installedVersion === electronVersion &&
installedPlatformPath === getElectronPlatformPath() &&
existsSync(getElectronExecutablePath())
electronDistMatchesPackage(getElectronExecutablePath()) &&
installedPlatformPath === getElectronPlatformPath()
)
} catch {
return false
}
}
function electronDistMatchesPackage(electronExecutable) {
try {
const installedVersion = readFileSync(resolve(electronPackageDir, 'dist', 'version'), 'utf8')
.trim()
.replace(/^v/, '')
return installedVersion === electronVersion && existsSync(electronExecutable)
} catch {
return false
}
}
function runElectronPackageBinaryInstall() {
const env = { ...process.env }
const env = {
...process.env,
ELECTRON_INSTALL_PLATFORM: electronInstallPlatform,
ELECTRON_INSTALL_ARCH: electronInstallArch
}
delete env.ELECTRON_SKIP_BINARY_DOWNLOAD
delete env.npm_config_electron_skip_binary_download
@@ -282,13 +299,6 @@ function runElectronPackageBinaryInstall() {
}
}
function resetPartialElectronInstall() {
// Why: Electron's installer can leave a partial dist/ tree behind after
// skipped or interrupted postinstall runs; retry from a clean target.
rmSync(resolve(electronPackageDir, 'dist'), { recursive: true, force: true })
rmSync(resolve(electronPackageDir, 'path.txt'), { force: true })
}
function continuePostinstallWithoutElectron() {
if (!isPostinstall() || process.env.ORCA_STRICT_ELECTRON_INSTALL === '1') {
return false
@@ -303,16 +313,22 @@ function continuePostinstallWithoutElectron() {
function repairElectronPathFile() {
const platformPath = getElectronPlatformPath()
if (!existsSync(getElectronExecutablePath())) {
return false
const electronExecutable = resolve(electronPackageDir, 'dist', platformPath)
if (!electronDistMatchesPackage(electronExecutable)) {
return
}
// Why: Electron's install script has exited successfully in CI after
// extraction without leaving path.txt. The package main only needs this file
// to point at the already-extracted executable.
writeFileSync(resolve(electronPackageDir, 'path.txt'), platformPath)
console.log(`[rebuild] Repaired Electron path.txt -> ${platformPath}`)
return true
const pathFile = resolve(electronPackageDir, 'path.txt')
let currentPath = ''
try {
currentPath = readFileSync(pathFile, 'utf8')
} catch {
// Missing path.txt is the common CI failure this script repairs.
}
if (currentPath !== platformPath) {
writeFileSync(pathFile, platformPath)
console.log(`[rebuild] Repaired Electron path.txt -> ${platformPath}`)
}
}
function logElectronInstallDiagnostics() {
@@ -336,9 +352,7 @@ function safeReaddir(targetPath) {
}
function getElectronPlatformPath() {
const targetPlatform =
process.env.ELECTRON_INSTALL_PLATFORM || process.env.npm_config_platform || rebuildPlatform
switch (targetPlatform) {
switch (electronInstallPlatform) {
case 'mas':
case 'darwin':
return 'Electron.app/Contents/MacOS/Electron'
@@ -349,7 +363,7 @@ function getElectronPlatformPath() {
case 'win32':
return 'electron.exe'
default:
throw new Error(`Electron builds are not available on platform: ${targetPlatform}`)
throw new Error(`Electron builds are not available on platform: ${electronInstallPlatform}`)
}
}
+91 -509
View File
@@ -1,29 +1,16 @@
import { spawnSync } from 'node:child_process'
import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const sourceScriptPath = fileURLToPath(new URL('./rebuild-native-deps.mjs', import.meta.url))
const sourceInstallScriptPath = fileURLToPath(
new URL('./install-electron-package-binary.mjs', import.meta.url)
)
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)
)
import {
mkTempProject,
runRebuildScript,
writeFakeElectronExtractor,
writeFakeElectronGet,
writeFakeElectronPackage,
writeFakeElectronRebuild,
writeFakeUsableElectronPackage
} from './rebuild-native-deps-test-fixtures.mjs'
describe('rebuild-native-deps Electron install fallback', () => {
it('continues non-strict postinstall when Electron retry download fails', () => {
@@ -32,7 +19,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { downloadRejects: true })
writeFakeExtractZip(projectDir, { createExecutable: false })
writeFakeElectronExtractor(projectDir, { createExecutable: false })
writeFakeElectronRebuild(projectDir)
const result = runRebuildScript(projectDir, {
@@ -59,7 +46,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { downloadRejects: true })
writeFakeExtractZip(projectDir, { createExecutable: false })
writeFakeElectronExtractor(projectDir, { createExecutable: false })
writeFakeElectronRebuild(projectDir)
const result = runRebuildScript(projectDir, {
@@ -83,7 +70,7 @@ describe('rebuild-native-deps Electron install fallback', () => {
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { downloadRejects: true })
writeFakeExtractZip(projectDir, { createExecutable: false })
writeFakeElectronExtractor(projectDir, { createExecutable: false })
writeFakeElectronRebuild(projectDir)
const result = runRebuildScript(projectDir)
@@ -98,13 +85,13 @@ describe('rebuild-native-deps Electron install fallback', () => {
}
})
it('clears partial Electron package contents before retrying install', () => {
it('preserves partial Electron package contents while retrying install', () => {
const projectDir = mkTempProject()
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { logPartialStateBeforeInstall: true })
writeFakeExtractZip(projectDir, { createExecutable: false })
writeFakeElectronExtractor(projectDir, { createExecutable: false })
writeFakeElectronRebuild(projectDir)
mkdirSync(join(projectDir, 'node_modules', 'electron', 'dist', 'locales'), {
recursive: true
@@ -121,534 +108,129 @@ describe('rebuild-native-deps Electron install fallback', () => {
expect(result.status).toBe(1)
expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toBe(
'partial cleared\ndownload attempted\n'
'partial still present\ndownload attempted\n'
)
expect(existsSync(join(projectDir, 'node_modules/electron/dist/locales/stale.pak'))).toBe(
true
)
expect(readFileSync(join(projectDir, 'node_modules/electron/path.txt'), 'utf8')).toBe(
'stale-path'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
})
describe('rebuild-native-deps patched node-pty rebuild', () => {
it.skipIf(process.platform !== 'win32')(
'repairs a missing ConPTY runtime before probing without recompiling node-pty',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../build/Release/' })
writeFakeWindowsRegistry(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Restored node-pty ConPTY runtime files')
expect(result.stdout).toContain(
'Native modules already load in Electron; skipping rebuild.'
)
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it('stages windows-process-tree node-addon-api headers before a Windows rebuild', () => {
it('passes the rebuild target to the Electron binary installer', () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { logTargetBeforeInstall: true })
writeFakeElectronExtractor(projectDir, { createExecutable: true })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, 'x64')
writeFakeWindowsProcessTreeWithNodeAddonApi(projectDir)
const result = runRebuildScript(
projectDir,
{ npm_config_platform: 'win32', npm_config_arch: 'x64' },
['--platform=win32', '--arch=x64', '--force']
{ npm_config_platform: '', npm_config_arch: '' },
['--platform=linux', '--arch=arm64', '--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')
expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toBe(
'platform=linux arch=arm64\ndownload attempted\n'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it('restores the ConPTY runtime payload after a Windows Electron rebuild', () => {
it('lets an explicit rebuild target win over inherited installer variables', () => {
const projectDir = mkTempProject()
try {
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeUsableElectronPackage(projectDir, { platform: 'linux' })
writeFakeElectronGet(projectDir, { logTargetBeforeInstall: true })
writeFakeElectronRebuild(projectDir)
writeFakeNodePtyConptyPayload(projectDir, 'x64')
const result = runRebuildScript(
projectDir,
{ npm_config_platform: 'win32', npm_config_arch: 'x64' },
['--platform=win32', '--arch=x64', '--force']
{ ELECTRON_INSTALL_PLATFORM: 'win32', ELECTRON_INSTALL_ARCH: 'arm64' },
['--platform=linux', '--arch=x64', '--force']
)
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Restored node-pty ConPTY runtime files for win10-x64')
const runtimeDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release', 'conpty')
expect(readFileSync(join(runtimeDir, 'conpty.dll'), 'utf8')).toBe('conpty.dll x64')
expect(readFileSync(join(runtimeDir, 'OpenConsole.exe'), 'utf8')).toBe('OpenConsole.exe x64')
expect(existsSync(join(projectDir, 'electron-get.log'))).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
it.skipIf(process.platform !== 'win32')(
'does not rebuild a healthy node-pty when another Windows addon fails its probe',
() => {
const projectDir = mkTempProject()
it('installs the inherited installer target when no rebuild target is passed', () => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir)
writeFakeWindowsProcessTree(projectDir)
writeFakeNodePtyConptyPayload(projectDir, process.arch)
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { logTargetBeforeInstall: true })
writeFakeElectronExtractor(projectDir, { createExecutable: true })
writeFakeElectronRebuild(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
const result = runRebuildScript(projectDir, {
ELECTRON_INSTALL_PLATFORM: 'win32',
ELECTRON_INSTALL_ARCH: 'arm64'
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: windows-native-registry')
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['windows-native-registry'])
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform !== 'win32')(
'rebuilds a loadable ConPTY native that lacks Orca job ownership',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir, { platform: 'win32' })
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { ownsPtyJob: false })
writeFakeWindowsRegistry(projectDir)
writeFakeWindowsProcessTree(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath,
npm_config_platform: 'win32',
npm_config_arch: process.arch
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: node-pty')
expect(result.stdout).toContain('missing listJobProcessIds')
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'rebuilds when Electron can load node-pty but patched build artifacts are missing',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir)
writeNodePtyPatchFile(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain(
'Patched node-pty build artifacts are missing; rebuilding from source.'
)
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
expect(rebuildCall.ignoreModules).toEqual(['cpu-features'])
expect(rebuildCall.force).toBe(true)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'keeps the Electron load-probe fast path once patched node-pty artifacts exist',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../build/Release/' })
writeNodePtyPatchFile(projectDir)
writePatchedNodePtyBuildArtifacts(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain(
'Native modules already load in Electron; skipping rebuild.'
)
expect(existsSync(rebuildLogPath)).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'rebuilds when patched artifacts exist but Electron falls back to node-pty prebuilds',
() => {
const projectDir = mkTempProject()
try {
const rebuildLogPath = join(projectDir, 'electron-rebuild.log')
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir, { logPathEnv: 'ORCA_REBUILD_TEST_LOG' })
writeFakeLoadableNodePty(projectDir, { nativeDir: '../prebuilds/darwin-arm64/' })
writeNodePtyPatchFile(projectDir)
writePatchedNodePtyBuildArtifacts(projectDir)
const result = runRebuildScript(projectDir, {
ORCA_REBUILD_TEST_LOG: rebuildLogPath
})
expect(result.status, result.stderr).toBe(0)
expect(result.stdout).toContain('Rebuilding failed native modules: node-pty')
expect(result.stdout).toContain("expected build/Release so Orca's node-pty patch is active")
const rebuildCall = JSON.parse(readFileSync(rebuildLogPath, 'utf8').trim())
expect(rebuildCall.onlyModules).toEqual(['node-pty'])
expect(rebuildCall.force).toBe(true)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
)
})
function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-rebuild-native-deps-'))
mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true })
copyFileSync(sourceScriptPath, join(projectDir, 'config', 'scripts', 'rebuild-native-deps.mjs'))
copyFileSync(
sourceInstallScriptPath,
join(projectDir, 'config', 'scripts', 'install-electron-package-binary.mjs')
)
copyFileSync(
sourceNodePtyJobOwnershipPath,
join(projectDir, 'config', 'scripts', 'node-pty-job-ownership.cjs')
)
copyFileSync(
sourceWindowsProcessTreeGypRebuildPath,
join(projectDir, 'config', 'scripts', 'windows-process-tree-gyp-rebuild.mjs')
)
return projectDir
}
function runRebuildScript(projectDir, extraEnv = {}, args = []) {
const env = {
...process.env,
npm_config_platform: 'linux',
npm_config_arch: 'x64'
}
for (const key of Object.keys(env)) {
if (
key.toLowerCase() === 'orca_strict_electron_install' ||
key.toLowerCase() === 'npm_lifecycle_event'
) {
delete env[key]
}
}
return spawnSync(process.execPath, ['config/scripts/rebuild-native-deps.mjs', ...args], {
cwd: projectDir,
encoding: 'utf8',
env: {
...env,
...extraEnv
expect(result.status, result.stderr).toBe(0)
expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toContain(
'platform=win32 arch=arm64'
)
expect(readFileSync(join(projectDir, 'node_modules/electron/path.txt'), 'utf8')).toBe(
'electron.exe'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
}
function writeFakeElectronPackage(projectDir) {
const electronDir = join(projectDir, 'node_modules', 'electron')
mkdirSync(electronDir, { recursive: true })
writeFileSync(
join(electronDir, 'package.json'),
JSON.stringify({ name: 'electron', version: '41.5.0' })
)
writeFileSync(join(electronDir, 'checksums.json'), '{}')
writeFileSync(
join(electronDir, 'index.js'),
`
const fs = require('node:fs')
const path = require('node:path')
const pathFile = path.join(__dirname, 'path.txt')
if (!fs.existsSync(pathFile)) {
throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')
}
const electronPath = path.join(__dirname, 'dist', fs.readFileSync(pathFile, 'utf8'))
if (!fs.existsSync(electronPath)) {
throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')
}
module.exports = electronPath
`
)
}
it('falls back to npm config when no rebuild or installer target is set', () => {
const projectDir = mkTempProject()
function writeFakeElectronGet(
projectDir,
{ downloadRejects = false, logPartialStateBeforeInstall = false } = {}
) {
const getDir = join(projectDir, 'node_modules', 'electron', 'node_modules', '@electron', 'get')
mkdirSync(getDir, { recursive: true })
writeFileSync(
join(getDir, 'index.js'),
`
const { appendFileSync, existsSync, mkdirSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
exports.downloadArtifact = async function downloadArtifact(details) {
if (${JSON.stringify(logPartialStateBeforeInstall)}) {
appendFileSync(
'electron-get.log',
existsSync('node_modules/electron/dist') || existsSync('node_modules/electron/path.txt')
? 'partial still present\\n'
: 'partial cleared\\n'
)
}
appendFileSync('electron-get.log', 'download attempted\\n')
if (${JSON.stringify(downloadRejects)}) {
throw new Error('download failed')
}
mkdirSync(details.cacheRoot, { recursive: true })
const artifactPath = join(details.cacheRoot, 'electron.zip')
writeFileSync(artifactPath, 'fake zip')
return artifactPath
}
`
)
}
try {
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { logTargetBeforeInstall: true })
writeFakeElectronExtractor(projectDir, { createExecutable: true })
writeFakeElectronRebuild(projectDir)
function writeFakeExtractZip(projectDir, { createExecutable }) {
const extractDir = join(projectDir, 'node_modules', 'electron', 'node_modules', 'extract-zip')
mkdirSync(extractDir, { recursive: true })
writeFileSync(
join(extractDir, 'index.js'),
`
const { mkdirSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
module.exports = async function extract(_zipPath, options) {
mkdirSync(join(options.dir, 'locales'), { recursive: true })
if (${JSON.stringify(createExecutable)}) {
writeFileSync(join(options.dir, 'electron'), '')
writeFileSync(join(options.dir, 'version'), 'v41.5.0')
}
}
`
)
chmodSync(join(extractDir, 'index.js'), 0o755)
}
// runRebuildScript defaults npm_config_platform=linux / npm_config_arch=x64.
const result = runRebuildScript(projectDir)
function writeFakeElectronRebuild(projectDir, { logPathEnv = null } = {}) {
const rebuildDir = join(projectDir, 'node_modules', '@electron', 'rebuild')
mkdirSync(rebuildDir, { recursive: true })
writeFileSync(join(rebuildDir, 'package.json'), JSON.stringify({ type: 'module' }))
writeFileSync(
join(rebuildDir, 'index.js'),
logPathEnv
? `
import { appendFileSync } from 'node:fs'
export async function rebuild(options) {
const logPath = process.env[${JSON.stringify(logPathEnv)}]
if (!logPath) {
return
}
appendFileSync(
logPath,
JSON.stringify({
arch: options.arch,
electronVersion: options.electronVersion,
force: options.force,
ignoreModules: options.ignoreModules,
onlyModules: options.onlyModules,
platform: options.platform
}) + '\\n'
)
}
`
: 'export async function rebuild() {}\n'
)
}
function writeFakeUsableElectronPackage(projectDir, { platform = 'linux' } = {}) {
writeFakeElectronPackage(projectDir)
const electronDir = join(projectDir, 'node_modules', 'electron')
const platformExecutable = platform === 'win32' ? 'electron.exe' : 'electron'
const electronPath = join(electronDir, 'dist', platformExecutable)
mkdirSync(join(electronDir, 'dist'), { recursive: true })
writeFileSync(join(electronDir, 'path.txt'), platformExecutable)
writeFileSync(join(electronDir, 'dist', 'version'), 'v41.5.0')
if (platform === 'win32') {
copyFileSync(process.execPath, electronPath)
} else {
writeFileSync(
electronPath,
`#!/usr/bin/env node
const { spawnSync } = require('node:child_process')
const result = spawnSync(process.execPath, process.argv.slice(2), {
cwd: process.cwd(),
env: process.env,
stdio: 'inherit'
})
if (result.error) {
console.error(result.error.message)
process.exit(1)
}
process.exit(result.status ?? 0)
`
)
chmodSync(electronPath, 0o755)
}
}
function writeFakeNodePtyConptyPayload(projectDir, arch) {
const releaseDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release')
mkdirSync(releaseDir, { recursive: true })
writeFileSync(join(releaseDir, 'conpty.node'), 'native addon')
const sourceDir = join(
projectDir,
'node_modules',
'node-pty',
'third_party',
'conpty',
'0.1.0',
`win10-${arch}`
)
mkdirSync(sourceDir, { recursive: true })
writeFileSync(join(sourceDir, 'conpty.dll'), `conpty.dll ${arch}`)
writeFileSync(join(sourceDir, 'OpenConsole.exe'), `OpenConsole.exe ${arch}`)
}
function writeFakeLoadableNodePty(
projectDir,
{ nativeDir = 'prebuilds/pty', ownsPtyJob = true } = {}
) {
const nodePtyDir = join(projectDir, 'node_modules', 'node-pty')
mkdirSync(join(nodePtyDir, 'lib'), { recursive: true })
writeFileSync(join(nodePtyDir, 'index.js'), 'module.exports = {}\n')
writeFileSync(
join(nodePtyDir, 'lib', 'utils.js'),
`
exports.loadNativeModule = function loadNativeModule(nativeName) {
return {
dir: ${JSON.stringify(nativeDir)},
module: {
nativeName,
...(${JSON.stringify(ownsPtyJob)}
? {
listJobProcessIds() {},
terminateJob() {},
assignCurrentProcessToJob() {}
}
: {})
expect(result.status, result.stderr).toBe(0)
expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toContain(
'platform=linux arch=x64'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
}
}
`
)
}
})
function writeFakeWindowsRegistry(projectDir) {
const registryDir = join(projectDir, 'node_modules', 'windows-native-registry')
mkdirSync(registryDir, { recursive: true })
writeFileSync(
join(registryDir, 'index.js'),
'exports.HK = { CU: 0x80000001 }; exports.getRegistryKey = () => ({})\n'
)
}
it('repairs existing Electron path metadata without invoking the installer', () => {
const projectDir = mkTempProject()
function writeFakeWindowsProcessTree(projectDir) {
const processTreeDir = join(projectDir, 'node_modules', '@vscode', 'windows-process-tree')
mkdirSync(processTreeDir, { recursive: true })
writeFileSync(join(processTreeDir, 'index.js'), 'module.exports = {}\n')
}
try {
writeFakeUsableElectronPackage(projectDir)
writeFakeElectronRebuild(projectDir)
rmSync(join(projectDir, 'node_modules/electron/path.txt'))
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')
}
const result = runRebuildScript(projectDir, {}, ['--platform=linux', '--force'])
function writeNodePtyPatchFile(projectDir) {
mkdirSync(join(projectDir, 'config', 'patches'), { recursive: true })
writeFileSync(join(projectDir, 'config', 'patches', 'node-pty@1.1.0.patch'), 'patch marker\n')
}
function writePatchedNodePtyBuildArtifacts(projectDir) {
const buildDir = join(projectDir, 'node_modules', 'node-pty', 'build', 'Release')
mkdirSync(buildDir, { recursive: true })
if (process.platform === 'win32') {
writeFileSync(join(buildDir, 'conpty.node'), '')
mkdirSync(join(buildDir, 'conpty'), { recursive: true })
writeFileSync(join(buildDir, 'conpty', 'conpty.dll'), '')
writeFileSync(join(buildDir, 'conpty', 'OpenConsole.exe'), '')
return
}
writeFileSync(join(buildDir, 'pty.node'), '')
if (process.platform === 'darwin') {
writeFileSync(join(buildDir, 'spawn-helper'), '')
}
}
expect(result.status, result.stderr).toBe(0)
expect(readFileSync(join(projectDir, 'node_modules/electron/path.txt'), 'utf8')).toBe(
'electron'
)
expect(result.stdout).toContain('Repaired Electron path.txt -> electron')
expect(existsSync(join(projectDir, 'electron-get.log'))).toBe(false)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
})