mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix(orcad): stop demanding a spawn-helper only macOS builds (#18122)
node-pty declares the spawn-helper target inside binding.gyp's OS=="mac" block and pty.cc execs it only under __APPLE__. Asserting it on `!== 'win32'` made every Linux orcad boot degraded with spawn_helper_missing while its terminals worked fine. Route all four sites through one shared `usesNodePtySpawnHelper` predicate: the precondition verdict, the prebuilt slot install, the +x repair, and the prebuilds build script (which threw outright on a Linux slot build). Fixes #17844
This commit is contained in:
@@ -177,10 +177,11 @@ function build() {
|
||||
copyFileSync(builtBinary, join(slotDir, 'pty.node'))
|
||||
console.log(`[orcad-prebuilds] stored ${slot}/pty.node`)
|
||||
|
||||
// Why spawn-helper ships too: on Unix node-pty posix_spawns build/Release/spawn-helper,
|
||||
// Why spawn-helper ships too: on macOS node-pty posix_spawns build/Release/spawn-helper,
|
||||
// so a slot without it installs cleanly and then fails ENOENT the first time a user
|
||||
// opens a terminal. Windows has no spawn-helper.
|
||||
if (process.platform !== 'win32') {
|
||||
// opens a terminal. binding.gyp builds the helper only under OS=="mac"; every other
|
||||
// platform forks directly, so demanding one there fails a healthy Linux slot build.
|
||||
if (process.platform === 'darwin') {
|
||||
const helperSource = join(dirname(builtBinary), 'spawn-helper')
|
||||
if (!existsSync(helperSource)) {
|
||||
throw new Error(`[orcad-prebuilds] spawn-helper missing at ${helperSource}`)
|
||||
|
||||
@@ -17,6 +17,14 @@ const LINUX_GLIBC: NativeHostAbi = {
|
||||
nodeAbi: '127'
|
||||
}
|
||||
|
||||
const DARWIN_ARM64: NativeHostAbi = {
|
||||
platform: 'darwin',
|
||||
arch: 'arm64',
|
||||
libc: 'none',
|
||||
glibcVersion: null,
|
||||
nodeAbi: '127'
|
||||
}
|
||||
|
||||
const dirs: string[] = []
|
||||
const temp = (): string => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'orcad-slot-'))
|
||||
@@ -48,18 +56,32 @@ describe('resolveOrcadPrebuildsDir', () => {
|
||||
})
|
||||
|
||||
describe('installPrebuiltSlot', () => {
|
||||
it('installs the slot binary and spawn-helper into build/Release', () => {
|
||||
it('installs the slot binary and spawn-helper into build/Release on macOS', () => {
|
||||
const prebuilds = temp()
|
||||
const nodePtyDir = temp()
|
||||
stageSlot(prebuilds, 'darwin-arm64')
|
||||
|
||||
const outcome = installPrebuiltSlot({ abi: DARWIN_ARM64, nodePtyDir, prebuildsDir: prebuilds })
|
||||
|
||||
expect(outcome).toEqual({ installed: true, slot: 'darwin-arm64', spawnHelper: true })
|
||||
expect(existsSync(join(nodePtyDir, 'build', 'Release', 'pty.node'))).toBe(true)
|
||||
// Without the executable bit every spawn fails EACCES at the moment a user opens a terminal.
|
||||
const helper = statSync(join(nodePtyDir, 'build', 'Release', 'spawn-helper'))
|
||||
expect(helper.mode & 0o111).not.toBe(0)
|
||||
})
|
||||
|
||||
it('installs a Linux slot without claiming a spawn-helper it never execs', () => {
|
||||
// node-pty builds spawn-helper only under binding.gyp's OS=="mac"; reporting one off
|
||||
// macOS is what made every Linux orcad boot degraded on spawn_helper_missing (#17844).
|
||||
const prebuilds = temp()
|
||||
const nodePtyDir = temp()
|
||||
stageSlot(prebuilds, 'linux-x64-glibc')
|
||||
|
||||
const outcome = installPrebuiltSlot({ abi: LINUX_GLIBC, nodePtyDir, prebuildsDir: prebuilds })
|
||||
|
||||
expect(outcome).toEqual({ installed: true, slot: 'linux-x64-glibc', spawnHelper: true })
|
||||
expect(outcome).toEqual({ installed: true, slot: 'linux-x64-glibc', spawnHelper: false })
|
||||
expect(existsSync(join(nodePtyDir, 'build', 'Release', 'pty.node'))).toBe(true)
|
||||
// Without the executable bit every spawn fails EACCES at the moment a user opens a terminal.
|
||||
const helper = statSync(join(nodePtyDir, 'build', 'Release', 'spawn-helper'))
|
||||
expect(helper.mode & 0o111).not.toBe(0)
|
||||
expect(existsSync(join(nodePtyDir, 'build', 'Release', 'spawn-helper'))).toBe(false)
|
||||
})
|
||||
|
||||
it('will not load a glibc slot on a musl host', () => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { usesNodePtySpawnHelper } from '../../shared/node-pty-spawn-helper'
|
||||
import { nativeSlotName, type NativeHostAbi } from './native-host-abi'
|
||||
|
||||
export type PrebuiltSlotManifest = {
|
||||
@@ -103,11 +104,11 @@ export function installPrebuiltSlot(options: {
|
||||
mkdirSync(releaseDir, { recursive: true })
|
||||
copyFileSync(source, join(releaseDir, 'pty.node'))
|
||||
|
||||
// Why this matters as much as pty.node: on Unix node-pty posix_spawns
|
||||
// Why this matters as much as pty.node: on macOS node-pty posix_spawns
|
||||
// build/Release/spawn-helper. Without it every spawn fails with ENOENT at the moment
|
||||
// a user opens a terminal, long after the "install succeeded" line.
|
||||
let spawnHelper = false
|
||||
if (options.abi.platform !== 'win32') {
|
||||
if (usesNodePtySpawnHelper(options.abi.platform)) {
|
||||
const helperSource = join(prebuildsDir, slot, 'spawn-helper')
|
||||
if (existsSync(helperSource)) {
|
||||
const helperDest = join(releaseDir, 'spawn-helper')
|
||||
|
||||
@@ -31,10 +31,10 @@ const realNodePtyLoads = ((): boolean => {
|
||||
if (!existsSync(REAL_PTY_NODE)) {
|
||||
return false
|
||||
}
|
||||
// Why spawn-helper too: a slot without it is legitimately 'degraded', so a test that
|
||||
// expects 'ok' has an unsatisfiable premise on a host that lacks it. CI has the
|
||||
// binding but not the helper, which is what made the previous gate insufficient.
|
||||
if (process.platform !== 'win32' && !existsSync(REAL_SPAWN_HELPER)) {
|
||||
// Why spawn-helper too: on macOS a slot without it is legitimately 'degraded', so a
|
||||
// test that expects 'ok' has an unsatisfiable premise on a host that lacks it. Only
|
||||
// macOS builds the helper, so gating other platforms on it never lets them run.
|
||||
if (process.platform === 'darwin' && !existsSync(REAL_SPAWN_HELPER)) {
|
||||
return false
|
||||
}
|
||||
const probe = spawnSync(process.execPath, ['-e', `require(${JSON.stringify(REAL_PTY_NODE)})`], {
|
||||
@@ -240,8 +240,8 @@ describe('checkNodePtyPrecondition', () => {
|
||||
|
||||
// Why gated on the real binding: this asserts a LOAD outcome, so it needs a pty.node
|
||||
// built for the Node ABI. CI's shard never runs ensure-native-runtime, so the copy
|
||||
// ENOENT'd there.
|
||||
it.runIf(process.platform !== 'win32' && realNodePtyLoads)(
|
||||
// ENOENT'd there. macOS only — it is the only platform that execs spawn-helper.
|
||||
it.runIf(process.platform === 'darwin' && realNodePtyLoads)(
|
||||
'degrades rather than blocks when only spawn-helper is missing',
|
||||
() => {
|
||||
// node-pty posix_spawns spawn-helper, so this host loads fine and then fails ENOENT
|
||||
@@ -258,6 +258,31 @@ describe('checkNodePtyPrecondition', () => {
|
||||
}
|
||||
)
|
||||
|
||||
// Same staging as above, read through a Linux ABI: node-pty builds spawn-helper only
|
||||
// under binding.gyp's OS=="mac", so demanding one here called every healthy Linux
|
||||
// orcad degraded while its terminals worked (#17844). Gated on a loadable binding for
|
||||
// the same reason as the macOS case; the ABI is what makes it a Linux verdict.
|
||||
it.runIf(process.platform !== 'win32' && realNodePtyLoads)(
|
||||
'does not call a Linux host degraded over a spawn-helper it never execs',
|
||||
() => {
|
||||
const dir = stageNodePty()
|
||||
cpSync(
|
||||
join(REAL_NODE_PTY, 'build', 'Release', 'pty.node'),
|
||||
join(dir, 'build', 'Release', 'pty.node')
|
||||
)
|
||||
expect(existsSync(join(dir, 'build', 'Release', 'spawn-helper'))).toBe(false)
|
||||
|
||||
const verdict = checkNodePtyPrecondition({
|
||||
nodePtyDir: dir,
|
||||
prebuildsDir: null,
|
||||
abi: { platform: 'linux', arch: 'x64', libc: 'glibc', glibcVersion: '2.31', nodeAbi: '127' }
|
||||
})
|
||||
|
||||
expect(verdict).toMatchObject({ status: 'ok', slot: 'linux-x64-glibc' })
|
||||
expect(verdict.reason).toBeUndefined()
|
||||
}
|
||||
)
|
||||
|
||||
// Why split: the "ok" half needs a REAL loadable pty.node, which only exists after
|
||||
// `ensure-native-runtime --runtime=node`. CI's shard runs vitest directly, so copying
|
||||
// from node_modules ENOENT'd there. Slot *placement* is the logic worth checking on
|
||||
|
||||
@@ -19,6 +19,7 @@ import { existsSync, accessSync, constants } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { runProcessSync, type ProcessResult } from '../../shared/child-process/run-process'
|
||||
import { usesNodePtySpawnHelper } from '../../shared/node-pty-spawn-helper'
|
||||
import type { RuntimeTerminalUnavailableReason } from '../../shared/runtime-types'
|
||||
import {
|
||||
buildToolchainProbeCommand,
|
||||
@@ -302,12 +303,12 @@ export function checkNodePtyPrecondition(
|
||||
}
|
||||
}
|
||||
|
||||
// Loaded. The remaining way terminals fail is spawn-time: node-pty posix_spawns
|
||||
// Loaded. The remaining way terminals fail is spawn-time: on macOS node-pty posix_spawns
|
||||
// build/Release/spawn-helper, and a missing one turns every terminal.create into ENOENT
|
||||
// on a host that otherwise looks healthy. That is a degradation, not a boot blocker.
|
||||
const outcome = readNodePtyProbeOutcome(result)
|
||||
const loadedDir = outcome.kind === 'loaded' ? outcome.loadedDir : null
|
||||
if (abi.platform !== 'win32') {
|
||||
if (usesNodePtySpawnHelper(abi.platform)) {
|
||||
const helper = join(loadedDir || join(nodePtyDir, 'build', 'Release'), 'spawn-helper')
|
||||
if (!isExecutableFile(helper)) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { basename, isAbsolute, join } from 'node:path'
|
||||
import { existsSync, accessSync, statSync, chmodSync, constants as fsConstants } from 'node:fs'
|
||||
import type * as pty from 'node-pty'
|
||||
import { usesNodePtySpawnHelper } from '../../shared/node-pty-spawn-helper'
|
||||
import {
|
||||
hostReportsChildExitStatus,
|
||||
wrapShellSpawnForMacosTccAttribution
|
||||
@@ -82,9 +83,10 @@ export function resolveUnixShellPath(shellPath: string): string {
|
||||
* Why: when Electron packages the app via asar, the native spawn-helper
|
||||
* binary may lose its +x permission. This function detects and repairs
|
||||
* that so pty.spawn() does not fail with EACCES on first launch.
|
||||
* macOS only — no other platform builds or execs the helper.
|
||||
*/
|
||||
export function ensureNodePtySpawnHelperExecutable(): void {
|
||||
if (didEnsureSpawnHelperExecutable || process.platform === 'win32') {
|
||||
if (didEnsureSpawnHelperExecutable || !usesNodePtySpawnHelper(process.platform)) {
|
||||
return
|
||||
}
|
||||
didEnsureSpawnHelperExecutable = true
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { usesNodePtySpawnHelper } from './node-pty-spawn-helper'
|
||||
|
||||
describe('usesNodePtySpawnHelper', () => {
|
||||
it('is macOS only', () => {
|
||||
// The predicate this file exists for: node-pty's binding.gyp declares the
|
||||
// spawn-helper target inside OS=="mac". Reading it as "every non-Windows platform"
|
||||
// is what reported spawn_helper_missing on healthy Linux hosts (#17844).
|
||||
expect(usesNodePtySpawnHelper('darwin')).toBe(true)
|
||||
for (const platform of ['linux', 'win32', 'freebsd', 'openbsd', 'sunos', 'aix']) {
|
||||
expect(usesNodePtySpawnHelper(platform)).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Whether node-pty execs its `spawn-helper` binary on a platform.
|
||||
*
|
||||
* Only macOS: binding.gyp declares the `spawn-helper` target inside `OS=="mac"`, and
|
||||
* `src/unix/pty.cc` reads the helper path only under `#if defined(__APPLE__)`. Every
|
||||
* other platform forks directly, so requiring the helper there calls a working host
|
||||
* broken.
|
||||
*/
|
||||
export function usesNodePtySpawnHelper(platform: string): boolean {
|
||||
return platform === 'darwin'
|
||||
}
|
||||
Reference in New Issue
Block a user