mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
From 40.10.3 on, every Electron line unpacks its binary with @electron-internal/extract-zip, an MSVC-built binding that imports VCRUNTIME140.dll. A Windows host that never installed the VC++ Redistributable has no copy, so install.js dies with ERR_DLOPEN_FAILED. This is the failure that madebb8280b753roll Electron back to 40.10.2. Our Python ships vcruntime140.dll. source_build_env() passes its path to the Node builders as HERMES_VC_RUNTIME, and electron-runtime.mjs copies it beside the extractor's binding right before it runs install.js. Node loads bindings with LOAD_WITH_ALTERED_SEARCH_PATH (libuv uv_dlopen), so Windows finds that copy first. This is the same app-local copyfdea3e92e0ships beside ARM64 rg.exe. Clean Windows Sandbox (ARM64, no redist), Node 26.7.0, electron 44.9.3: - before: install.js exits 1, "Cannot find native binding", ERR_DLOPEN_FAILED - after: install.js exits 0, dist/electron.exe --version prints v44.9.3
66 lines
4.1 KiB
JavaScript
66 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from 'node:child_process'
|
|
import { copyFileSync, existsSync, readFileSync, statSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { parseArgs } from 'node:util'
|
|
|
|
// Do not require('electron'): Electron 42's entry point can acquire a binary.
|
|
// Only this explicit preparation phase may execute the installed installer.
|
|
/** @param {{ source: string, install?: boolean, env?: NodeJS.ProcessEnv }} options @returns {string} */
|
|
export function prepareElectronRuntime({ source, install = true, env = process.env }) {
|
|
source = resolve(source)
|
|
const manifest = [join(source, 'apps/desktop/node_modules/electron/package.json'),
|
|
join(source, 'node_modules/electron/package.json')].find(existsSync)
|
|
if (!manifest) throw new Error('Missing prepared Electron dependency; install the locked desktop workspace first')
|
|
const packageRoot = dirname(manifest)
|
|
const { version, dependencies } = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
const desktop = JSON.parse(readFileSync(join(source, 'apps/desktop/package.json'), 'utf8'))
|
|
const lock = JSON.parse(readFileSync(join(source, 'package-lock.json'), 'utf8'))
|
|
const entry = lock.packages?.[relative(source, packageRoot).split('\\').join('/')]
|
|
const lockedVersion = entry?.link ? lock.packages[entry.resolved]?.version : entry?.version
|
|
if (version !== desktop.devDependencies?.electron || version !== lockedVersion) {
|
|
throw new Error('Installed Electron is not the desktop lock-pinned version')
|
|
}
|
|
const binaryName = process.platform === 'darwin' ? 'Electron.app/Contents/MacOS/Electron'
|
|
: process.platform === 'win32' ? 'electron.exe' : 'electron'
|
|
const executable = join(packageRoot, 'dist', binaryName)
|
|
const prepared = () => {
|
|
try {
|
|
return readFileSync(join(packageRoot, 'path.txt'), 'utf8').trim() === binaryName
|
|
&& readFileSync(join(packageRoot, 'dist/version'), 'utf8').trim().replace(/^v/, '') === version
|
|
&& statSync(executable).isFile()
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
if (prepared()) return executable
|
|
if (!install) throw new Error('Electron binary is missing or stale and lazy installs are disabled; run an explicit desktop build/update')
|
|
const excluded = new Set(['electron_skip_binary_download', 'electron_override_dist_path',
|
|
'electron_install_arch', 'electron_install_platform', 'npm_config_arch', 'npm_config_platform'])
|
|
const installerEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !excluded.has(key.toLowerCase())))
|
|
Object.assign(installerEnv, { ELECTRON_INSTALL_PLATFORM: process.platform, ELECTRON_INSTALL_ARCH: process.arch,
|
|
npm_config_platform: process.platform, npm_config_arch: process.arch })
|
|
// install.js unpacks with @electron-internal/extract-zip, whose Windows binding imports
|
|
// VCRUNTIME140.dll. A fresh Windows has none, so the load fails (ERR_DLOPEN_FAILED).
|
|
// Node loads bindings with LOAD_WITH_ALTERED_SEARCH_PATH, so a copy beside the binding
|
|
// is found first. HERMES_VC_RUNTIME is the copy our Python ships.
|
|
if (process.platform === 'win32' && env.HERMES_VC_RUNTIME && dependencies?.['@electron-internal/extract-zip']) {
|
|
const binding = dirname(createRequire(manifest).resolve('@electron-internal/extract-zip'))
|
|
copyFileSync(env.HERMES_VC_RUNTIME, join(binding, 'vcruntime140.dll'))
|
|
}
|
|
console.log(`electron-runtime: preparing locked Electron ${version} (${process.platform}-${process.arch})...`)
|
|
execFileSync(process.execPath, [join(packageRoot, 'install.js')], { cwd: source, env: installerEnv, stdio: 'inherit' })
|
|
if (!prepared()) throw new Error('Electron installer did not produce the prepared host runtime')
|
|
return executable
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
const { values } = parseArgs({ options: {
|
|
source: { type: 'string' }, 'no-install': { type: 'boolean', default: false },
|
|
} })
|
|
if (!values.source) throw new Error('--source is required')
|
|
prepareElectronRuntime({ source: values.source, install: !values['no-install'] })
|
|
}
|