fix(desktop): repair missing Windows runtime

This commit is contained in:
Gille
2026-08-17 21:43:52 -06:00
parent f30ea0eb9a
commit 48c1eefa26
3 changed files with 68 additions and 39 deletions
+14 -10
View File
@@ -3707,16 +3707,20 @@ async function handOffWindowsBootstrapRecovery(reason) {
const venvHermes = path.join(venvBin, IS_WINDOWS ? 'hermes.exe' : 'hermes')
const venvPython = path.join(venvBin, IS_WINDOWS ? 'python.exe' : 'python')
// Choose the gentle in-place --update when ANY real-install signal is present,
// not just the `hermes.exe` console-script shim. That shim is generated at the
// END of venv setup and is absent in exactly the interrupted/quarantined states
// this recovery exists to heal — gating on it alone forced the destructive
// --repair (full venv recreate) and drove reinstall loops. The venv interpreter
// and the bootstrap-complete marker are present earlier and are better signals.
const haveRealInstall =
fileExists(venvPython) || fileExists(venvHermes) || fileExists(path.join(updateRoot, '.hermes-bootstrap-complete'))
const updaterArgs = chooseUpdaterArgs(haveRealInstall, branch)
// The updater invokes the venv's Hermes launcher, which in turn requires the
// venv interpreter. A bootstrap-complete marker proves only that setup once
// finished; it can outlive a manually removed or quarantined venv. Sending a
// marker-only install through --update dead-ends at "Could not find the hermes
// CLI" instead of rebuilding the runtime, so only a runnable pair gets the
// gentle update path. Partial or missing runtimes go through full repair.
const updaterArgs = chooseUpdaterArgs(
{
hasBootstrapMarker: fileExists(path.join(updateRoot, '.hermes-bootstrap-complete')),
hasVenvHermes: fileExists(venvHermes),
hasVenvPython: fileExists(venvPython)
},
branch
)
await releaseBackendLockForUpdate(updateRoot)
@@ -5,9 +5,9 @@
// 1. buildPathExtCandidates() — PATHEXT extensions must be tried BEFORE the
// empty extension, or an extensionless Git-Bash `hermes` shim shadows
// the real hermes.cmd/hermes.exe.
// 2. chooseUpdaterArgs() — must gate on haveRealInstall (any real-install
// signal), not just the hermes.exe console-script shim, or healthy
// installs get forced into a destructive --repair.
// 2. chooseUpdaterArgs() — must distinguish a runnable updater from stale
// install provenance. The bootstrap marker can outlive the venv, and a
// partial venv cannot run the updater; those states require --repair.
// 3. resolveVenvHermesCommand() — must probe the venv python via
// canImportHermesCli() before trusting it, or a broken venv gets
// re-selected forever instead of falling through to bootstrap.
@@ -45,17 +45,40 @@ test('buildPathExtCandidates: non-Windows only tries the bare name', () => {
assert.deepEqual(buildPathExtCandidates(undefined, false), [''])
})
test('chooseUpdaterArgs: gentle --update when a real-install signal is present', () => {
assert.deepEqual(chooseUpdaterArgs(true, 'main'), ['--update', '--branch', 'main'])
test('chooseUpdaterArgs: gentle --update when both updater runtime files exist', () => {
assert.deepEqual(
chooseUpdaterArgs({ hasBootstrapMarker: true, hasVenvHermes: true, hasVenvPython: true }, 'main'),
['--update', '--branch', 'main']
)
})
test('chooseUpdaterArgs: destructive --repair only when NO real-install signal is present', () => {
assert.deepEqual(chooseUpdaterArgs(false, 'main'), ['--repair', '--branch', 'main'])
test('chooseUpdaterArgs: marker-only install uses --repair when the venv is gone', () => {
assert.deepEqual(
chooseUpdaterArgs({ hasBootstrapMarker: true, hasVenvHermes: false, hasVenvPython: false }, 'main'),
['--repair', '--branch', 'main']
)
})
test('chooseUpdaterArgs: passes the branch through unchanged in both cases', () => {
assert.deepEqual(chooseUpdaterArgs(true, 'release/1.2'), ['--update', '--branch', 'release/1.2'])
assert.deepEqual(chooseUpdaterArgs(false, 'release/1.2'), ['--repair', '--branch', 'release/1.2'])
test('chooseUpdaterArgs: partial updater runtimes use --repair', () => {
assert.deepEqual(
chooseUpdaterArgs({ hasBootstrapMarker: true, hasVenvHermes: false, hasVenvPython: true }, 'main'),
['--repair', '--branch', 'main']
)
assert.deepEqual(
chooseUpdaterArgs({ hasBootstrapMarker: true, hasVenvHermes: true, hasVenvPython: false }, 'main'),
['--repair', '--branch', 'main']
)
})
test('chooseUpdaterArgs: passes the branch through unchanged in both modes', () => {
assert.deepEqual(
chooseUpdaterArgs({ hasBootstrapMarker: false, hasVenvHermes: true, hasVenvPython: true }, 'release/1.2'),
['--update', '--branch', 'release/1.2']
)
assert.deepEqual(
chooseUpdaterArgs({ hasBootstrapMarker: false, hasVenvHermes: false, hasVenvPython: false }, 'release/1.2'),
['--repair', '--branch', 'release/1.2']
)
})
function makeDeps(overrides: Partial<Parameters<typeof resolveVenvHermesCommand>[2]> = {}) {
+21 -19
View File
@@ -11,12 +11,11 @@
* hermes.cmd/hermes.exe; the shim then failed the --version probe and
* the desktop fell through to a spurious bootstrap/repair. The fix:
* PATHEXT extensions first, empty extension LAST.
* 2. chooseUpdaterArgs() — handOffWindowsBootstrapRecovery() chose
* --update vs the destructive --repair by checking ONLY
* venv\Scripts\hermes.exe (the console-script shim, written at the END
* of venv setup and absent in interrupted states), so it escalated to a
* full venv recreate even on healthy installs. The fix: gate on ANY
* real-install signal, not just the shim.
* 2. chooseUpdaterArgs() — handOffWindowsBootstrapRecovery() must separate
* install provenance from updater viability. A bootstrap-complete marker
* can outlive a deleted venv, while the updater needs BOTH the venv Python
* and Hermes launcher. Marker-only or partial runtimes must use --repair;
* only a runnable pair can use --update.
* 3. resolveVenvHermesCommand() — unwrapWindowsVenvHermesCommand() returned
* the venv python with NO runtime probe (bypassing the caller's
* --version check too), so a venv broken mid-update (e.g. missing
@@ -61,23 +60,26 @@ export function buildPathExtCandidates(pathext: string | undefined, isWindows: b
}
/**
* Choose the Windows bootstrap-recovery updater invocation: the gentle
* in-place --update when ANY real-install signal is present, the
* destructive --repair (full venv recreate) otherwise.
* Choose the Windows bootstrap-recovery invocation. The gentle in-place
* updater can only start when both pieces of its runtime contract exist: the
* venv Python interpreter and the Hermes launcher that drives `hermes update`.
* A bootstrap-complete marker proves install provenance, not current runtime
* usability, and may remain after the venv is removed or quarantined.
*
* haveRealInstall must be computed by the caller from ALL real-install
* signals (venv python interpreter, venv hermes shim, bootstrap-complete
* marker) — gating on just the hermes.exe console-script shim alone is the
* regression this function's callers must avoid: that shim is written at
* the END of venv setup and is absent in exactly the interrupted/quarantined
* states this recovery exists to heal.
*
* @param {boolean} haveRealInstall
* @param {BootstrapRecoverySignals} signals
* @param {string} branch
* @returns {string[]} updater argv, e.g. ['--update', '--branch', 'main'].
*/
export function chooseUpdaterArgs(haveRealInstall: boolean, branch: string): string[] {
return haveRealInstall ? ['--update', '--branch', branch] : ['--repair', '--branch', branch]
export interface BootstrapRecoverySignals {
hasBootstrapMarker: boolean
hasVenvHermes: boolean
hasVenvPython: boolean
}
export function chooseUpdaterArgs(signals: BootstrapRecoverySignals, branch: string): string[] {
const canRunUpdater = signals.hasVenvHermes && signals.hasVenvPython
return canRunUpdater ? ['--update', '--branch', branch] : ['--repair', '--branch', branch]
}
/**