mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
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:
@@ -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'), '')
|
||||
}
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user