From a2a3c348fac5e0f8955bbd68c9c958a54b77ee17 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 27 May 2026 17:25:22 -0700 Subject: [PATCH] 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. --- config/scripts/rebuild-native-deps.mjs | 40 +++-- config/scripts/rebuild-native-deps.test.mjs | 156 +++++++++++++++----- 2 files changed, 149 insertions(+), 47 deletions(-) diff --git a/config/scripts/rebuild-native-deps.mjs b/config/scripts/rebuild-native-deps.mjs index 80e8b3c74..49ebc103d 100644 --- a/config/scripts/rebuild-native-deps.mjs +++ b/config/scripts/rebuild-native-deps.mjs @@ -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 diff --git a/config/scripts/rebuild-native-deps.test.mjs b/config/scripts/rebuild-native-deps.test.mjs index 7dbb96f29..55ff6c41f 100644 --- a/config/scripts/rebuild-native-deps.test.mjs +++ b/config/scripts/rebuild-native-deps.test.mjs @@ -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) {