orca/config/scripts/ensure-native-runtime.mjs

250 lines
6.4 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from 'node:child_process'
import { createRequire } from 'node:module'
import { release } from 'node:os'
import { basename, dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const require = createRequire(import.meta.url)
const scriptPath = fileURLToPath(import.meta.url)
const projectDir = resolve(dirname(scriptPath), '../..')
const runtime = readRuntimeArg()
const NATIVE_MODULES = ['better-sqlite3', 'node-pty']
const CHILD_CHECK_FLAG = '--check-only'
if (process.argv.includes(CHILD_CHECK_FLAG)) {
const failures = collectNativeModuleFailures()
if (failures.length > 0) {
for (const failure of failures) {
console.error(`${failure.moduleName}: ${failure.message}`)
}
process.exit(1)
}
process.exit(0)
}
if (runtime === 'node') {
ensureNodeRuntime()
} else if (runtime === 'electron') {
ensureElectronRuntime()
} else {
console.error('Usage: node config/scripts/ensure-native-runtime.mjs --runtime=node|electron')
process.exit(2)
}
function readRuntimeArg() {
const inline = process.argv.find((arg) => arg.startsWith('--runtime='))
if (inline) {
return inline.slice('--runtime='.length)
}
const runtimeIndex = process.argv.indexOf('--runtime')
if (runtimeIndex >= 0) {
return process.argv[runtimeIndex + 1]
}
return null
}
function ensureNodeRuntime() {
const initial = runCurrentProcessCheck()
if (initial.ok) {
return
}
const failedModules = initial.failures.map((failure) => failure.moduleName)
console.warn(
`[native-runtime] ${formatRuntimeLabel('node')} cannot load native modules; rebuilding ${failedModules.join(', ')} for Node.`
)
printCheckError(initial)
runPnpm(['rebuild', ...failedModules])
const final = runCurrentProcessCheck()
if (!final.ok) {
console.error(
`[native-runtime] Native modules still do not load for ${formatRuntimeLabel('node')}.`
)
printCheckError(final)
process.exit(1)
}
}
function ensureElectronRuntime() {
const initial = runElectronCheck()
if (initial.ok) {
return
}
console.warn(
`[native-runtime] ${formatRuntimeLabel('electron')} cannot load native modules; rebuilding native deps for Electron.`
)
printCheckError(initial)
runNodeScript(['config/scripts/rebuild-native-deps.mjs'])
const final = runElectronCheck()
if (!final.ok) {
console.error(
`[native-runtime] Native modules still do not load for ${formatRuntimeLabel('electron')}.`
)
printCheckError(final)
process.exit(1)
}
}
function runCurrentProcessCheck() {
const failures = collectNativeModuleFailures()
if (failures.length === 0) {
return { ok: true, failures }
}
return { ok: false, failures }
}
function runElectronCheck() {
let electronExecutable
try {
electronExecutable = require('electron')
} catch (error) {
return { ok: false, error }
}
const result = spawnSync(electronExecutable, [scriptPath, CHILD_CHECK_FLAG], {
cwd: projectDir,
env: {
...process.env,
ELECTRON_RUN_AS_NODE: '1'
},
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
})
return {
ok: result.status === 0,
status: result.status,
stdout: result.stdout,
stderr: result.stderr,
error: result.error
}
}
function collectNativeModuleFailures() {
const failures = []
for (const moduleName of NATIVE_MODULES) {
try {
loadNativeModule(moduleName)
} catch (cause) {
failures.push({ moduleName, message: formatError(cause), cause })
}
}
return failures
}
function loadNativeModule(moduleName) {
if (moduleName === 'better-sqlite3') {
const Database = require(moduleName)
// Why: better-sqlite3 defers loading its .node binding until Database is
// constructed, so a plain require() misses Node ABI mismatches.
const db = new Database(':memory:')
db.close()
return
}
if (moduleName === 'node-pty') {
loadNodePtyNativeModule()
return
}
require(moduleName)
}
function loadNodePtyNativeModule() {
require('node-pty')
const { loadNativeModule } = require('node-pty/lib/utils')
const nativeName = getNodePtyNativeModuleName()
// Why: node-pty's Windows JS wrapper defers conpty.node/pty.node until a
// terminal is created, so require('node-pty') alone can miss ABI mismatches.
loadNativeModule(nativeName)
}
function getNodePtyNativeModuleName() {
if (process.platform !== 'win32') {
return 'pty'
}
return getWindowsBuildNumber() >= 18309 ? 'conpty' : 'pty'
}
function getWindowsBuildNumber() {
const match = /(\d+)\.(\d+)\.(\d+)/g.exec(release())
return match && match.length === 4 ? Number.parseInt(match[3], 10) : 0
}
function runPnpm(args) {
const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
const result = spawnSync(command, args, {
cwd: projectDir,
stdio: 'inherit',
shell: false
})
if (result.error || result.status !== 0) {
console.error(`[native-runtime] ${command} ${args.join(' ')} failed.`)
if (result.error) {
console.error(formatError(result.error))
}
process.exit(result.status ?? 1)
}
}
function runNodeScript(args) {
const result = spawnSync(process.execPath, args, {
cwd: projectDir,
stdio: 'inherit'
})
if (result.error || result.status !== 0) {
console.error(`[native-runtime] ${basename(process.execPath)} ${args.join(' ')} failed.`)
if (result.error) {
console.error(formatError(result.error))
}
process.exit(result.status ?? 1)
}
}
function printCheckError(result) {
for (const failure of result.failures ?? []) {
console.warn(`[native-runtime] ${failure.moduleName}: ${failure.message}`)
}
if (result.error) {
console.warn(`[native-runtime] ${formatError(result.error)}`)
}
if (result.stderr?.trim()) {
console.warn(result.stderr.trim())
}
if (result.stdout?.trim()) {
console.warn(result.stdout.trim())
}
if (
result.status != null &&
!result.error &&
!result.stderr?.trim() &&
!result.stdout?.trim() &&
result.status !== 0
) {
console.warn(`[native-runtime] Native check exited with status ${result.status}.`)
}
}
function formatError(error) {
return error instanceof Error ? error.message : String(error)
}
function formatRuntimeLabel(value) {
if (value === 'electron') {
return `Electron ${process.env.npm_package_devDependencies_electron ?? ''}`.trim()
}
return `Node ${process.versions.node}`
}