install.sh --skip-browser / --no-playwright and install.ps1 -SkipBrowser
now hand PM `--without agent-browser` again instead of `--without chromium`.
The downloaded installer is the newest script, but by default it installs
the stable tag, whose PM (v0.22.5) rejects `--without chromium` with
"--without accepts only optional default packages (agent-browser,
cua-driver)" and exits 1, failing the whole install. The new PM already
reads `agent-browser` as `chromium` (pm.defaults.RETIRED_DEFAULTS), so the
legacy spelling works against both the stable tag and main. Direct
`hermes pm install --without chromium` stays valid on the new PM.
- pm: AgentBrowser class + lock entry gone; Chromium default=True and its env
exports only PLAYWRIGHT_BROWSERS_PATH (Playwright launches without an
executable path still need it). A recorded or passed --without agent-browser
maps to chromium (pm.defaults.RETIRED_DEFAULTS), and pm gc forgets the stale
agent-browser fact so its store entry is reclaimed.
- install.sh --skip-browser/--no-playwright and install.ps1 -SkipBrowser pass
--without chromium; undo hint is hermes pm install chromium.
- Docker bakes /etc/hermes/chromium-executable-path; stage2-hook validates it
and exports nothing; the sandbox-desktop image drops the agent-browser npm
install and keeps its Playwright Chromium.
When a checkout's .git exists but git rejects it (empty HEAD, a gitdir:
file pointing nowhere), every `hermes update` failed at the fetch with a
generic "Failed to fetch updates" and, on Windows, left the paused
gateways stopped with "the next `hermes` command restarts them". No
later command could: the whole-tree gate held the set while HEAD was
unreadable, so bots stayed down until someone repaired the checkout by
hand.
- tree gate: a set whose every pause already found HEAD unreadable and
that never bound a tree move resumes. The gateways were serving that
exact tree and no update step wrote it. A recorded move, or a HEAD
that was readable at the pause, still holds the set.
- fetch failure: "not a git repository" now says the checkout's git data
is damaged and prints the installer one-liner that repairs it.
- install.sh: a dangling `.git` file is moved aside and re-cloned like a
commit-less one, matching install.ps1 (Test-Path already matched it);
both installers' warning names a damaged .git, not only an
interrupted clone.
stage_prerequisites sent every Mac whose git_runs failed into
install_macos_git, and git_runs is also false when git is missing from
PATH. So a Mac with no git got the Command Line Tools message instead of
"git is required", contradicting the "no git on PATH: unchanged" row and
failing test_real_install_sh_ladder_leaves_only_closed_tokens on macOS.
Gate the macOS path on git being on PATH.
That ladder test was gated with a bare skipif, so the macOS lane never
imported it; give it platforms("posix") per tests/AGENTS.md.
Also from review:
- Check the developer tools' git before Homebrew, so a git that fails
with the tools present (unaccepted Xcode license, broken ~/.gitconfig)
reports git's own error instead of running brew install git first.
- Log why xcode-select --install failed (e.g. no GUI session over SSH)
before the wait, instead of discarding its output.
- Parametrize the dialog-then-continue and fail-fast cases.
A macOS upgrade can leave /Library/Developer/CommandLineTools behind with its contents gone; xcode-select -p still names it, so the folder-exists check skipped the install dialog for the most common broken-git Mac. Require $dev/usr/bin/git instead, which both the CLT folder and Xcode's Contents/Developer hold.
When xcode-select --install refuses over such a leftover folder, no dialog opens: fail at once with the reinstall remedy instead of waiting 15 minutes.
When xcode-select -p names an existing developer directory (e.g. Xcode installed, license never accepted), no install dialog will open: skip the dialog and the wait and fail with git's own error instead of waiting 15 minutes and suggesting the wrong fix.
Hermes-Setup drives stages with --non-interactive --json; it has no stage timeout, a Cancel button, and streams log lines as progress, so wait for the Command Line Tools there too. Only a plain headless --non-interactive run fails fast.
Run Homebrew with HOMEBREW_NO_AUTO_UPDATE=1 (auto-update runs the broken git and can take minutes) through run_logged instead of discarding its output.
Move the prerequisite tests to tests/scripts/install/ and drop the uname stub: macOS cases are platforms("macos"), the Linux case platforms("linux"). The probe PATH holds only system dirs so a host Homebrew never runs.
install.ps1 clones into %LOCALAPPDATA%\hermes\.hermes-clone-<pid>-<random>\tree
before publishing, which puts the deepest tracked file (a 159-character zh-Hans
docs path) past MAX_PATH once LOCALAPPDATA is longer than ~55 characters. Git
for Windows then refuses the file ("Filename too long"), every clone attempt
and the deferred checkout fail the same way, and the user sees "fatal: unable to
checkout working tree" from both the PowerShell one-liner and Hermes-Setup.
Clone with --config core.longpaths=true so the setting lands before the first
checkout and stays in the repo for `hermes update`; an installer rerun over an
existing checkout sets it too.
Salvages the long-path half of #72744 by @Imagineer99, written against the
pre-rewrite installer.
Co-authored-by: Lee Jackson <samleejackson0@gmail.com>
A failed clone, fetch, checkout or reset reported only the step ("git clone
failed; no checkout published"). Hermes-Setup shows the stage reason as-is, so a
user behind a proxy or a DNS block saw no cause and every report needed a
round-trip for bootstrap-installer.log.
run_logged / Invoke-Logged now keep the failed command's first fatal:/error:
line (else its last line, capped at 240 chars) and the git Fail sites append it:
"git clone failed; no checkout published: fatal: unable to access ...: Could not
resolve host: ...". Both output modes are covered: redirected (the Hermes-Setup
driver, CI) and the interactive status line. stderr still streams as before.
Script-only: the website Hermes-Setup.exe downloads install.ps1 / install.sh
from main on every run and on Retry, so copies already in the field show it.
With --no-checkout the file download moved out of the retried clone into the single unretried pin checkout, which ran after the empty clone was already published: a throttled pin left INSTALL_DIR with no index, and the next rerun stashed a 'delete every file' autostash. The pin now runs in the staged tree inside the deferred-checkout retry loop (which also stops materializing the tip first), and nothing is published until it succeeds.
Mirrors the install.sh change from #135092 on Windows. With -Commit (what
the desktop bootstrap passes), the fresh clone runs with --no-checkout and
the pin is checked out with retries inside the staging dir, so the branch
tip is never materialized first and a refused or failed pin never publishes
a checkout. A rerun over an existing checkout keeps pinning after the fetch.
The bare origin had no main HEAD, so the clone had no commits and stage_repository
re-cloned it as an interrupted install; a plain-path clone also ignores --filter.
The test passed on the unfixed installer. Use -b main and a file:// URL, and assert
the checkout was converted in place.
Installers have cloned with --filter=tree:0 since c3e9ac87a7. A treeless
clone keeps commits only, and git requests a missing tree without telling
the server which trees it already has, so every checkout and every
path-filtered history walk re-downloads complete directory snapshots
(~0.58 MB per commit for this repo). GitHub's own guidance is that
treeless clones are for throwaway CI builds, not long-lived checkouts;
the blobless layout keeps every commit and tree local and fetches only
file contents on demand.
- install.sh / install.ps1 clone --filter=blob:none (same release tags,
--commit pins and ancestor checks the treeless switch was made for).
- hermes update (post-update step) converts an existing tree:0 checkout once with
`git fetch --refetch --filter=blob:none origin` (~120 MB, ~8 s
from GitHub) and records the new filter only after it succeeds, so a
failed refetch retries next update.
- The shallow-heal conversion unshallows as blob:none instead of tree:0.
- checkout_contains asks git with GIT_NO_LAZY_FETCH: a recorded SHA the
checkout lacks is "not contained", never a download of that commit's
whole history.
A login bash reads only the first of ~/.bash_profile, ~/.bash_login and
~/.profile. wire_shell_path always wrote ~/.profile and ~/.bash_profile
when present, so a user whose only login file is ~/.bash_login never saw
~/.local/bin on PATH. Treat ~/.bash_login like ~/.bash_profile: append the
guarded PATH line when the file exists, never create it (creating
~/.bash_profile would shadow it).
The test helper resolves bash by absolute path so the login-PATH tests run
on hosts without /bin/bash (NixOS).
test_install_ps1_json_failure_frame runs a full `install.ps1 -Stage repository
-Json` under Windows PowerShell with a 30 s timeout. On the windows-arm64 lane
that run takes 25.0 s when green and 68.8 s when slow, so it times out
intermittently (twice on PR #130298, a gateway-only change). The same file
invocation in test_install_ps1_staged_git uses 240 s; use that budget here and
for staged_git's dot-sourced Ensure-Git probe, which parses the same script under
the same 30 s limit.
Also read the preserved user file with utf-8-sig, the BOM-tolerant read the
Windows-footgun lint requires for this file.
An install whose updater dies on the git 2.53+ pack-objects BUG (#124272)
never fetches the release that heals it: the fetch runs in the installed
code. The installer rerun (install.sh / install.ps1, served fresh) is the
path that runs new code, so when origin is a promisor remote it now marks
the checkout's unmarked packs before its existing-checkout fetch. Marking is
idempotent and never rewrites objects, so it runs up front rather than as a
retry that would first print a failed-fetch report.
Maintainer ruling: only Hermes and only its packaged package managers
(uv/pip/node/npm) are ever used; no PATH fallback when the managed tool is
missing, no "prefer the user's if new enough".
- hermes_constants.find_node_executable: node/npm/npx resolve to PM's
installed copy or None. Every caller already pm.ensure()s on None, so a
missing runtime is now provisioned instead of silently borrowing the
user's Node (native-addon ABI / npm cache mismatches).
- agent/lsp/install._install_npm: pm.ensure('npm') when PM npm is absent,
instead of failing over to whatever npm is on PATH.
- gateway._append_node_dir_for_service: stop baking the invoker's PATH node
dir into generated systemd/launchd units.
- main_install_repair._resolve_node_runtime_npm: drop the PATH re-scan for
another npm.
- source_build.source_product_current: run the freshness reader only with
PM's node.
- doctor: Node/npm rows and npm audit use PM's copies (Termux APT distro
keeps its system Node).
- install.sh ensure_uv / install.ps1 Get-Uv: always stage the pinned uv
artifact; delete the "uv on PATH if new enough" developer shortcut.
Get-PinnedGit no longer invokes tar at all (no $inboxTar seam exists since
the PortableGit pin), so the C# stub tar.exe and the Invoke-Native override
that bound it were never reached: the test compiled a stub for nothing and
its docstring described a mechanism the installer does not have. Keep the
real-download Get-PinnedGit run under a bzip2-less PATH and the git.exe +
bash.exe contract checks; that is what the test proves.
Windows 10 boxes whose System32 tar.exe cannot run the bzip2 filter die
at stage=prerequisites with "unable to run program bzip2 -d" while
extracting the pinned Git-2.53.0.3 tar.bz2 (#122512). Repin git for
both win32 targets to git-for-windows' PortableGit self-extracting 7z,
which carries its own extractor and the bundled usr/bin/bash.exe:
- pm/lock.json: new artifact urls + sha256 (the pin authority)
- scripts/install.ps1: generated fragment regenerated; Get-PinnedGit
downloads the SFX and waits on it explicitly (the stub is a
GUI-subsystem exe, so PowerShell's & does not wait); the System32
tar invocation and its msys symlink excludes go away
- pm/packages.py: Git.fetch_url/Git.unpack run the self-extractor
after the sha256-verified download
- pm/store.py: drop the now-callerless git_msys branch of extract_tar
- tests: RED->GREEN test runs the real Get-PinnedGit against a
bzip2-less System32 tar.exe stub with no bzip2 on PATH; the three
obsolete tar-contract tests and the install.ps1/PM msys-links parity
test are replaced by a no-external-decompressor contract test
Closes#122512
(cherry picked from commit 4915304213495d3207ec6cd659e57cd16ef08d60)
Address review on #125371: the binding test passed the script-level
-NonInteractive alongside the alias, so deleting the fold line kept it
green. The new test runs -Stage setup -Json with only -SkipSetup and
asserts the skipped/needs-user-input frame, which only the fold can
produce (without it the stage dispatches interactively and fails under
a non-interactive host). Also document the deprecated alias in the
zh-Hans windows-native guide, which still said -SkipSetup is rejected.
(cherry picked from commit b97fc4561af918910b8b2b9bf728e501b89aeb27)
- tests/scripts/install/test_install_ps1_skipsetup_alias.py: Windows-lane
test driving the real installer with -SkipSetup -ShowResolvedPaths —
the side-effect-free contract that still exercises parameter binding.
Red on main (NamedParameterNotFound), green with the alias restored.
- tests/pm/test_ffmpeg_pin_liveness.py: walks every BtbN URL in the
lockfile against the live upstream (HEAD); catches the next pin rot
before a fresh install 404s. Skips without network.
(cherry picked from commit 736b7ff4c1; ffmpeg lock/liveness hunks dropped, superseded by #125468)
Keep one PM invariant (a musl ELF userland resolves to linux-<arch>-musl
and the default closure is satisfiable with musl-native artifacts) and
one installer invariant (uv_bootstrap_target picks musl uv from a musl
ELF interpreter even when ldd says glibc). Drop the lock-content and
URL-shape change-detectors.
Fall back to the musl loader if ldd cannot identify libc, while honoring
an explicit GNU libc report on hosts with a secondary musl toolchain.
Cover both paths against the pinned uv URL and digest.
Refs: #123682
Skip glibc-linked FFmpeg on musl in both default install and update roots.
Select musl uv during the standalone shell bootstrap, and let the native
userland resolve libc before bootstrap Python build metadata.
Add regression checks for the closure, target precedence, and installer pins.
wire_shell_path's existing-setup regex required a character before PATH=, so it missed bare assignments such as Fedora's ~/.bashrc ( PATH="$HOME/.local/bin:$HOME/bin:$PATH") and Debian's ~/.profile. The installer then appended its own line to .bashrc, .profile and .bash_profile, and because Fedora's .bash_profile sources .bashrc, login shells got ~/.local/bin on PATH several times.
Match bare assignments too, and make the appended line a no-op when PATH already contains ~/.local/bin.
electron-builder names the unpacked output <os>-unpacked on x64 and
<os>-<arch>-unpacked elsewhere (linux-arm64-unpacked, win-arm64-unpacked).
install.sh desktop_product_present and install.ps1
Test-DesktopProductPresent only listed the x64 names, so a rerun on an
ARM64 desktop install skipped the desktop rebuild and left a bundle built
from the previous code. List every unpacked dir main_desktop.py already
resolves, in both installers.
The flag used to exit 1 as retired. Now that PM installs the browser tools
by default, it maps to `pm.cli install --without agent-browser`, which later
installs and `hermes update` honour.
The pm-era installer printed "[hermes]" lines between raw git, uv and pm
output. Restore the pre-pm installer's look (→ ✓ ⚠ ✗, banner on the full
ladder) and route every child command through run_logged: on a terminal it
shows one status line rewritten with the command's newest output line
(git clone phases via --progress), appends everything to
$HERMES_HOME/logs/install.log, and on failure prints the last 20 lines plus
the log path. CI, --verbose, HERMES_INSTALL_VERBOSE and a non-terminal
stdout (the Hermes-Setup --json driver, E2E transcripts) keep the full
stream, so their parsers see what they saw before. Errors stay on stderr.
`irm | iex` runs install.ps1 as text, which execution policy never
checks, but Invoke-InstalledHermes then dot-sourced runtime.ps1 from
disk. That is a file load, and the default Restricted policy (Windows
Sandbox, fresh machines) refused it right after "hermes command
installed". Load the helper from its text instead.
That failure hid a second one on the same path: the `$command` local
was shadowed inside Invoke-Native by its case-insensitive `$Command`
parameter, so `& $command[0]` invoked the scriptblock itself until the
call depth overflowed. Rename the local.
The documented one-liner, iex (irm .../install.ps1), runs the installer
inside the user's own session. Fail ended with exit 1, so any failed
stage closed the user's PowerShell window.
Fail now throws. The two entry points own reporting and the exit code:
-Stage prints the reason, emits the -Json frame and exits 1, as before;
the full install exits 1 only when it runs from a script file, and under
iex it prints the reason, sets LASTEXITCODE=1 and returns. A scriptblock
literal's File tells the two apart: $MyInvocation.MyCommand.Path names the
caller's script under iex.
PM launchers run python -I, which ignores PYTHONPATH, so the macOS
hermes-desktop-app-update route never loaded the sitecustomize capture hook.
Route PM launchers through pm-launch.py as the Linux driver already does, and
cover the packaged .app shape the macOS route launches.