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:
parent
4d59f3b9e5
commit
e10d16b3c2
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue