fix: Node native runtime rebuild verification (#2845)

* Fix Node native runtime rebuild verification

* Add native runtime rebuild regression test

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
Xipeng Jin 2026-05-26 22:15:42 -04:00 committed by GitHub
parent 4d59f3b9e5
commit e10d16b3c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 166 additions and 9 deletions

View File

@ -49,7 +49,7 @@ function readRuntimeArg() {
}
function ensureNodeRuntime() {
const initial = runCurrentProcessCheck()
const initial = runNodeCheck()
if (initial.ok) {
return
}
@ -61,7 +61,7 @@ function ensureNodeRuntime() {
printCheckError(initial)
runPnpm(['rebuild', ...failedModules])
const final = runCurrentProcessCheck()
const final = runNodeCheck()
if (!final.ok) {
console.error(
`[native-runtime] Native modules still do not load for ${formatRuntimeLabel('node')}.`
@ -93,12 +93,16 @@ function ensureElectronRuntime() {
}
}
function runCurrentProcessCheck() {
const failures = collectNativeModuleFailures()
if (failures.length === 0) {
return { ok: true, failures }
}
return { ok: false, failures }
function runNodeCheck() {
// Why: a failed native addon load can poison the current process, so the
// post-rebuild verification must happen in a fresh Node process.
const result = spawnSync(process.execPath, [scriptPath, CHILD_CHECK_FLAG], {
cwd: projectDir,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
})
return parseChildCheckResult(result)
}
function runElectronCheck() {
@ -119,15 +123,33 @@ function runElectronCheck() {
stdio: ['ignore', 'pipe', 'pipe']
})
return parseChildCheckResult(result)
}
function parseChildCheckResult(result) {
const failures = parseCheckFailures(result.stderr)
return {
ok: result.status === 0,
status: result.status,
stdout: result.stdout,
stderr: result.stderr,
error: result.error
error: result.error,
failures
}
}
function parseCheckFailures(stderr) {
const failures = []
for (const line of (stderr ?? '').split(/\r?\n/)) {
const match = /^([^:]+):\s*(.*)$/.exec(line)
if (match && NATIVE_MODULES.includes(match[1])) {
failures.push({ moduleName: match[1], message: match[2] })
}
}
return failures
}
function collectNativeModuleFailures() {
const failures = []
for (const moduleName of NATIVE_MODULES) {

View File

@ -0,0 +1,135 @@
import { spawnSync } from 'node:child_process'
import {
chmodSync,
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
const sourceScriptPath = fileURLToPath(new URL('./ensure-native-runtime.mjs', import.meta.url))
describe('ensure-native-runtime', () => {
it('rechecks Node native modules in fresh child processes after rebuilding', () => {
const projectDir = mkTempProject()
try {
const scriptPath = join(projectDir, 'config', 'scripts', 'ensure-native-runtime.mjs')
const logPath = join(projectDir, 'native-runtime.log')
const markerPath = join(projectDir, 'rebuilt.marker')
const binDir = join(projectDir, 'bin')
copyFileSync(sourceScriptPath, scriptPath)
writeFakeNativeModules(projectDir)
writeFakePnpm(binDir)
const result = spawnSync(process.execPath, [scriptPath, '--runtime=node'], {
cwd: projectDir,
encoding: 'utf8',
env: envWithPrependedPath(binDir, {
ORCA_NATIVE_TEST_LOG: logPath,
ORCA_NATIVE_TEST_MARKER: markerPath
})
})
expect(result.status, result.stderr).toBe(0)
const log = readFileSync(logPath, 'utf8')
expect(log).toContain('pnpm rebuild better-sqlite3\n')
expect(log.split('\n').filter((line) => line.startsWith('better-sqlite3 '))).toEqual([
'better-sqlite3 child marker=false',
'better-sqlite3 child marker=true'
])
} finally {
rmSync(projectDir, { recursive: true, force: true })
}
})
})
function mkTempProject() {
const projectDir = mkdtempSync(join(tmpdir(), 'orca-native-runtime-'))
mkdirSync(join(projectDir, 'config', 'scripts'), { recursive: true })
return projectDir
}
function envWithPrependedPath(binDir, extraEnv) {
const pathKey =
process.platform === 'win32'
? (Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ?? 'Path')
: 'PATH'
return {
...process.env,
...extraEnv,
[pathKey]: `${binDir}${delimiter}${process.env[pathKey] ?? ''}`
}
}
function writeFakeNativeModules(projectDir) {
const sqliteDir = join(projectDir, 'node_modules', 'better-sqlite3')
const nodePtyDir = join(projectDir, 'node_modules', 'node-pty')
mkdirSync(sqliteDir, { recursive: true })
mkdirSync(join(nodePtyDir, 'lib'), { recursive: true })
writeFileSync(
join(sqliteDir, 'index.js'),
`
const { appendFileSync, existsSync } = require('node:fs')
module.exports = class Database {
constructor() {
const markerExists = existsSync(process.env.ORCA_NATIVE_TEST_MARKER)
appendFileSync(
process.env.ORCA_NATIVE_TEST_LOG,
\`better-sqlite3 \${process.argv.includes('--check-only') ? 'child' : 'parent'} marker=\${markerExists}\\n\`
)
if (!markerExists) {
throw new Error('ABI mismatch sentinel')
}
}
close() {}
}
`
)
writeFileSync(join(nodePtyDir, 'index.js'), 'module.exports = {}\n')
writeFileSync(
join(nodePtyDir, 'lib', 'utils.js'),
`
const { appendFileSync } = require('node:fs')
exports.loadNativeModule = function loadNativeModule(nativeName) {
appendFileSync(
process.env.ORCA_NATIVE_TEST_LOG,
\`node-pty \${process.argv.includes('--check-only') ? 'child' : 'parent'} \${nativeName}\\n\`
)
}
`
)
}
function writeFakePnpm(binDir) {
mkdirSync(binDir, { recursive: true })
const shimPath = join(binDir, 'pnpm-shim.cjs')
writeFileSync(
shimPath,
`
const { appendFileSync, writeFileSync } = require('node:fs')
appendFileSync(process.env.ORCA_NATIVE_TEST_LOG, \`pnpm \${process.argv.slice(2).join(' ')}\\n\`)
writeFileSync(process.env.ORCA_NATIVE_TEST_MARKER, 'rebuilt')
`
)
const posixPnpmPath = join(binDir, 'pnpm')
writeFileSync(posixPnpmPath, `#!/usr/bin/env node\nrequire(${JSON.stringify(shimPath)})\n`)
chmodSync(posixPnpmPath, 0o755)
writeFileSync(
join(binDir, 'pnpm.cmd'),
`@echo off\r\n"${process.execPath}" "%~dp0\\pnpm-shim.cjs" %*\r\n`
)
}