Files
hermes-agent/scripts/build/electron-runtime.mjs
T
ethernet 8c3734f1d7 fix(desktop): fresh Windows hosts can unpack Electron 40.10.3+ without the VC++ redist
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 made bb8280b753 roll 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 copy
fdea3e92e0 ships 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
2026-10-22 16:17:57 -04:00

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'] })
}