fix(install): publish source launchers through one writer
Setup searched for console scripts in a checkout-local venv that PM no longer creates. Publish both commands through the shared writer after PM setup, using store Python rather than a dependency interpreter. Native and shell launchers load the selected dependency generation at boot, retain the custom-home default, and ignore foreign Python paths. Both installer stages reuse their bootstrap interpreter for publication. PowerShell passes the resolved home to child processes. Verified: 57 focused tests passed with one POSIX host skip. Real Windows launchers, generated shell scripts, and both stage callers ran against temporary trees. No full cold dependency install or native POSIX install was run. The user-PATH edge is stubbed in the PowerShell test.
This commit is contained in:
@@ -299,10 +299,9 @@ function Initialize-ResolvedPaths {
|
||||
if ($script:BoundParams.ContainsKey('InstallDir')) {
|
||||
$script:InstallDir = ConvertTo-LongPath $script:InstallDir
|
||||
} else {
|
||||
$script:InstallDir = ConvertTo-LongPath $(
|
||||
if ($env:HERMES_HOME) { "$env:HERMES_HOME\hermes-agent" } else { "$env:LOCALAPPDATA\hermes\hermes-agent" }
|
||||
)
|
||||
$script:InstallDir = Join-Path $script:HermesHome 'hermes-agent'
|
||||
}
|
||||
$env:HERMES_HOME = $script:HermesHome
|
||||
if ($script:NormalizedProfilePaths) {
|
||||
Write-PathDiag "resolved install paths: HermesHome=$script:HermesHome InstallDir=$script:InstallDir"
|
||||
}
|
||||
@@ -501,20 +500,25 @@ function Stage-Venv {
|
||||
# tool store — all hash-verified against pm/lock.json + uv.lock. install.ps1
|
||||
# no longer runs `uv sync` directly; pm is the single install authority
|
||||
# (the run_locked_uv_sync contract moved into pm/packages.py::uv_env).
|
||||
function Invoke-BootstrapPm {
|
||||
function Get-BootstrapPython {
|
||||
$uv = Get-Uv
|
||||
$lock = Get-Content (Join-Path $InstallDir "pm\lock.json") -Raw | ConvertFrom-Json
|
||||
$pyPin = $lock.packages.python
|
||||
$pyVersion = if ($pyPin) { ($pyPin.version -split '\+')[0] -replace '^(\d+\.\d+).*', '$1' } else { '3.14' }
|
||||
& $uv python install --no-bin $pyVersion | Out-Host
|
||||
if ($LASTEXITCODE) { Fail "bootstrap Python installation failed" }
|
||||
$bootPy = (& $uv python find --managed-python --no-project $pyVersion) -join "`n"
|
||||
if ($LASTEXITCODE -or -not $bootPy) { Fail "bootstrap Python lookup failed" }
|
||||
return $bootPy.Trim()
|
||||
}
|
||||
|
||||
function Invoke-BootstrapPm {
|
||||
$bootPy = Get-BootstrapPython
|
||||
Log "delegating python + venv + tools to pm (hash-verified via uv.lock)"
|
||||
Push-Location $InstallDir
|
||||
try {
|
||||
# Finish bootstrap uv before PM replaces or cleans its store entry.
|
||||
& $uv python install --no-bin $pyVersion
|
||||
if ($LASTEXITCODE) { Fail "bootstrap Python installation failed" }
|
||||
$bootPy = (& $uv python find --managed-python $pyVersion) -join "`n"
|
||||
if ($LASTEXITCODE -or -not $bootPy) { Fail "bootstrap Python lookup failed" }
|
||||
& $bootPy.Trim() -m pm.cli install
|
||||
& $bootPy -m pm.cli install
|
||||
if ($LASTEXITCODE) { Fail "pm install failed" }
|
||||
} finally {
|
||||
Pop-Location
|
||||
@@ -531,28 +535,25 @@ function Stage-NodeDeps {
|
||||
|
||||
function Stage-Path {
|
||||
$binDir = Join-Path $HermesHome "bin"
|
||||
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
|
||||
# Mint the boot launchers (hermes / hermes-acp) bound to the pm STORE
|
||||
# python with PYTHONPATH=repo;venv-site-packages — never the venv
|
||||
# python (no boot through the venv; pyvenv.cfg is inert dead config).
|
||||
# The venv python below is install-time machinery (the materializer),
|
||||
# not a boot path. On a fresh install the store interpreter does not
|
||||
# exist yet, so a runtime-resolving .cmd delegator is staged;
|
||||
# hermes_cli/_install_repair.py upgrades it to an exe once
|
||||
# `hermes pm install` materializes the store.
|
||||
$venvPython = Join-Path $InstallDir "venv\Scripts\python.exe"
|
||||
if (-not (Test-Path $venvPython)) { Fail "venv python missing at $venvPython" }
|
||||
$bootPy = Get-BootstrapPython
|
||||
Push-Location $InstallDir
|
||||
& $venvPython -c "from hermes_cli._launchers import ensure_install_launchers; import sys; written = ensure_install_launchers(r'$InstallDir', r'$binDir'); print(';'.join(written)); sys.exit(0 if written else 1)"
|
||||
$code = $LASTEXITCODE
|
||||
Pop-Location
|
||||
try {
|
||||
& $bootPy -I -X utf8 hermes_cli/_launchers.py $binDir
|
||||
$code = $LASTEXITCODE
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
if ($code) { Fail "launcher staging failed" }
|
||||
Set-LauncherUserPath $binDir
|
||||
Log "hermes command installed at $binDir"
|
||||
}
|
||||
|
||||
function Set-LauncherUserPath([string]$binDir) {
|
||||
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||
if ($userPath -notlike "*$binDir*") {
|
||||
[Environment]::SetEnvironmentVariable("Path", "$binDir;$userPath", "User")
|
||||
Log "added $binDir to your user PATH (new shells pick it up)"
|
||||
}
|
||||
Log "hermes command installed at $binDir"
|
||||
}
|
||||
|
||||
function Stage-Config {
|
||||
|
||||
@@ -307,21 +307,24 @@ stage_venv() {
|
||||
(cd "$INSTALL_DIR" && "$UV_CMD" venv --allow-existing venv) || fail "uv venv failed"
|
||||
}
|
||||
|
||||
# uv installs and locates bootstrap Python, then exits before PM starts.
|
||||
# PM owns the final interpreter, tool store, and selected dependency generation.
|
||||
bootstrap_pm() {
|
||||
# Resolve the bootstrap interpreter without assuming a checkout-local venv.
|
||||
bootstrap_python() {
|
||||
ensure_uv
|
||||
local _py
|
||||
_py="$(awk '/^ "python": \{/ { in_py = 1 }
|
||||
in_py && /^ "version":/ { gsub(/.*: "|"$|",$/, ""); print; exit }' \
|
||||
"$INSTALL_DIR/pm/lock.json" | cut -d+ -f1 | cut -d. -f1,2)"
|
||||
[ -n "$_py" ] || _py="3.14"
|
||||
log "delegating python + venv + tools to pm (hash-verified via uv.lock)"
|
||||
# Finish bootstrap uv before PM replaces or cleans its store entry.
|
||||
"$UV_CMD" python install --no-bin "$_py" || fail "bootstrap Python installation failed"
|
||||
local boot_py
|
||||
boot_py="$("$UV_CMD" python find --managed-python "$_py")" || fail "bootstrap Python lookup failed"
|
||||
boot_py="${boot_py%$'\r'}"
|
||||
}
|
||||
|
||||
# uv exits before PM can replace its tool entry.
|
||||
bootstrap_pm() {
|
||||
local boot_py
|
||||
bootstrap_python
|
||||
log "delegating python + venv + tools to pm (hash-verified via uv.lock)"
|
||||
(cd "$INSTALL_DIR" && "$boot_py" -m pm.cli install) || fail "pm install failed"
|
||||
}
|
||||
|
||||
@@ -337,49 +340,9 @@ stage_node_deps() {
|
||||
|
||||
stage_path() {
|
||||
local link_dir="$HOME/.local/bin"
|
||||
mkdir -p "$link_dir"
|
||||
rm -f "$link_dir/hermes"
|
||||
# Boot wrapper: exec the pm STORE python with PYTHONPATH=repo:venv-
|
||||
# site-packages (repo first). Never boots through venv/bin/python —
|
||||
# the venv is only a uv sync target and pyvenv.cfg is inert dead
|
||||
# config (pm work item 3). Store root + python entry mirror
|
||||
# pm/paths.py / pm facts.json; resolved at BOOT so the wrapper picks
|
||||
# up `hermes pm install` whenever it materializes the store.
|
||||
cat > "$link_dir/hermes" <<WRAPPER
|
||||
#!/usr/bin/env bash
|
||||
# Hermes boot wrapper: store python + PYTHONPATH (never the venv python).
|
||||
set -u
|
||||
repo="\${HERMES_INSTALL_DIR:-$INSTALL_DIR}"
|
||||
unset PYTHONHOME
|
||||
runtime="\${HERMES_RUNTIME_DIR:-}"
|
||||
if [ -z "\$runtime" ] && [ -f "\$repo/install-stamp.json" ]; then
|
||||
runtime="\$(sed -n 's/.*"runtimeDir"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "\$repo/install-stamp.json" | head -n 1)"
|
||||
fi
|
||||
[ -n "\$runtime" ] || runtime="\$HOME/.hermes/tools"
|
||||
store_py=""
|
||||
if [ -f "\$runtime/facts.json" ]; then
|
||||
entry="\$(sed -n 's/.*"entry"[[:space:]]*:[[:space:]]*"\(python-[^"]*\)".*/\1/p' "\$runtime/facts.json" | head -n 1)"
|
||||
if [ -n "\$entry" ] && [ -x "\$runtime/\$entry/bin/python3" ]; then
|
||||
store_py="\$runtime/\$entry/bin/python3"
|
||||
fi
|
||||
fi
|
||||
if [ -z "\$store_py" ]; then
|
||||
for d in "\$runtime"/python-*/bin/python3; do
|
||||
[ -x "\$d" ] && store_py="\$d"
|
||||
done
|
||||
fi
|
||||
if [ -z "\$store_py" ]; then
|
||||
echo "hermes: no pm store interpreter under \$runtime - run 'hermes pm install' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
site=""
|
||||
for d in "\$repo"/venv/lib/python3.*/site-packages "\$repo"/.venv/lib/python3.*/site-packages; do
|
||||
[ -d "\$d" ] && site="\$d"
|
||||
done
|
||||
export PYTHONPATH="\$repo\${site:+:\$site}"
|
||||
exec "\$store_py" "\$repo/hermes" "\$@"
|
||||
WRAPPER
|
||||
chmod +x "$link_dir/hermes"
|
||||
local boot_py
|
||||
bootstrap_python
|
||||
(cd "$INSTALL_DIR" && "$boot_py" -I -X utf8 hermes_cli/_launchers.py "$link_dir") || fail "launcher publication failed"
|
||||
case ":$PATH:" in
|
||||
*":$link_dir:"*) : ;;
|
||||
*) log "add $link_dir to your PATH to use the hermes command" ;;
|
||||
|
||||
Reference in New Issue
Block a user