Fix Electron binary repair in release CI

Use the stricter Electron package binary installer when native rebuild needs to repair a missing Electron executable, avoiding partial dist/path.txt installs in release CI.
This commit is contained in:
Neil 2026-05-27 17:25:22 -07:00 committed by GitHub
parent 7e1cde4673
commit a2a3c348fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 149 additions and 47 deletions

View File

@ -137,19 +137,12 @@ function ensureElectronPackageInstalled() {
}
// Why: CI has observed Electron's postinstall exiting cleanly without
// writing path.txt; native rebuild and tests both need the binary path.
console.log('[rebuild] Electron package binary is missing; rerunning Electron install.')
// writing path.txt; use our strict installer instead of Electron's install.js
// so partial extracts and poisoned caches are rejected before native rebuild.
console.log('[rebuild] Electron package binary is missing; installing Electron package binary.')
resetPartialElectronInstall()
try {
execFileSync(process.execPath, [require.resolve('electron/install.js')], {
cwd: projectDir,
env: {
...process.env,
ELECTRON_SKIP_BINARY_DOWNLOAD: '',
force_no_cache: 'true'
},
stdio: 'inherit'
})
runElectronPackageBinaryInstall()
} catch (/** @type {any} */ err) {
console.error('[rebuild] Electron install retry failed:', err?.message ?? err)
logElectronInstallDiagnostics()
@ -185,6 +178,31 @@ function ensureElectronPackageInstalled() {
}
}
function runElectronPackageBinaryInstall() {
const env = { ...process.env }
delete env.ELECTRON_SKIP_BINARY_DOWNLOAD
delete env.npm_config_electron_skip_binary_download
const result = spawnSync(
process.execPath,
['config/scripts/install-electron-package-binary.mjs'],
{
cwd: projectDir,
env,
stdio: 'inherit'
}
)
if (result.error) {
throw result.error
}
if (result.status !== 0) {
throw new Error(
`config/scripts/install-electron-package-binary.mjs exited with status ${result.status}`
)
}
}
function resetPartialElectronInstall() {
const electronPackageDir = resolve(projectDir, 'node_modules/electron')
// Why: Electron's installer can leave a partial dist/ tree behind after

View File

@ -1,18 +1,31 @@
import { spawnSync } from 'node:child_process'
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import {
chmodSync,
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
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)
)
describe('rebuild-native-deps Electron install fallback', () => {
it('continues non-strict postinstall when Electron retry download fails', () => {
const projectDir = mkTempProject()
try {
writeFakeElectronPackage(projectDir, { installExitsWith: 1 })
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { downloadRejects: true })
writeFakeExtractZip(projectDir, { createExecutable: false })
writeFakeElectronRebuild(projectDir)
const result = runRebuildScript(projectDir, {
@ -22,8 +35,12 @@ describe('rebuild-native-deps Electron install fallback', () => {
expect(result.status, result.stderr).toBe(0)
expect(result.stderr).toContain('Electron install retry failed')
expect(result.stderr).toContain('Continuing postinstall because Electron binary installation failed')
expect(readFileSync(join(projectDir, 'electron-install.log'), 'utf8')).toBe('install attempted\n')
expect(result.stderr).toContain(
'Continuing postinstall because Electron binary installation failed'
)
expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toBe(
'download attempted\n'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
@ -33,7 +50,9 @@ describe('rebuild-native-deps Electron install fallback', () => {
const projectDir = mkTempProject()
try {
writeFakeElectronPackage(projectDir, { installExitsWith: 1 })
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { downloadRejects: true })
writeFakeExtractZip(projectDir, { createExecutable: false })
writeFakeElectronRebuild(projectDir)
const result = runRebuildScript(projectDir, {
@ -43,7 +62,9 @@ describe('rebuild-native-deps Electron install fallback', () => {
expect(result.status).toBe(1)
expect(result.stderr).toContain('Electron install retry failed')
expect(result.stderr).not.toContain('Continuing postinstall because Electron binary installation failed')
expect(result.stderr).not.toContain(
'Continuing postinstall because Electron binary installation failed'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
@ -53,14 +74,18 @@ describe('rebuild-native-deps Electron install fallback', () => {
const projectDir = mkTempProject()
try {
writeFakeElectronPackage(projectDir, { installExitsWith: 1 })
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { downloadRejects: true })
writeFakeExtractZip(projectDir, { createExecutable: false })
writeFakeElectronRebuild(projectDir)
const result = runRebuildScript(projectDir)
expect(result.status).toBe(1)
expect(result.stderr).toContain('Electron install retry failed')
expect(result.stderr).not.toContain('Continuing postinstall because Electron binary installation failed')
expect(result.stderr).not.toContain(
'Continuing postinstall because Electron binary installation failed'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
@ -70,13 +95,17 @@ describe('rebuild-native-deps Electron install fallback', () => {
const projectDir = mkTempProject()
try {
writeFakeElectronPackage(projectDir, {
installExitsWith: 1,
logPartialStateBeforeInstall: true
})
writeFakeElectronPackage(projectDir)
writeFakeElectronGet(projectDir, { logPartialStateBeforeInstall: true })
writeFakeExtractZip(projectDir, { createExecutable: false })
writeFakeElectronRebuild(projectDir)
mkdirSync(join(projectDir, 'node_modules', 'electron', 'dist', 'locales'), { recursive: true })
writeFileSync(join(projectDir, 'node_modules', 'electron', 'dist', 'locales', 'stale.pak'), '')
mkdirSync(join(projectDir, 'node_modules', 'electron', 'dist', 'locales'), {
recursive: true
})
writeFileSync(
join(projectDir, 'node_modules', 'electron', 'dist', 'locales', 'stale.pak'),
''
)
writeFileSync(join(projectDir, 'node_modules', 'electron', 'path.txt'), 'stale-path')
const result = runRebuildScript(projectDir, {
@ -84,8 +113,8 @@ describe('rebuild-native-deps Electron install fallback', () => {
})
expect(result.status).toBe(1)
expect(readFileSync(join(projectDir, 'electron-install.log'), 'utf8')).toBe(
'partial cleared\ninstall attempted\n'
expect(readFileSync(join(projectDir, 'electron-get.log'), 'utf8')).toBe(
'partial cleared\ndownload attempted\n'
)
} finally {
rmSync(projectDir, { recursive: true, force: true })
@ -97,6 +126,10 @@ 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')
)
return projectDir
}
@ -106,39 +139,90 @@ function runRebuildScript(projectDir, extraEnv = {}) {
encoding: 'utf8',
env: {
...process.env,
npm_config_platform: 'linux',
npm_config_arch: 'x64',
...extraEnv
}
})
}
function writeFakeElectronPackage(
projectDir,
{ installExitsWith, logPartialStateBeforeInstall = false }
) {
function writeFakeElectronPackage(projectDir) {
const electronDir = join(projectDir, 'node_modules', 'electron')
mkdirSync(electronDir, { recursive: true })
writeFileSync(join(electronDir, 'package.json'), JSON.stringify({ version: '41.5.0' }))
writeFileSync(
join(electronDir, 'package.json'),
JSON.stringify({ name: 'electron', version: '41.5.0' })
)
writeFileSync(join(electronDir, 'checksums.json'), '{}')
writeFileSync(
join(electronDir, 'index.js'),
"throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again')\n"
)
writeFileSync(
join(electronDir, 'install.js'),
`
const { appendFileSync, existsSync } = require('node:fs')
if (${JSON.stringify(logPartialStateBeforeInstall)}) {
appendFileSync(
'electron-install.log',
existsSync('node_modules/electron/dist') || existsSync('node_modules/electron/path.txt')
? 'partial still present\\n'
: 'partial cleared\\n'
)
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')
}
appendFileSync('electron-install.log', 'install attempted\\n')
process.exit(${installExitsWith})
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
`
)
chmodSync(join(electronDir, 'install.js'), 0o755)
}
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
}
`
)
}
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)
}
function writeFakeElectronRebuild(projectDir) {