fix: skip optional native rebuild in release packaging

This commit is contained in:
Neil 2026-05-30 19:32:24 -07:00
parent f450813a21
commit ce0cd79ec1
5 changed files with 177 additions and 6 deletions

View File

@ -1,6 +1,7 @@
const { chmodSync, existsSync, readdirSync } = require('node:fs')
const { execFileSync } = require('node:child_process')
const { join, resolve } = require('node:path')
const electronBuilderNativeRebuild = require('./scripts/electron-builder-native-rebuild.cjs')
const isMacRelease = process.env.ORCA_MAC_RELEASE === '1'
const featureWallResources = {
@ -230,11 +231,13 @@ module.exports = {
artifactName: 'orca-ide-${version}.${arch}.${ext}',
depends: ['python3', 'python3-gobject', 'at-spi2-core', 'xdotool', 'xclip']
},
beforeBuild: electronBuilderNativeRebuild,
// Why: must be true so that electron-builder rebuilds native modules
// (node-pty) for each target architecture when producing dual-arch macOS
// builds (x64 + arm64). With npmRebuild disabled, CI on an arm64 runner
// packages arm64 binaries into the x64 DMG, causing "posix_spawnp failed"
// on Intel Macs.
// on Intel Macs. The beforeBuild hook performs Orca's targeted rebuild and
// returns false so electron-builder does not rebuild optional cpu-features.
npmRebuild: true,
publish: {
provider: 'github',

View File

@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const electronBuilderConfig = require('../electron-builder.config.cjs')
const electronBuilderNativeRebuild = require('./electron-builder-native-rebuild.cjs')
describe('electron-builder config', () => {
it('uses the multi-size icon source for Linux packages', () => {
@ -18,4 +19,9 @@ describe('electron-builder config', () => {
artifactName: 'orca-ide-${version}.${arch}.${ext}'
})
})
it('uses Orca native rebuild hook instead of electron-builder default rebuild', () => {
expect(electronBuilderConfig.beforeBuild).toBe(electronBuilderNativeRebuild)
expect(electronBuilderConfig.npmRebuild).toBe(true)
})
})

View File

@ -0,0 +1,52 @@
const { execFileSync } = require('node:child_process')
const { resolve } = require('node:path')
const projectDir = resolve(__dirname, '../..')
function electronBuilderNativeRebuild(context) {
return runElectronBuilderNativeRebuild(context)
}
function runElectronBuilderNativeRebuild(context, runner = execFileSync) {
const args = buildNativeRebuildArgs(context)
runner(process.execPath, args, {
cwd: projectDir,
stdio: 'inherit'
})
// Why: returning false tells electron-builder that native deps were handled
// externally, avoiding its all-module rebuild of optional cpu-features.
return false
}
function buildNativeRebuildArgs(context) {
const platform = readPlatformName(context?.platform)
const arch = readArchName(context?.arch)
return [
'config/scripts/rebuild-native-deps.mjs',
`--platform=${platform}`,
`--arch=${arch}`,
'--force'
]
}
function readPlatformName(platform) {
const name = typeof platform === 'string' ? platform : platform?.nodeName
if (!name) {
throw new Error('electron-builder native rebuild context is missing platform.nodeName')
}
return name
}
function readArchName(arch) {
if (!arch || typeof arch !== 'string') {
throw new Error('electron-builder native rebuild context is missing arch')
}
return arch
}
module.exports = electronBuilderNativeRebuild
module.exports.default = electronBuilderNativeRebuild
module.exports.buildNativeRebuildArgs = buildNativeRebuildArgs
module.exports.runElectronBuilderNativeRebuild = runElectronBuilderNativeRebuild

View File

@ -0,0 +1,49 @@
import { createRequire } from 'node:module'
import { describe, expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const {
buildNativeRebuildArgs,
runElectronBuilderNativeRebuild
} = require('./electron-builder-native-rebuild.cjs')
describe('electron-builder native rebuild hook', () => {
it('passes the target platform and arch to Orca native rebuild script', () => {
expect(
buildNativeRebuildArgs({
platform: { nodeName: 'darwin' },
arch: 'x64'
})
).toEqual([
'config/scripts/rebuild-native-deps.mjs',
'--platform=darwin',
'--arch=x64',
'--force'
])
})
it('returns false so electron-builder skips its optional module rebuild pass', () => {
const calls = []
const result = runElectronBuilderNativeRebuild(
{
platform: { nodeName: 'linux' },
arch: 'arm64'
},
(...args) => calls.push(args)
)
expect(result).toBe(false)
expect(calls).toEqual([
[
process.execPath,
['config/scripts/rebuild-native-deps.mjs', '--platform=linux', '--arch=arm64', '--force'],
expect.objectContaining({ stdio: 'inherit' })
]
])
})
it('rejects incomplete electron-builder contexts', () => {
expect(() => buildNativeRebuildArgs({ arch: 'x64' })).toThrow(/platform/)
expect(() => buildNativeRebuildArgs({ platform: { nodeName: 'linux' } })).toThrow(/arch/)
})
})

View File

@ -11,8 +11,9 @@
* `pnpm install` from completing.
*
* This script replaces `electron-builder install-app-deps` in the postinstall
* lifecycle. It calls @electron/rebuild's JS API directly so that we can skip
* `cpu-features` when rebuilding modules against Electron. Skipping
* lifecycle and the electron-builder beforeBuild hook. It calls
* @electron/rebuild's JS API directly so that we can skip `cpu-features` when
* rebuilding modules against Electron. Skipping
* cpu-features is safe: ssh2 detects the missing native module and falls back
* to pure-JS CPU feature detection automatically.
*/
@ -24,6 +25,15 @@ import { platform as osPlatform } from 'node:os'
import { resolve } from 'node:path'
const projectDir = process.cwd()
let cliOptions
try {
cliOptions = readCliOptions(process.argv.slice(2))
} catch (error) {
console.error(`[rebuild] ${formatError(error)}`)
process.exit(2)
}
const rebuildPlatform = cliOptions.platform ?? osPlatform()
const rebuildArch = cliOptions.arch ?? process.arch
const electronPackageDir = resolve(projectDir, 'node_modules/electron')
const electronVersion = JSON.parse(
readFileSync(resolve(electronPackageDir, 'package.json'), 'utf8')
@ -41,7 +51,11 @@ if (ignoreModules.length > 0) {
// ABI regardless of the package manager's store layout.
const NATIVE_MODULES = ['node-pty', 'cpu-features']
const onlyModules = NATIVE_MODULES.filter((m) => !ignoreModules.includes(m))
const forceRebuild = process.env.ORCA_FORCE_NATIVE_REBUILD === '1'
const forceRebuild =
process.env.ORCA_FORCE_NATIVE_REBUILD === '1' ||
cliOptions.force ||
rebuildPlatform !== osPlatform() ||
rebuildArch !== process.arch
ensureElectronPackageInstalled()
@ -58,7 +72,7 @@ if (!forceRebuild) {
console.log(probe.stderr.trim())
}
} else {
console.log('[rebuild] ORCA_FORCE_NATIVE_REBUILD=1 set; forcing native rebuild.')
console.log(`[rebuild] Forcing native rebuild for ${rebuildPlatform}-${rebuildArch}.`)
}
// Why: cpu-features ships without `buildcheck.gypi`; its own `install` script
@ -95,6 +109,8 @@ try {
await rebuild({
buildPath: projectDir,
electronVersion,
platform: rebuildPlatform,
arch: rebuildArch,
ignoreModules,
onlyModules,
// Why: without force, @electron/rebuild skips modules it considers
@ -253,7 +269,7 @@ function safeReaddir(targetPath) {
function getElectronPlatformPath() {
const targetPlatform =
process.env.ELECTRON_INSTALL_PLATFORM || process.env.npm_config_platform || osPlatform()
process.env.ELECTRON_INSTALL_PLATFORM || process.env.npm_config_platform || rebuildPlatform
switch (targetPlatform) {
case 'mas':
case 'darwin':
@ -269,6 +285,51 @@ function getElectronPlatformPath() {
}
}
function readCliOptions(args) {
const options = { force: false }
for (let index = 0; index < args.length; index += 1) {
const arg = args[index]
if (arg === '--force') {
options.force = true
continue
}
if (arg === '--platform') {
options.platform = readRequiredArgValue(args, (index += 1), '--platform')
continue
}
if (arg.startsWith('--platform=')) {
options.platform = readInlineArgValue(arg, '--platform')
continue
}
if (arg === '--arch') {
options.arch = readRequiredArgValue(args, (index += 1), '--arch')
continue
}
if (arg.startsWith('--arch=')) {
options.arch = readInlineArgValue(arg, '--arch')
continue
}
throw new Error(`Unknown argument: ${arg}`)
}
return options
}
function readRequiredArgValue(args, index, flag) {
const value = args[index]
if (!value || value.startsWith('--')) {
throw new Error(`Missing value for ${flag}`)
}
return value
}
function readInlineArgValue(arg, flag) {
const value = arg.slice(`${flag}=`.length)
if (!value) {
throw new Error(`Missing value for ${flag}`)
}
return value
}
function getElectronExecutablePath() {
const platformPath = getElectronPlatformPath()
return process.env.ELECTRON_OVERRIDE_DIST_PATH