Files
hermes-agent/apps/desktop/installer/prereq-check.nsh
T

768 lines
33 KiB
NSIS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; ============================================================================
; Hermes Desktop installer — prerequisite detection page
; ============================================================================
;
; A native NSIS Wizard page (using nsDialogs) inserted between the directory
; selection page and the install-files page. Detects Python 3.11+, Git for
; Windows, and ripgrep; offers to install missing items via winget.
;
; Page sequence:
; Welcome → Directory → [PrereqPage] → InstFiles → Finish
;
; Hooks used:
; customInit — open $TEMP\Hermes-Installer.log for diagnostics
; customPageAfterChangeDir — page declaration (electron-builder's hook for
; inserting a page between Directory and InstFiles)
; customInstall — execute winget for any prereqs the user
; checked on the page; close the log file
;
; Diagnostics:
; $TEMP\Hermes-Installer.log captures every detection probe (command,
; exit code, captured output), the user's checkbox choices, and full
; winget stdout/stderr for Python and ripgrep installs. Git install
; goes via ExecShellWait (for UAC focus reasons) which cannot capture
; output, so for Git we log start/end + the post-install filesystem
; probe result only. Users hitting bugs should attach this file.
;
; The Function declarations live at top-level in this file so they're parsed
; at include time; the customPageAfterChangeDir macro references them via
; the Page directive so the optimizer doesn't strip them.
;
; UAC behavior:
; Python: --scope user, no UAC.
; ripgrep: --scope user, no UAC.
; Git for Windows: always per-machine, triggers UAC prompt.
; Footer warns the user about Git's UAC; ExecShellWait preserves the
; foreground focus chain so the prompt comes to front.
;
; Detection:
; Python: try `py -3.11`/`-3.12`/`-3.13`/`-3.14`. The Python launcher
; returns exit 0 only when that specific version is installed. The
; Microsoft Store "Python stub" doesn't install py.exe, so users with
; only the stub get correctly classified as not-installed.
; Git: `where git` returns exit 0 if git is on PATH.
; ripgrep: `where rg` returns exit 0 if rg is on PATH.
; winget: `where winget` returns exit 0 on Win11 / Win10 1809+ with App
; Installer. If unavailable, the page shows manual download URLs.
;
; Required vs. recommended:
; Python and Git are REQUIRED — without them the agent's runtime + terminal
; tool fail. The page emphasizes "required" wording and the bootstrapper
; throws if either is missing at first launch.
; ripgrep is RECOMMENDED — Hermes' search_files tool uses it for fast
; .gitignore-aware search, and falls back to grep/find from Git Bash when
; missing (works but slower, less filtering). Page wording is softer for
; ripgrep so users understand they CAN skip it.
;
; Skip behaviors:
; - All three already detected → page is auto-skipped via Abort
; - Silent install (/S) → customInstall winget block skips
; - User unchecks all checkboxes → page advances without running winget
; ============================================================================
!include "LogicLib.nsh"
!include "nsDialogs.nsh"
!include "WinMessages.nsh"
!include "FileFunc.nsh"
Var HermesDialog
Var HermesPyStatusLabel
Var HermesPyCheckbox
Var HermesGitStatusLabel
Var HermesGitCheckbox
Var HermesRgStatusLabel
Var HermesRgCheckbox
Var HermesFooterLabel
Var HermesHasWinget
Var HermesHasPython
Var HermesHasGit
Var HermesHasRipgrep
Var HermesInstallPython
Var HermesInstallGit
Var HermesInstallRipgrep
Var HermesLogHandle
Var HermesLogPath
; ----------------------------------------------------------------------------
; Installer logging
; ----------------------------------------------------------------------------
; We write a structured log to $TEMP\Hermes-Installer.log so users can attach
; it to bug reports when prereq detection or winget installs misbehave.
;
; Why this design:
; - The wizard's built-in Details panel only exists at runtime; once the
; user clicks Finish (or Cancel) it's gone. The file persists.
; - NSIS's built-in `LogSet on` / `LogText` requires the "advanced logging"
; build of makensis (NSIS_CONFIG_LOG=1), which electron-builder's bundled
; binary doesn't include. So we roll our own with FileWrite.
; - Every winget invocation streams its full stdout/stderr into the log via
; nsExec::ExecToStack — the same data the Details panel shows, but
; captured for post-mortem.
; - Detection probes also log exit codes + captured output, so when a user
; reports "the page said Python isn't installed but I have it", we can
; see exactly which probes ran and what they returned.
; - File is opened (truncate mode) in customInit and explicitly closed at
; the end of customInstall. If the installer crashes or the user
; cancels before customInstall completes, the file remains on disk —
; whatever we wrote up to that point survives. FileWrite per-line is a
; normal Windows I/O call that hits the kernel buffer cache; the OS
; flushes that buffer when the process exits, so even on hard cancel
; the user can attach a partial log.
;
; Macros:
; ${HermesLog} "free-form text" — emit a timestamped line
; ${HermesLogKV} "key" "value" — emit a "key = value" line
; ${HermesLogBlock} "label" "varname" — emit a delimited block (no `$`)
;
; The macros are no-ops when $HermesLogHandle is empty (e.g. if FileOpen
; failed because $TEMP was unwritable — rare but defensive).
; ----------------------------------------------------------------------------
!macro _HermesLogRaw Line
${If} $HermesLogHandle != ""
FileWrite $HermesLogHandle "${Line}$\r$\n"
${EndIf}
!macroend
!macro _HermesLogTimestamped Msg
; ${__TIMESTAMP__} is the BUILD-time stamp, not runtime. We want runtime,
; so use ${GetTime} from FileFunc.nsh. $R0..$R6 = day, month, year, dow,
; hour, minute, second. Stash callers' $R0$R6 first.
Push $R0
Push $R1
Push $R2
Push $R3
Push $R4
Push $R5
Push $R6
${GetTime} "" "L" $R0 $R1 $R2 $R3 $R4 $R5 $R6
${If} $HermesLogHandle != ""
FileWrite $HermesLogHandle "[$R2-$R1-$R0 $R4:$R5:$R6] ${Msg}$\r$\n"
${EndIf}
Pop $R6
Pop $R5
Pop $R4
Pop $R3
Pop $R2
Pop $R1
Pop $R0
!macroend
!define HermesLog "!insertmacro _HermesLogTimestamped"
!macro _HermesLogKV Key Value
${HermesLog} "${Key} = ${Value}"
!macroend
!define HermesLogKV "!insertmacro _HermesLogKV"
; HermesLogBlock — write a multi-line block (typically captured command
; output) with a "--- begin/end ---" frame so it's clear in the log where
; the captured payload starts and stops. The `Payload` parameter is the
; NSIS variable name (without `$`) holding the captured string.
!macro _HermesLogBlock Label PayloadVar
${HermesLog} "--- begin ${Label} ---"
!insertmacro _HermesLogRaw "$${PayloadVar}"
${HermesLog} "--- end ${Label} ---"
!macroend
!define HermesLogBlock "!insertmacro _HermesLogBlock"
; ----------------------------------------------------------------------------
; HermesDetectPythonViaRegistry — sets $HermesHasPython="1" if a PEP 514
; entry exists for any of the supported Python versions. Reads HKLM
; (system-wide installs) then HKCU (per-user installs). Vendor "PythonCore"
; covers official python.org distributions; "ContinuumAnalytics" covers
; Anaconda/Miniconda. We don't enumerate other vendors because they're
; rare in our user base and we'd rather miss them and let winget add a
; second Python than misclassify something else as a working Python.
; ----------------------------------------------------------------------------
Function HermesDetectPythonViaRegistry
Push $1
Push $2
${HermesLog} "registry: scanning HKLM/HKCU SOFTWARE\Python\PythonCore for 3.11/3.12/3.13"
; Set view to 64-bit on x64 systems so we read the right hive — the
; default 32-bit view would miss a 64-bit Python install on 64-bit
; Windows. SetRegView 32 restored at function exit.
SetRegView 64
; Iterate the supported versions. Each is its own ReadRegStr — NSIS
; doesn't have loops over arrays inside functions easily, and four
; copies is clearer than gymnastics with $R0-$R9.
ReadRegStr $1 HKLM "SOFTWARE\Python\PythonCore\3.11\InstallPath" ""
${If} $1 != ""
${HermesLog} " hit: HKLM\Python\PythonCore\3.11\InstallPath = $1"
StrCpy $HermesHasPython "1"
Goto hermes_py_reg_done
${EndIf}
ReadRegStr $1 HKLM "SOFTWARE\Python\PythonCore\3.12\InstallPath" ""
${If} $1 != ""
${HermesLog} " hit: HKLM\Python\PythonCore\3.12\InstallPath = $1"
StrCpy $HermesHasPython "1"
Goto hermes_py_reg_done
${EndIf}
ReadRegStr $1 HKLM "SOFTWARE\Python\PythonCore\3.13\InstallPath" ""
${If} $1 != ""
${HermesLog} " hit: HKLM\Python\PythonCore\3.13\InstallPath = $1"
StrCpy $HermesHasPython "1"
Goto hermes_py_reg_done
${EndIf}
ReadRegStr $1 HKCU "SOFTWARE\Python\PythonCore\3.11\InstallPath" ""
${If} $1 != ""
${HermesLog} " hit: HKCU\Python\PythonCore\3.11\InstallPath = $1"
StrCpy $HermesHasPython "1"
Goto hermes_py_reg_done
${EndIf}
ReadRegStr $1 HKCU "SOFTWARE\Python\PythonCore\3.12\InstallPath" ""
${If} $1 != ""
${HermesLog} " hit: HKCU\Python\PythonCore\3.12\InstallPath = $1"
StrCpy $HermesHasPython "1"
Goto hermes_py_reg_done
${EndIf}
ReadRegStr $1 HKCU "SOFTWARE\Python\PythonCore\3.13\InstallPath" ""
${If} $1 != ""
${HermesLog} " hit: HKCU\Python\PythonCore\3.13\InstallPath = $1"
StrCpy $HermesHasPython "1"
Goto hermes_py_reg_done
${EndIf}
${HermesLog} " no registry keys matched"
hermes_py_reg_done:
SetRegView 32
Pop $2
Pop $1
FunctionEnd
; ----------------------------------------------------------------------------
; HermesDetectPythonViaFilesystem — sets $HermesHasPython="1" if a Python
; install exists at one of the standard locations. FileExists never runs
; the binary so this is safe even if the user has the MS Store stub on
; their PATH. We probe both system-wide (Program Files) and per-user
; (LocalAppData\Programs) install locations for versions 3.113.14.
; ----------------------------------------------------------------------------
Function HermesDetectPythonViaFilesystem
${HermesLog} "filesystem: probing standard Python install paths"
; System-wide installs (default location for python.org with admin)
${If} ${FileExists} "$PROGRAMFILES64\Python311\python.exe"
${HermesLog} " hit: $PROGRAMFILES64\Python311\python.exe"
StrCpy $HermesHasPython "1"
Return
${EndIf}
${If} ${FileExists} "$PROGRAMFILES64\Python312\python.exe"
${HermesLog} " hit: $PROGRAMFILES64\Python312\python.exe"
StrCpy $HermesHasPython "1"
Return
${EndIf}
${If} ${FileExists} "$PROGRAMFILES64\Python313\python.exe"
${HermesLog} " hit: $PROGRAMFILES64\Python313\python.exe"
StrCpy $HermesHasPython "1"
Return
${EndIf}
; Per-user installs (default location for python.org without admin
; or with "Install for me only"). Covers the user-reported case.
${If} ${FileExists} "$LOCALAPPDATA\Programs\Python\Python311\python.exe"
${HermesLog} " hit: $LOCALAPPDATA\Programs\Python\Python311\python.exe"
StrCpy $HermesHasPython "1"
Return
${EndIf}
${If} ${FileExists} "$LOCALAPPDATA\Programs\Python\Python312\python.exe"
${HermesLog} " hit: $LOCALAPPDATA\Programs\Python\Python312\python.exe"
StrCpy $HermesHasPython "1"
Return
${EndIf}
${If} ${FileExists} "$LOCALAPPDATA\Programs\Python\Python313\python.exe"
${HermesLog} " hit: $LOCALAPPDATA\Programs\Python\Python313\python.exe"
StrCpy $HermesHasPython "1"
Return
${EndIf}
${HermesLog} " no filesystem paths matched"
FunctionEnd
; ----------------------------------------------------------------------------
; HermesProbe — small wrapper around nsExec::ExecToStack that captures both
; exit code and stdout/stderr into a single log entry. Used instead of bare
; nsExec::Exec so we have evidence for "the page said X isn't installed but
; I have it" bug reports.
;
; Caller pushes command string onto the stack before Call.
; On return:
; $0 = exit code (0 = success on Windows)
; $1 = captured stdout+stderr (truncated by NSIS to ~64KB)
; Caller's $0/$1 are clobbered; $9 is preserved.
; ----------------------------------------------------------------------------
Function HermesProbe
; Stack on entry (top → bottom): <command>, <caller-return-addr>, ...
; Save $9 so we can use it as a local. Standard NSIS stack-arg idiom:
; Exch $9 ; $9 = arg, old $9 pushed onto stack
; ... work ...
; Pop $9 ; restore old $9 (discards arg)
Exch $9
nsExec::ExecToStack '$9'
Pop $0 ; exit code
Pop $1 ; captured output
${HermesLog} "probe: $9"
${HermesLog} " exit = $0"
${If} $1 != ""
${HermesLogBlock} "probe output" "1"
${EndIf}
Pop $9 ; restore caller's $9 (discards the command arg)
FunctionEnd
; ----------------------------------------------------------------------------
; HermesDetectPrereqs — populates $HermesHasWinget / $HermesHasPython /
; $HermesHasGit / $HermesHasRipgrep with "0" or "1". Called from the
; page-create function. Every probe is logged via HermesProbe.
; ----------------------------------------------------------------------------
Function HermesDetectPrereqs
${HermesLog} "=== HermesDetectPrereqs: begin ==="
; --- winget ---
Push 'cmd.exe /c where winget'
Call HermesProbe
${If} $0 == 0
StrCpy $HermesHasWinget "1"
${Else}
StrCpy $HermesHasWinget "0"
${EndIf}
${HermesLogKV} "HermesHasWinget" "$HermesHasWinget"
; --- Python 3.11 / 3.12 / 3.13 ---
; We deliberately accept 3.113.13 only and NOT 3.14, because some of
; Hermes' transitive deps (notably pywinpty, which carries Rust crates
; like windows_x86_64_msvc) don't yet publish 3.14 wheels. Without
; wheels, `pip install -e .` falls back to building from sdist, which
; needs a Rust toolchain. Users without one see a confusing "could
; not compile windows_x86_64_msvc build script" error. install.ps1
; sidesteps this by pinning to 3.11 via uv; the desktop installer
; can't easily install uv in the same flow yet, so we just refuse to
; accept 3.14 as "good" and offer 3.11 via winget instead. Revisit
; when 3.14 wheels are widely available across our dep tree.
;
; Detection strategy, in order from most-precise to least-precise.
; Each step uses ONLY operations that don't execute `python.exe`
; directly off PATH — running `python` on Windows can open the
; Microsoft Store if only the "Python stub" is installed, which is
; terrible UX during an installer. We avoid that by:
; (a) launcher checks (py.exe runs no python until -V),
; (b) registry reads (PEP 514, no execution at all),
; (c) filesystem probes via FileExists.
StrCpy $HermesHasPython "0"
; (1) The py launcher. Ships with python.org installer when
; "Install launcher for all users" is checked (default for some
; paths, not for per-user installs without elevation). When
; present, py -3.X --version returns 0 iff that version exists.
Push 'cmd.exe /c py -3.11 --version'
Call HermesProbe
${If} $0 == 0
StrCpy $HermesHasPython "1"
${Else}
Push 'cmd.exe /c py -3.12 --version'
Call HermesProbe
${If} $0 == 0
StrCpy $HermesHasPython "1"
${Else}
Push 'cmd.exe /c py -3.13 --version'
Call HermesProbe
${If} $0 == 0
StrCpy $HermesHasPython "1"
${EndIf}
${EndIf}
${EndIf}
${HermesLogKV} "after py-launcher probes, HermesHasPython" "$HermesHasPython"
; (2) PEP 514 registry probe. Every standards-compliant Python
; installer registers itself under HKLM or HKCU at
; SOFTWARE\Python\PythonCore\<version>\InstallPath. The MS Store
; stub does NOT register here — so we get a clean signal for
; "real Python is installed" without ever risking the Store
; popup. Covers the case the user reported: per-user Python.org
; install without launcher checkbox, plus Anaconda which writes
; similar keys under a different vendor name.
${If} $HermesHasPython == "0"
Call HermesDetectPythonViaRegistry
${HermesLogKV} "after registry probe, HermesHasPython" "$HermesHasPython"
${EndIf}
; (3) Filesystem probe of common install locations. Catches edge
; cases where the installer didn't update the registry (rare
; but possible with hand-extracted Python or some third-party
; installers). We only check standard paths — running anything
; would risk spawning the Store stub.
${If} $HermesHasPython == "0"
Call HermesDetectPythonViaFilesystem
${HermesLogKV} "after filesystem probe, HermesHasPython" "$HermesHasPython"
${EndIf}
; --- Git ---
Push 'cmd.exe /c where git'
Call HermesProbe
${If} $0 == 0
StrCpy $HermesHasGit "1"
${Else}
StrCpy $HermesHasGit "0"
${EndIf}
${HermesLogKV} "HermesHasGit" "$HermesHasGit"
; --- ripgrep ---
Push 'cmd.exe /c where rg'
Call HermesProbe
${If} $0 == 0
StrCpy $HermesHasRipgrep "1"
${Else}
StrCpy $HermesHasRipgrep "0"
${EndIf}
${HermesLogKV} "HermesHasRipgrep" "$HermesHasRipgrep"
${HermesLog} "=== HermesDetectPrereqs: end ==="
FunctionEnd
; ----------------------------------------------------------------------------
; HermesRunWinget — invoke `winget install ...` and capture both exit code
; and full stdout/stderr to the log. Also replays the captured output to the
; install Details panel via DetailPrint so the user sees progress (batched
; at end rather than live — acceptable trade-off; winget installs take 30-90
; seconds and emit ~10-30 lines).
;
; Caller pushes: <args-after-winget> (e.g. 'install -e --id Python...')
; <human-name> (e.g. 'Python 3.11')
; On return: $0 = winget exit code, $1 = full captured output
; ----------------------------------------------------------------------------
Function HermesRunWinget
Exch $9 ; $9 = human-name
Exch
Exch $8 ; $8 = args
Push $2 ; preserve for caller
${HermesLog} "winget: invoking for $9"
${HermesLog} " command: winget $8"
DetailPrint "Running: winget $8"
; ExecToStack captures up to ~64KB of combined stdout+stderr.
; The 'cmd.exe /c' wrapper ensures we use the user's PATH-resolved winget
; and that I/O redirection works portably.
nsExec::ExecToStack 'cmd.exe /c winget $8'
Pop $0 ; exit code
Pop $1 ; captured output
${HermesLog} " exit code = $0"
${If} $1 != ""
${HermesLogBlock} "winget output ($9)" "1"
; Echo captured output to Details panel so user sees what winget did.
; DetailPrint takes one line; the captured blob may contain $\r$\n. We
; pass it whole — DetailPrint handles embedded newlines reasonably.
DetailPrint "$1"
${EndIf}
Pop $2
Pop $8
Pop $9
FunctionEnd
; ----------------------------------------------------------------------------
; HermesPrereqPageCreate — builds the prereq page UI. If all three items are
; already installed we Abort, which causes NSIS to skip directly to the next
; page in the sequence (InstFiles).
; ----------------------------------------------------------------------------
Function HermesPrereqPageCreate
Call HermesDetectPrereqs
${If} $HermesHasPython == "1"
${AndIf} $HermesHasGit == "1"
${AndIf} $HermesHasRipgrep == "1"
${HermesLog} "page: all prereqs detected, auto-skipping prereq page"
Abort
${EndIf}
${HermesLog} "page: rendering prereq page (winget=$HermesHasWinget python=$HermesHasPython git=$HermesHasGit rg=$HermesHasRipgrep)"
; Set the wizard's standard header (top blue/gradient bar). 1037 is the
; title control, 1038 is the subtitle. Without this, the header still
; reads "Choose Install Location" left over from the Directory page.
GetDlgItem $0 $HWNDPARENT 1037
SendMessage $0 ${WM_SETTEXT} 0 "STR:System Requirements"
GetDlgItem $0 $HWNDPARENT 1038
SendMessage $0 ${WM_SETTEXT} 0 "STR:Hermes needs Python 3.11+ and Git for Windows. ripgrep is recommended."
nsDialogs::Create 1018
Pop $HermesDialog
${If} $HermesDialog == error
Abort
${EndIf}
StrCpy $HermesInstallPython "0"
StrCpy $HermesInstallGit "0"
StrCpy $HermesInstallRipgrep "0"
; Page body intro. The wizard's header (set above) shows the title
; "System Requirements" and subtitle, so we don't repeat them here —
; just one short explanatory line.
${NSD_CreateLabel} 0u 0u 100% 16u "Items already installed are listed as detected. Missing items can be installed automatically via winget."
Pop $0
; --- Python panel (REQUIRED) ---
${NSD_CreateGroupBox} 0u 18u 100% 30u "Python 3.11+ (required)"
Pop $0
${If} $HermesHasPython == "1"
${NSD_CreateLabel} 8u 28u 95% 10u "Detected on your system."
Pop $HermesPyStatusLabel
${Else}
${If} $HermesHasWinget == "1"
${NSD_CreateLabel} 8u 27u 95% 9u "Not detected."
Pop $HermesPyStatusLabel
${NSD_CreateCheckbox} 8u 37u 95% 9u "Install Python 3.11"
Pop $HermesPyCheckbox
${NSD_Check} $HermesPyCheckbox
${Else}
${NSD_CreateLabel} 8u 27u 95% 14u "Not detected. Install manually from https://www.python.org/downloads/ and re-run this installer."
Pop $HermesPyStatusLabel
${EndIf}
${EndIf}
; --- Git panel (REQUIRED) ---
${NSD_CreateGroupBox} 0u 50u 100% 30u "Git for Windows (required, provides Git Bash)"
Pop $0
${If} $HermesHasGit == "1"
${NSD_CreateLabel} 8u 60u 95% 10u "Detected on your system."
Pop $HermesGitStatusLabel
${Else}
${If} $HermesHasWinget == "1"
${NSD_CreateLabel} 8u 59u 95% 9u "Not detected. Required by Hermes' terminal tool."
Pop $HermesGitStatusLabel
${NSD_CreateCheckbox} 8u 69u 95% 9u "Install Git for Windows"
Pop $HermesGitCheckbox
${NSD_Check} $HermesGitCheckbox
${Else}
${NSD_CreateLabel} 8u 59u 95% 14u "Not detected. Install manually from https://git-scm.com/download/win and re-run this installer."
Pop $HermesGitStatusLabel
${EndIf}
${EndIf}
; --- ripgrep panel (RECOMMENDED) ---
${NSD_CreateGroupBox} 0u 82u 100% 30u "ripgrep (recommended for fast file search)"
Pop $0
${If} $HermesHasRipgrep == "1"
${NSD_CreateLabel} 8u 92u 95% 10u "Detected on your system."
Pop $HermesRgStatusLabel
${Else}
${If} $HermesHasWinget == "1"
${NSD_CreateLabel} 8u 91u 95% 9u "Not detected. Hermes will fall back to slower grep/find."
Pop $HermesRgStatusLabel
${NSD_CreateCheckbox} 8u 101u 95% 9u "Install ripgrep"
Pop $HermesRgCheckbox
${NSD_Check} $HermesRgCheckbox
${Else}
${NSD_CreateLabel} 8u 91u 95% 14u "Not detected. Install manually from https://github.com/BurntSushi/ripgrep#installation if you want fast .gitignore-aware search."
Pop $HermesRgStatusLabel
${EndIf}
${EndIf}
; --- Footer (UAC notice when Git install will run) ---
${If} $HermesHasGit == "0"
${AndIf} $HermesHasWinget == "1"
${NSD_CreateLabel} 0u 116u 100% 18u "Note: Git for Windows requires administrator approval. The UAC prompt may appear behind this window — check your taskbar."
Pop $HermesFooterLabel
${EndIf}
nsDialogs::Show
FunctionEnd
; ----------------------------------------------------------------------------
; HermesPrereqPageLeave — read checkbox states when the user clicks Next.
; Variables stay at "0" if a checkbox doesn't exist (because the
; corresponding prereq is already installed or winget isn't available).
; ----------------------------------------------------------------------------
Function HermesPrereqPageLeave
${If} $HermesHasPython == "0"
${AndIf} $HermesHasWinget == "1"
${NSD_GetState} $HermesPyCheckbox $HermesInstallPython
${EndIf}
${If} $HermesHasGit == "0"
${AndIf} $HermesHasWinget == "1"
${NSD_GetState} $HermesGitCheckbox $HermesInstallGit
${EndIf}
${If} $HermesHasRipgrep == "0"
${AndIf} $HermesHasWinget == "1"
${NSD_GetState} $HermesRgCheckbox $HermesInstallRipgrep
${EndIf}
${HermesLog} "page: user choices — install_python=$HermesInstallPython install_git=$HermesInstallGit install_ripgrep=$HermesInstallRipgrep"
FunctionEnd
; ----------------------------------------------------------------------------
; Page declaration — inserted between the Directory page and InstFiles via
; the customPageAfterChangeDir hook (defined in
; node_modules/app-builder-lib/templates/nsis/assistedInstaller.nsh, included
; whenever build.nsis.oneClick=false).
;
; Note: NSIS's optimizer emits "warning 6010: install function ... not
; referenced" for these functions because Page custom directives don't count
; as references in the optimizer's reference-tracking pass. We set
; build.nsis.warningsAsErrors=false in package.json so this warning doesn't
; fail the build. The functions ARE actually called by NSIS at page-display
; time — the optimizer just can't see it statically.
; ----------------------------------------------------------------------------
!macro customPageAfterChangeDir
Page custom HermesPrereqPageCreate HermesPrereqPageLeave
!macroend
; ----------------------------------------------------------------------------
; customInit — runs at installer startup, before any page. We use it to open
; the installer log file. The log path is $TEMP\Hermes-Installer.log; we
; truncate (mode "w") on each install so users don't get an ever-growing
; file. Users hitting bugs are asked to attach this file.
;
; If FileOpen fails (e.g. $TEMP unwritable, AV blocking) we just leave
; $HermesLogHandle empty — every log macro is a no-op when the handle is
; empty, so the installer still works, we just lose the diagnostic.
; ----------------------------------------------------------------------------
!macro customInit
StrCpy $HermesLogPath "$TEMP\Hermes-Installer.log"
ClearErrors
FileOpen $HermesLogHandle "$HermesLogPath" w
${If} ${Errors}
StrCpy $HermesLogHandle ""
; Don't MessageBox — installers shouldn't bother the user about logging
; failures. We still install successfully; we just won't have a log.
${Else}
; UTF-8 BOM so Notepad / editors don't garble any non-ASCII in winget
; output (which uses ✓ characters and other glyphs in some locales).
FileWriteByte $HermesLogHandle "239"
FileWriteByte $HermesLogHandle "187"
FileWriteByte $HermesLogHandle "191"
${HermesLog} "================================================================"
${HermesLog} "Hermes Desktop installer log"
${HermesLog} "================================================================"
${HermesLogKV} "log path" "$HermesLogPath"
${HermesLogKV} "installer name" "$EXEFILE"
${HermesLogKV} "installer dir" "$EXEDIR"
${HermesLogKV} "install target dir" "$INSTDIR"
${HermesLogKV} "TEMP" "$TEMP"
${HermesLogKV} "WINDIR" "$WINDIR"
${HermesLogKV} "PROGRAMFILES64" "$PROGRAMFILES64"
${HermesLogKV} "LOCALAPPDATA" "$LOCALAPPDATA"
${HermesLog} "================================================================"
${EndIf}
!macroend
; ----------------------------------------------------------------------------
; customInstall — runs the actual winget commands for whatever prereqs the
; user checked on the page. Output streams to the install progress log AND
; to $HermesLogPath via HermesRunWinget.
; ----------------------------------------------------------------------------
!macro customInstall
${HermesLog} "=== customInstall: begin ==="
; Tell the user where the log lives so they can attach it if anything
; goes wrong. Shown in the install Details panel.
${If} $HermesLogHandle != ""
DetailPrint "Installer log: $HermesLogPath"
${EndIf}
; Skip on silent installs (managed deploys handle prereqs out-of-band).
IfSilent 0 hermes_prereq_not_silent
${HermesLog} "silent install (/S) — skipping prereq winget block"
Goto hermes_prereq_install_done
hermes_prereq_not_silent:
${If} $HermesInstallPython == "1"
; Python with --scope user installs to %LOCALAPPDATA%\Programs\Python\
; — no UAC, no foreground chain to preserve. HermesRunWinget captures
; both the Details-panel output AND a copy to the installer log.
DetailPrint "Installing Python 3.11+ via winget (silent per-user install, no admin prompt)..."
Push 'install -e --id Python.Python.3.11 --scope user --silent --disable-interactivity --accept-package-agreements --accept-source-agreements'
Push 'Python 3.11'
Call HermesRunWinget
${If} $0 != 0
DetailPrint "Python install via winget exited with code $0."
${HermesLog} "Python install FAILED (exit $0). User notified via MessageBox."
MessageBox MB_OK|MB_ICONEXCLAMATION|MB_TOPMOST "Python install via winget did not complete successfully (exit code $0).$\r$\n$\r$\nSee log: $HermesLogPath$\r$\n$\r$\nYou can install Python 3.11+ manually from https://www.python.org/downloads/ after Hermes setup finishes. Hermes will not run until Python is installed."
${Else}
DetailPrint "Python 3.11+ installed successfully."
${HermesLog} "Python install succeeded"
${EndIf}
${EndIf}
${If} $HermesInstallRipgrep == "1"
; ripgrep with --scope user — ~5MB, no UAC needed. Failure is non-fatal:
; Hermes' search_files tool falls back to grep/find from Git Bash.
DetailPrint "Installing ripgrep via winget (silent per-user install, no admin prompt)..."
Push 'install -e --id BurntSushi.ripgrep.MSVC --scope user --silent --disable-interactivity --accept-package-agreements --accept-source-agreements'
Push 'ripgrep'
Call HermesRunWinget
${If} $0 != 0
DetailPrint "ripgrep install via winget exited with code $0 (non-fatal — Hermes will fall back to grep/find)."
${HermesLog} "ripgrep install failed (exit $0) — non-fatal"
${Else}
DetailPrint "ripgrep installed successfully."
${HermesLog} "ripgrep install succeeded"
${EndIf}
${EndIf}
${If} $HermesInstallGit == "1"
; Git for Windows always installs per-machine and triggers UAC. We use
; ExecShellWait (NSIS's wrapper around Windows ShellExecute) instead of
; nsExec because ShellExecute preserves the foreground focus chain
; across non-elevated → elevated process spawns. With nsExec the
; intermediate hidden winget.exe breaks that chain and UAC ends up
; behind the installer window.
;
; Trade-off: ExecShellWait doesn't capture output, so winget runs in
; its own console window. The console flashes briefly while winget
; downloads, then UAC fires for the elevated Git installer with
; correct foreground promotion. We CANNOT log winget's stdout/stderr
; for this case; we only log start time, end time, and the post-
; install filesystem probe result.
DetailPrint "Installing Git for Windows via winget (UAC prompt will appear)..."
${HermesLog} "Git: starting ExecShellWait — UAC will fire; no stdout capture possible"
${HermesLog} " command: winget install -e --id Git.Git --silent --disable-interactivity --accept-package-agreements --accept-source-agreements"
ExecShellWait "open" "winget" "install -e --id Git.Git --silent --disable-interactivity --accept-package-agreements --accept-source-agreements" SW_SHOWNORMAL
${HermesLog} "Git: ExecShellWait returned (no exit code available from ShellExecute)"
; ExecShellWait returns no exit code, so verify by checking the file
; system directly. Don't use `where git` — that reads OUR process's
; PATH, which was captured at NSIS startup before Git's installer ran
; and modified the system PATH. Until we restart, the new PATH isn't
; visible to us. Probe Git's standard install locations instead.
StrCpy $0 "0" ; "git found" flag
${If} ${FileExists} "$PROGRAMFILES64\Git\bin\bash.exe"
${HermesLog} "Git: found bash.exe at $PROGRAMFILES64\Git\bin\bash.exe"
StrCpy $0 "1"
${ElseIf} ${FileExists} "$PROGRAMFILES\Git\bin\bash.exe"
${HermesLog} "Git: found bash.exe at $PROGRAMFILES\Git\bin\bash.exe"
StrCpy $0 "1"
${ElseIf} ${FileExists} "$PROGRAMFILES32\Git\bin\bash.exe"
${HermesLog} "Git: found bash.exe at $PROGRAMFILES32\Git\bin\bash.exe"
StrCpy $0 "1"
${ElseIf} ${FileExists} "$LOCALAPPDATA\Programs\Git\bin\bash.exe"
${HermesLog} "Git: found bash.exe at $LOCALAPPDATA\Programs\Git\bin\bash.exe"
StrCpy $0 "1"
${Else}
${HermesLog} "Git: bash.exe NOT found at any standard location"
${EndIf}
${If} $0 == "1"
DetailPrint "Git for Windows installed successfully."
${HermesLog} "Git install succeeded (filesystem probe positive)"
${Else}
DetailPrint "Git for Windows install did not complete (bash.exe not found at standard install locations)."
${HermesLog} "Git install FAILED (filesystem probe negative). User notified via MessageBox."
MessageBox MB_OK|MB_ICONEXCLAMATION|MB_TOPMOST "Git for Windows install via winget did not complete successfully.$\r$\n$\r$\nSee log: $HermesLogPath$\r$\n$\r$\nYou can install Git for Windows manually from https://git-scm.com/download/win after Hermes setup finishes. Hermes' terminal tool will not work until Git Bash is available."
${EndIf}
${EndIf}
hermes_prereq_install_done:
${HermesLog} "=== customInstall: end ==="
; Flush by closing the log handle. NSIS doesn't expose fflush; FileClose
; both flushes and releases the handle. Subsequent macros become no-ops
; because we null out the handle. This is fine — there are no more log
; sites after customInstall in the install path.
${If} $HermesLogHandle != ""
FileClose $HermesLogHandle
StrCpy $HermesLogHandle ""
${EndIf}
!macroend