3bed7d4ae7
Two paths let a pre-existing system Node win over the Hermes-managed one. The desktop backend spawn built its managed-Node PATH entry as `<home>/node/bin` only. That is the POSIX layout install.sh produces; install.ps1 unpacks portable Node straight into `%LOCALAPPDATA%\hermes\node` with node.exe at the root and no `bin\`. On Windows the entry therefore pointed at a directory that does not exist, and the backend fell through to whatever Node was already on PATH. main.ts already had the correct platform-ordered list, behind a "keep this in sync with iter_hermes_node_dirs()" comment on a second copy of the rule. The two copies had drifted. Export the ordering from backend-env.ts and have main.ts consume it so there is one source of truth on the Node side (the Electron main process cannot import hermes_constants.py, so a mirror is unavoidable — but one mirror, not two). install.ps1 appended the node dir to the persisted User PATH instead of prepending it. The session PATH was already prepended correctly, so this only bit later processes: any shell opened after install, and a standalone hermes-setup.exe run that inherits User PATH rather than a curated env, both resolved a system Node ahead of the bundled one. Not a bug, for the record: update.rs's prepend list omits the same Windows root, but it inherits PATH from the desktop, which supplies the correct entries — so it is redundant rather than broken, and no installer rebuild is needed for this fix. Tests: managed dirs lead with the platform-native layout while always offering both shapes, empty without a home, and every managed dir outranks the inherited PATH on darwin and win32. The three existing tests that pinned `entries[1]` by index asserted the old single-dir shape and now assert the relationship instead. install.ps1 has no behavioral test here: CI has no PowerShell host, and AGENTS.md bans source-reading tests (the neighbouring test_install_ps1_node_path_for_npm.py predates that rule).
162 lines
4.8 KiB
TypeScript
162 lines
4.8 KiB
TypeScript
import path from 'node:path'
|
|
|
|
// Match the POSIX fallback surface used by the Python terminal environment.
|
|
// macOS apps launched from Finder/Dock often inherit only /usr/bin:/bin:/usr/sbin:/sbin,
|
|
// which misses Apple Silicon Homebrew and user-installed CLI tools such as codex.
|
|
const POSIX_SANE_PATH_ENTRIES = Object.freeze([
|
|
'/opt/homebrew/bin',
|
|
'/opt/homebrew/sbin',
|
|
'/usr/local/sbin',
|
|
'/usr/local/bin',
|
|
'/usr/sbin',
|
|
'/usr/bin',
|
|
'/sbin',
|
|
'/bin'
|
|
])
|
|
|
|
function delimiterForPlatform(platform = process.platform) {
|
|
return platform === 'win32' ? ';' : ':'
|
|
}
|
|
|
|
function pathModuleForPlatform(platform = process.platform) {
|
|
return platform === 'win32' ? path.win32 : path.posix
|
|
}
|
|
|
|
function pathEnvKey(env = process.env, platform = process.platform) {
|
|
if (platform !== 'win32') {
|
|
return 'PATH'
|
|
}
|
|
|
|
return Object.keys(env || {}).find(key => key.toUpperCase() === 'PATH') || 'PATH'
|
|
}
|
|
|
|
function currentPathValue(env = process.env, platform = process.platform) {
|
|
const key = pathEnvKey(env, platform)
|
|
|
|
return env?.[key] || ''
|
|
}
|
|
|
|
function appendUniquePathEntries(entries, { delimiter = path.delimiter } = {}) {
|
|
const seen = new Set()
|
|
const ordered = []
|
|
|
|
for (const entry of entries) {
|
|
if (!entry) {
|
|
continue
|
|
}
|
|
|
|
const parts = Array.isArray(entry) ? entry : String(entry).split(delimiter)
|
|
|
|
for (const part of parts) {
|
|
if (!part || seen.has(part)) {
|
|
continue
|
|
}
|
|
|
|
seen.add(part)
|
|
ordered.push(part)
|
|
}
|
|
}
|
|
|
|
return ordered.join(delimiter)
|
|
}
|
|
|
|
/**
|
|
* Hermes-managed Node.js directories, in preferred lookup order.
|
|
*
|
|
* There are two on-disk layouts. `scripts/install.ps1` unpacks portable Node
|
|
* straight into `%LOCALAPPDATA%\hermes\node` (node.exe at the root, no `bin\`);
|
|
* `scripts/install.sh` and the node-bootstrap helper use the POSIX
|
|
* `$HERMES_HOME/node/bin`. Emit BOTH on every platform so mixed and migrated
|
|
* installs resolve, leading with the layout native to the current platform.
|
|
*
|
|
* This is the single source of truth for the ordering rule on the Node side —
|
|
* `main.ts` imports it rather than keeping its own copy. Mirrors
|
|
* `iter_hermes_node_dirs()` in hermes_constants.py, which the Electron main
|
|
* process cannot import.
|
|
*/
|
|
function hermesManagedNodePathEntries(
|
|
hermesHome,
|
|
{ platform = process.platform, pathModule = pathModuleForPlatform(platform) }: any = {}
|
|
) {
|
|
if (!hermesHome) {
|
|
return []
|
|
}
|
|
|
|
const root = pathModule.join(hermesHome, 'node')
|
|
const bin = pathModule.join(root, 'bin')
|
|
|
|
return platform === 'win32' ? [root, bin] : [bin, root]
|
|
}
|
|
|
|
function buildDesktopBackendPath({
|
|
hermesHome,
|
|
venvRoot,
|
|
currentPath = '',
|
|
platform = process.platform,
|
|
pathModule = pathModuleForPlatform(platform)
|
|
}: any = {}) {
|
|
const delimiter = delimiterForPlatform(platform)
|
|
const hermesNodeDirs = hermesManagedNodePathEntries(hermesHome, { platform, pathModule })
|
|
const venvBin = venvRoot ? pathModule.join(venvRoot, platform === 'win32' ? 'Scripts' : 'bin') : null
|
|
const saneEntries = platform === 'win32' ? [] : POSIX_SANE_PATH_ENTRIES
|
|
|
|
return appendUniquePathEntries([hermesNodeDirs, venvBin, currentPath, saneEntries], { delimiter })
|
|
}
|
|
|
|
function normalizeHermesHomeRoot(hermesHome, { pathModule = pathModuleForPlatform(process.platform) }: any = {}) {
|
|
if (!hermesHome) {
|
|
return hermesHome
|
|
}
|
|
|
|
const resolved = pathModule.resolve(String(hermesHome))
|
|
const parent = pathModule.dirname(resolved)
|
|
|
|
if (pathModule.basename(parent).toLowerCase() === 'profiles') {
|
|
return pathModule.dirname(parent)
|
|
}
|
|
|
|
return resolved
|
|
}
|
|
|
|
function buildDesktopBackendEnv({
|
|
hermesHome,
|
|
pythonPathEntries = [],
|
|
venvRoot,
|
|
currentEnv = process.env,
|
|
platform = process.platform,
|
|
pathModule = pathModuleForPlatform(platform)
|
|
}: any = {}) {
|
|
const delimiter = delimiterForPlatform(platform)
|
|
const currentPythonPath = currentEnv?.PYTHONPATH || ''
|
|
const key = pathEnvKey(currentEnv, platform)
|
|
|
|
return {
|
|
PYTHONPATH: appendUniquePathEntries([...pythonPathEntries, currentPythonPath], { delimiter }),
|
|
// Force PEP 540 UTF-8 mode in the spawned Python backend so its stdio and
|
|
// subprocess defaults are UTF-8 even on non-UTF-8 Windows locales (GBK,
|
|
// cp1252, ...). hermes_bootstrap sets this inside the child too, but only
|
|
// after import — anything emitted earlier (interpreter startup errors,
|
|
// pre-bootstrap tracebacks) still decodes with the locale default without
|
|
// this. User's explicit setting wins. Re-port of PR #56499 (echoriver89).
|
|
PYTHONUTF8: currentEnv?.PYTHONUTF8 ?? '1',
|
|
[key]: buildDesktopBackendPath({
|
|
hermesHome,
|
|
venvRoot,
|
|
currentPath: currentPathValue(currentEnv, platform),
|
|
platform,
|
|
pathModule
|
|
})
|
|
}
|
|
}
|
|
|
|
export {
|
|
appendUniquePathEntries,
|
|
buildDesktopBackendEnv,
|
|
buildDesktopBackendPath,
|
|
delimiterForPlatform,
|
|
hermesManagedNodePathEntries,
|
|
normalizeHermesHomeRoot,
|
|
pathEnvKey,
|
|
POSIX_SANE_PATH_ENTRIES
|
|
}
|