mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
Merge pull request #67182 from NousResearch/bb/p0-salvage
fix(install): keep install.ps1 pure ASCII so Windows PowerShell 5.1 doesn't misparse it (#66994/#67000)
This commit is contained in:
commit
ed957aeb26
2 changed files with 82 additions and 27 deletions
|
|
@ -443,7 +443,7 @@ function Get-PowerShellHostExe {
|
|||
}
|
||||
|
||||
function Install-Uv {
|
||||
# Hermes owns its own uv at $HermesHome\bin\uv.exe. Always install there —
|
||||
# Hermes owns its own uv at $HermesHome\bin\uv.exe. Always install there --
|
||||
# no PATH probing, no conda guards, no multi-location resolution chains.
|
||||
# The runtime update path (hermes_cli/managed_uv.py) looks in the same
|
||||
# place, so install.ps1 and `hermes update` stay in sync.
|
||||
|
|
@ -528,7 +528,7 @@ function Ensure-NodeExeOnPath {
|
|||
# prior process is not visible here. Later stages (Test-Python,
|
||||
# Install-Venv, Install-Dependencies, Install-PlatformSdks) call this
|
||||
# at the top to populate $script:UvCmd from the managed location.
|
||||
# Throws if uv is not findable — the caller's stage then surfaces a
|
||||
# Throws if uv is not findable -- the caller's stage then surfaces a
|
||||
# clean error via the stage-driver's try/catch.
|
||||
function Resolve-UvCmd {
|
||||
# Already resolved (default invocation path: Install-Uv ran earlier
|
||||
|
|
@ -544,7 +544,7 @@ function Resolve-UvCmd {
|
|||
# Stale; fall through to re-discover.
|
||||
}
|
||||
|
||||
# Check the managed location first — this is where Install-Uv puts it.
|
||||
# Check the managed location first -- this is where Install-Uv puts it.
|
||||
$managedUv = Join-Path $HermesHome "bin\uv.exe"
|
||||
if (Test-Path $managedUv) {
|
||||
$script:UvCmd = $managedUv
|
||||
|
|
@ -1516,7 +1516,7 @@ function Install-Repository {
|
|||
if ($LASTEXITCODE -ne 0) { throw "git checkout $Branch failed (exit $LASTEXITCODE)" }
|
||||
# Managed installs should follow origin/$Branch exactly. If
|
||||
# the checkout has diverged (or has local-only commits),
|
||||
# ff-only pull cannot succeed — mirror ``hermes update`` and
|
||||
# ff-only pull cannot succeed -- mirror ``hermes update`` and
|
||||
# reset to the fetched remote so bootstrap/install can recover.
|
||||
git -c windows.appendAtomically=false pull --ff-only origin $Branch
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
|
|
@ -1573,11 +1573,11 @@ function Install-Repository {
|
|||
Write-Host ""
|
||||
Write-Host "Conflicted files:"
|
||||
foreach ($file in $conflictedFiles) {
|
||||
Write-Host " • $file"
|
||||
Write-Host " - $file"
|
||||
}
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Info "Your stashed changes are preserved — nothing is lost."
|
||||
Write-Info "Your stashed changes are preserved -- nothing is lost."
|
||||
Write-Info " Stash ref: $autostashRef"
|
||||
git -c windows.appendAtomically=false reset --hard HEAD 2>$null | Out-Null
|
||||
Write-Info "Working tree reset to clean state."
|
||||
|
|
@ -1798,8 +1798,8 @@ function Install-Venv {
|
|||
# /End stops a running task instance; /Change /DISABLE stops it
|
||||
# from re-firing mid-install. (The Startup-folder .vbs fallback is
|
||||
# NOT touched: it only fires at logon, so it cannot respawn a
|
||||
# gateway mid-install.) Re-enabled in the finally below — including
|
||||
# on failure — but only for tasks that were enabled to begin with.
|
||||
# gateway mid-install.) Re-enabled in the finally below -- including
|
||||
# on failure -- but only for tasks that were enabled to begin with.
|
||||
# Best-effort: a missing task just errors quietly.
|
||||
try {
|
||||
schtasks /Query /FO CSV 2>$null | ConvertFrom-Csv | Where-Object { $_.TaskName -like '*Hermes_Gateway*' } | ForEach-Object {
|
||||
|
|
@ -1893,7 +1893,7 @@ function Install-Venv {
|
|||
}
|
||||
|
||||
# Clean up parked venvs from previous installs whose handles have since
|
||||
# been released. Best-effort — a still-held tree just stays for next time.
|
||||
# been released. Best-effort -- a still-held tree just stays for next time.
|
||||
Get-ChildItem -Directory -Filter "venv.stale.*" -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -1926,10 +1926,10 @@ function Install-Venv {
|
|||
} finally {
|
||||
Pop-Location
|
||||
# Re-arm the gateway autostart tasks disabled during the venv teardown
|
||||
# — in a finally so a failed teardown/creation can never strand the
|
||||
# -- in a finally so a failed teardown/creation can never strand the
|
||||
# user's gateway autostart in the disabled state. Same function scope,
|
||||
# so the list survives even under the stage-per-process bootstrap.
|
||||
# Deliberately NOT started here — dependencies aren't installed yet;
|
||||
# Deliberately NOT started here -- dependencies aren't installed yet;
|
||||
# the task fires normally on next logon and `hermes update` / the
|
||||
# gateway resume path handles the immediate restart.
|
||||
if ($gatewayTasksDisabled -and $gatewayTasksDisabled.Count -gt 0) {
|
||||
|
|
@ -2247,7 +2247,7 @@ function Set-PathVariable {
|
|||
function Write-BootstrapMarker {
|
||||
# Writes $InstallDir\.hermes-bootstrap-complete which tells the Hermes
|
||||
# desktop app (apps/desktop/electron/main.ts) "install.ps1 ran
|
||||
# successfully — DON'T trigger the legacy first-launch bootstrap
|
||||
# successfully -- DON'T trigger the legacy first-launch bootstrap
|
||||
# runner."
|
||||
#
|
||||
# Schema mirrors what main.ts's writeBootstrapMarker() / isBootstrap
|
||||
|
|
@ -2268,7 +2268,7 @@ function Write-BootstrapMarker {
|
|||
# Resolve the pinned commit: explicit -Commit wins, otherwise read
|
||||
# the checkout's HEAD via git. If git can't run, leave commit empty
|
||||
# and the marker will fail desktop validation (pinnedCommit.length
|
||||
# >= 7) — better to be invalid than wrong.
|
||||
# >= 7) -- better to be invalid than wrong.
|
||||
$pinnedCommit = $Commit
|
||||
if (-not $pinnedCommit) {
|
||||
# PS 5.1 doesn't support the ?. null-conditional operator, so
|
||||
|
|
@ -2283,7 +2283,7 @@ function Write-BootstrapMarker {
|
|||
$pinnedCommit = $resolved.Trim()
|
||||
}
|
||||
} catch {
|
||||
# Ignore — pinnedCommit stays empty, marker stays invalid,
|
||||
# Ignore -- pinnedCommit stays empty, marker stays invalid,
|
||||
# desktop falls through to its legacy bootstrap path.
|
||||
} finally {
|
||||
Pop-Location
|
||||
|
|
@ -2302,7 +2302,7 @@ function Write-BootstrapMarker {
|
|||
pinnedCommit = $pinnedCommit
|
||||
pinnedBranch = $pinnedBranch
|
||||
completedAt = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
||||
# desktopVersion field intentionally omitted — only the desktop
|
||||
# desktopVersion field intentionally omitted -- only the desktop
|
||||
# app knows its own version, and the marker validator doesn't
|
||||
# require it. The desktop fills it in if/when it writes its
|
||||
# own marker (e.g. after a future in-app upgrade).
|
||||
|
|
@ -2311,7 +2311,7 @@ function Write-BootstrapMarker {
|
|||
|
||||
# Write WITHOUT a UTF-8 BOM. PowerShell 5.1's `Set-Content -Encoding UTF8`
|
||||
# always emits a BOM, and Node's plain JSON.parse rejects the BOM as an
|
||||
# unexpected character — so a BOM'd marker would silently fail the
|
||||
# unexpected character -- so a BOM'd marker would silently fail the
|
||||
# desktop's readJson(), make isBootstrapComplete() return null, and the
|
||||
# desktop would re-run the legacy bootstrap runner anyway. Defeats the
|
||||
# whole point. Use the .NET API directly for BOM-less UTF-8.
|
||||
|
|
@ -2411,7 +2411,7 @@ function Install-NodeDeps {
|
|||
# Cross-process driver mode (Hermes-Setup.exe runs each -Stage NAME
|
||||
# in a fresh powershell.exe) means $script:HasNode set by Stage-Node
|
||||
# in the previous process isn't visible here. Re-probe rather than
|
||||
# trust the stale global — Stage-Node already ran successfully or
|
||||
# trust the stale global -- Stage-Node already ran successfully or
|
||||
# the bootstrap would've aborted, so npm is reachable.
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
Write-Info "Skipping Node.js dependencies (Node not installed)"
|
||||
|
|
@ -2688,7 +2688,7 @@ function Clear-ElectronBuildCache {
|
|||
# Last-resort Electron mirror after GitHub download fails (#47266).
|
||||
$script:DesktopElectronFallbackMirror = "https://npmmirror.com/mirrors/electron/"
|
||||
|
||||
# Electron package dir — workspace-local nest first, then root hoist.
|
||||
# Electron package dir -- workspace-local nest first, then root hoist.
|
||||
function Get-ElectronDir {
|
||||
param([string]$InstallDir)
|
||||
$desktopLocal = Join-Path $InstallDir 'apps\desktop\node_modules\electron'
|
||||
|
|
@ -2769,7 +2769,7 @@ function Install-Desktop {
|
|||
#
|
||||
# The Tauri bootstrap installer's launch_hermes_desktop command
|
||||
# resolves apps/desktop/release/win-unpacked/Hermes.exe directly,
|
||||
# so an "unpacked" build (electron-builder --dir) is enough — we
|
||||
# so an "unpacked" build (electron-builder --dir) is enough -- we
|
||||
# don't need to produce an NSIS/MSI artifact here.
|
||||
|
||||
# Always re-resolve Node here. Stages run in separate PowerShell processes,
|
||||
|
|
@ -2823,7 +2823,7 @@ function Install-Desktop {
|
|||
#
|
||||
# The streaming sink in bootstrap.rs's run_install_script
|
||||
# captures every stdout/stderr line as it's emitted, so we don't
|
||||
# need a side TEMP log file — the installer's bootstrap log
|
||||
# need a side TEMP log file -- the installer's bootstrap log
|
||||
# IS the artifact a support engineer reads.
|
||||
#
|
||||
# Prefer `npm ci`: it wipes node_modules and reinstalls from the
|
||||
|
|
@ -2878,7 +2878,7 @@ function Install-Desktop {
|
|||
# NOT signing the output. Combined with signAndEditExecutable=false in
|
||||
# apps/desktop/package.json's build.win block, electron-builder never
|
||||
# invokes signtool and therefore never fetches/extracts winCodeSign
|
||||
# (whose macOS symlinks crash 7-Zip on non-admin Windows — a dead end we
|
||||
# (whose macOS symlinks crash 7-Zip on non-admin Windows -- a dead end we
|
||||
# are NOT trying to work around). The Hermes icon + product name are
|
||||
# stamped onto Hermes.exe by our own rcedit step (Set-DesktopExeIdentity)
|
||||
# AFTER this build, completely decoupled from electron-builder signing.
|
||||
|
|
@ -2949,7 +2949,7 @@ function Install-Desktop {
|
|||
Pop-Location
|
||||
throw
|
||||
} finally {
|
||||
# Restore env to whatever the caller had — don't leak our
|
||||
# Restore env to whatever the caller had -- don't leak our
|
||||
# signing-off override into anything install.ps1 invokes later
|
||||
# (Stage-PlatformSdks, etc.).
|
||||
$env:CSC_IDENTITY_AUTO_DISCOVERY = $prevCSCAuto
|
||||
|
|
@ -2980,7 +2980,7 @@ function Install-Desktop {
|
|||
|
||||
# 3b. The Hermes icon + identity are stamped onto Hermes.exe by the
|
||||
# electron-builder `afterPack` hook (apps/desktop/scripts/after-pack.mjs)
|
||||
# during `npm run pack` above — for every build, so the installer's
|
||||
# during `npm run pack` above -- for every build, so the installer's
|
||||
# --update rebuild stays branded too. No separate stamp step needed here.
|
||||
# electron-builder's own rcedit step stays disabled (signAndEditExecutable
|
||||
# =false) because enabling it drags in signtool -> winCodeSign -> the
|
||||
|
|
@ -2990,7 +2990,7 @@ function Install-Desktop {
|
|||
# directory. Chromium's GPU/renderer sandboxes CHECK-fail with
|
||||
# 0x80000003 when this ACE is missing alongside orphan AppContainer
|
||||
# SIDs under %LOCALAPPDATA% (electron/electron#51761, hermes-agent#38216).
|
||||
# Best-effort — never fail an otherwise-good install over ACL repair.
|
||||
# Best-effort -- never fail an otherwise-good install over ACL repair.
|
||||
try {
|
||||
$appDir = Split-Path -Parent $desktopExe
|
||||
& icacls $appDir /grant "*S-1-15-2-2:(OI)(CI)(RX)" /T /C /Q | Out-Null
|
||||
|
|
@ -3006,7 +3006,7 @@ function Install-Desktop {
|
|||
# 4. Create Start Menu + Desktop shortcuts pointing DIRECTLY at the packed
|
||||
# Hermes.exe. We deliberately do NOT point them at `hermes desktop`: that
|
||||
# command rebuilds (npm install + electron-builder) on every launch,
|
||||
# which would cost minutes each time. The packed exe is the consumer —
|
||||
# which would cost minutes each time. The packed exe is the consumer --
|
||||
# launching it directly is instant, and updates flow through the
|
||||
# installer's --update path (which rebuilds once, then relaunches).
|
||||
New-DesktopShortcuts -TargetExe $desktopExe
|
||||
|
|
@ -3062,11 +3062,11 @@ function New-DesktopShortcuts {
|
|||
# cached bitmap. Critical on the --update path: the exe was re-stamped
|
||||
# with the Hermes icon, but without this the shortcut can keep drawing
|
||||
# the old Electron icon until the user manually refreshes / reboots.
|
||||
# Best-effort and silent — never fail the install over a cosmetic cache.
|
||||
# Best-effort and silent -- never fail the install over a cosmetic cache.
|
||||
try {
|
||||
& ie4uinit.exe -show 2>$null
|
||||
} catch {
|
||||
# ie4uinit may be absent/renamed on some SKUs — ignore.
|
||||
# ie4uinit may be absent/renamed on some SKUs -- ignore.
|
||||
}
|
||||
} catch {
|
||||
Write-Warn "Skipping shortcut creation: $($_.Exception.Message)"
|
||||
|
|
|
|||
55
tests/test_install_ps1_ascii_only.py
Normal file
55
tests/test_install_ps1_ascii_only.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Regression: install.ps1 must stay pure ASCII.
|
||||
|
||||
Issues #66994 / #67000 reported the Windows GUI installer (Hermes-Setup.exe)
|
||||
crashing before it did anything, with a cascade of PowerShell parser errors at
|
||||
lines 1619 / 1770 ("Missing argument in parameter list", "A 'using' statement
|
||||
must appear before any other statements in a script").
|
||||
|
||||
Root cause: ``scripts/install.ps1`` has no UTF-8 BOM, and a commit added two
|
||||
non-ASCII characters *inside double-quoted string literals* (a bullet and an
|
||||
em-dash). Windows PowerShell 5.1 -- which the bootstrap runs the cached script
|
||||
under -- reads a BOM-less ``.ps1`` in the system ANSI code page (e.g. CP1252),
|
||||
not UTF-8. The em-dash's UTF-8 tail byte (0x94) decodes to a "smart" close-quote
|
||||
(U+201D), which the PowerShell tokenizer treats as a string delimiter. That
|
||||
prematurely closes the string and desyncs the parser for the rest of the file,
|
||||
surfacing as unrelated syntax errors far downstream.
|
||||
|
||||
Non-ASCII bytes inside ``#`` comments are harmless (the tokenizer skips a
|
||||
comment to end-of-line regardless of what it contains), which is why the file
|
||||
carried em-dashes in comments for months without breaking -- only a non-ASCII
|
||||
byte in *code* (a string literal) triggers the desync.
|
||||
|
||||
This test is source-level because Linux CI cannot execute the PowerShell
|
||||
installer. Keeping the whole file ASCII-only is the transport-independent
|
||||
invariant: pure ASCII cannot be misdecoded under any code page, BOM or not, so
|
||||
the bug class cannot recur -- in a comment or a string.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
INSTALL_PS1 = REPO_ROOT / "scripts" / "install.ps1"
|
||||
|
||||
|
||||
def test_install_ps1_is_pure_ascii() -> None:
|
||||
raw = INSTALL_PS1.read_bytes()
|
||||
|
||||
offenders = []
|
||||
line_no = 1
|
||||
for byte in raw:
|
||||
if byte == 0x0A:
|
||||
line_no += 1
|
||||
elif byte >= 0x80:
|
||||
offenders.append(line_no)
|
||||
|
||||
assert not offenders, (
|
||||
"scripts/install.ps1 must be pure ASCII so Windows PowerShell 5.1 "
|
||||
"(which reads a BOM-less .ps1 in the system ANSI code page, not UTF-8) "
|
||||
"cannot misdecode a byte into a stray quote and desync the parser "
|
||||
"(issues #66994 / #67000). Non-ASCII bytes found on line(s): "
|
||||
f"{sorted(set(offenders))}. Use ASCII equivalents (em-dash -> '--', "
|
||||
"bullet -> '-')."
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue