Files
Orkas/scripts/verify-native-dependencies.cjs
OrkasandCloudTianTian 33bc31194b Fix native dependency repair and isolate sharp in bundled Node (#73)
* fix: verify native dependencies in Electron and support repair

* ci: capture native child diagnostics on repair failures

* fix: preserve known Linux sharp warnings and stable Windows ESM tests

* Isolate application sharp operations in bundled Node

* Test isolated SVG text rendering without native diagnostics

* Keep CI system fonts read-only and reject GUI GLib collisions

---------

Co-authored-by: CloudTianTian <[email protected]>
2026-09-12 12:47:27 +08:00

114 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { createRequire } = require('node:module');
const { spawnSync } = require('node:child_process');
const { probeSqlite } = require('./native-dependency-probe.cjs');
const MINIMUM_NODE_VERSION = '22.12.0';
const PROBE_TIMEOUT_MS = 60_000;
const root = path.resolve(__dirname, '..');
function verifySharpRuntime({ packageRoot = root, spawn = spawnSync, env = process.env } = {}) {
let nodeExecutable;
let nodeVersion;
try {
const { verifyRuntimeDir } = require('../bin/runtime-gate.cjs');
const runtimeRoot = path.join(packageRoot, 'resources', 'runtime');
const manifest = JSON.parse(fs.readFileSync(path.join(runtimeRoot, 'manifest.json'), 'utf8'));
const key = `${process.platform}-${process.arch}`;
nodeVersion = manifest.node.version;
nodeExecutable = verifyRuntimeDir('node', path.join(runtimeRoot, 'node', key), key,
manifest.node, manifest.node.assets[key], process.platform, process.arch);
} catch {
throw new Error('The bundled Node image runtime is missing or invalid; run npm run native:repair.');
}
const { probeImageRuntime } = require('../bin/sharp-worker.cjs');
const record = probeImageRuntime(nodeExecutable, path.join(packageRoot, 'bin', 'sharp-worker.cjs'), spawn, env);
if (record.node !== nodeVersion || record.platform !== process.platform || record.arch !== process.arch) {
throw new Error('sharp failed verification in the bundled Node image runtime; run npm run native:repair.');
}
return record;
}
// Built-ins only: this preflight also runs before node_modules exists.
function assertBootstrapNode(version = process.versions.node) {
const parts = String(version).split('.');
const valid = parts.length === 3 && parts.every(part => /^\d+$/.test(part));
if (!valid || Number(parts[0]) < 22 || (Number(parts[0]) === 22 && Number(parts[1]) < 12)) {
throw new Error(`Node.js ${MINIMUM_NODE_VERSION}+ is required to prepare native dependencies; use Node.js 24 LTS and retry.`);
}
}
function verifyNativeDependencies({ packageRoot = root, spawn = spawnSync, env = process.env } = {}) {
assertBootstrapNode();
const requirePackage = createRequire(path.join(packageRoot, 'package.json'));
let lock;
try { lock = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package-lock.json'), 'utf8')); }
catch { throw new Error('Cannot read package-lock.json; restore the checkout lockfile, then run npm run native:repair.'); }
for (const name of ['electron', 'better-sqlite3', 'sqlite-vec', 'sharp']) {
const expected = lock.packages?.[`node_modules/${name}`]?.version;
let installed;
try { installed = JSON.parse(fs.readFileSync(path.join(packageRoot, 'node_modules', name, 'package.json'), 'utf8')).version; }
catch { /* handled as an incomplete installation below */ }
if (!expected || installed !== expected) {
throw new Error(`${name} does not match package-lock.json; run npm run native:repair in this checkout.`);
}
}
const electronVersion = lock.packages['node_modules/electron'].version;
let electronPath;
try { electronPath = requirePackage('electron'); }
catch { throw new Error('Electron is not installed correctly; run npm run native:repair in this checkout.'); }
// Use the target Electron's require, never this system Node's addon loader.
const script = `
const { createRequire } = require('node:module');
const requirePackage = createRequire(${JSON.stringify(path.join(packageRoot, 'package.json'))});
(async () => {
const sqlite = await (${probeSqlite.toString()})(requirePackage);
console.log(JSON.stringify({status:'passed', ...sqlite,
platform:process.platform, arch:process.arch, electron:process.versions.electron,
node:process.versions.node, modules:process.versions.modules, napi:process.versions.napi}));
})().catch(error => { console.error(error.message); process.exitCode = 1; });
`;
const result = spawn(electronPath, ['-e', script], {
cwd: packageRoot, env: { ...env, ELECTRON_RUN_AS_NODE: '1' },
encoding: 'utf8', timeout: PROBE_TIMEOUT_MS, maxBuffer: 1024 * 1024,
windowsHide: true,
});
let record;
try { record = JSON.parse(String(result.stdout || '').trim()); } catch { /* fail closed */ }
if (result.error || result.status !== 0 || String(result.stderr || '').trim()
|| record?.status !== 'passed' || record.electron !== electronVersion
|| record.platform !== process.platform || record.arch !== process.arch
|| typeof record.sqliteVec !== 'string' || !record.sqliteVec
|| record.vectorDistance !== 5) {
// Only relay the bounded messages authored by our probe. Native stderr can
// contain private paths; unknown loader failures keep the recovery context.
const known = ['better-sqlite3', 'sqlite-vec', 'sharp'].find(name =>
String(result.stderr || '').startsWith(`${name} could not complete`));
const reason = result.error?.code === 'ETIMEDOUT' ? 'timed out' : 'failed';
throw new Error(`Native dependency verification ${reason}${known ? ` (${known})` : ''} for Electron ${electronVersion} on ${process.platform}-${process.arch}. Run npm run native:repair; check optional packages, the target architecture and system shared libraries.`);
}
const imageRuntime = verifySharpRuntime({ packageRoot, spawn, env });
return { ...record, sharp: imageRuntime.sharp, imageRuntime };
}
if (require.main === module) {
try {
const args = process.argv.slice(2);
if (args.length > 1 || (args.length === 1 && args[0] !== '--host-only')) throw new Error('Use --host-only or no arguments.');
assertBootstrapNode();
if (process.argv[2] !== '--host-only') {
const result = verifyNativeDependencies();
console.log(JSON.stringify(result));
}
} catch (error) {
console.error(`[native-deps] ${error.message}`);
process.exitCode = 1;
}
}
module.exports = { MINIMUM_NODE_VERSION, PROBE_TIMEOUT_MS, assertBootstrapNode, verifySharpRuntime, verifyNativeDependencies };