fix(setup): parse lock and mirror json by structure, not indentation

Two pre-Python readers anchored their awk patterns on the exact leading
whitespace of the machine-written json they parse:

- setup-hermes.sh read artifact-mirror.json with /^  "origin"/ (exact
  two spaces), so any other one-member-per-line layout lost the mirror
  fallback silently: the mirror url resolved empty and fetch_pinned
  reported only the primary url it failed on.
- scripts/install.sh read packages.python.version from pm/lock.json
  with exact 4-space and 6-space anchors; on any other layout the pin
  resolved empty and the install fell back to the hardcoded "3.14".

setup-hermes.sh's own pin() already established the contract for the
same file ("follow object names and braces, not indentation"); both
sites now follow it. The mirror read is a flat key match; the python
pin uses the same brace-tracking reader as pin().

No other site in the tree has the pattern: setup-hermes.ps1 uses
ConvertFrom-Json, nix uses builtins.fromJSON, python readers use
json.loads, and remaining awk users parse command output, not config.

Tests:

- tests/pm/test_setup_lock_format.py now parametrizes the mirror json
  indent (it was fixed at 2, leaving the mirror read unguarded) and
  adds a mirror-fallback E2E: the primary url is a dead port (curl
  exit 7, retriable), so the staged uv must come from the mirror,
  with the mirror json written at 9-space indent. Red on the old
  regex (mirror url resolves empty, exit 1), green with the fix.
- tests/test_install_sh_python_pin_indent.py (new) sources
  install.sh --manifest and proves bootstrap_python resolves the
  pinned version through a fake uv that records its calls, across
  2/4/0/tab/blank-line lock layouts.

Verified via scripts/run_tests.sh: 16 passed, 0 failed on the fix; the
new mirror-fallback test fails against the old parsers (verified by
stashing the two script changes and re-running). Sibling install/setup
tests (test_install_sh_node_deps_workspaces, test_install_stage_frames,
test_install_sh_desktop_stage, pm/test_activate_scripts) all green.
This commit is contained in:
ethernet
2026-09-11 16:33:02 -04:00
parent 6d344be588
commit ad231c164b
4 changed files with 151 additions and 8 deletions

View File

@@ -329,9 +329,13 @@ stage_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)"
# Read packages.python.version by following object names and braces, not
# indentation — same pre-Python reader contract as setup-hermes.sh's pin().
_py="$(awk -F '"' '
/^[[:space:]]*("[^"]+"[[:space:]]*:[[:space:]]*)?\{/ { path[++depth] = $2; next }
/^[[:space:]]*\}[[:space:]]*,?[[:space:]]*$/ { delete path[depth--]; next }
path[2] == "packages" && path[3] == "python" && $2 == "version" && depth == 3 { print $4; exit }
' "$INSTALL_DIR/pm/lock.json" | cut -d+ -f1 | cut -d. -f1,2)"
[ -n "$_py" ] || _py="3.14"
"$UV_CMD" python install --no-bin "$_py" || fail "bootstrap Python installation failed"
boot_py="$("$UV_CMD" python find --managed-python "$_py")" || fail "bootstrap Python lookup failed"

View File

@@ -51,9 +51,10 @@ echo -e "${CYAN}→${NC} Checking for uv..."
lock="$SCRIPT_DIR/pm/lock.json"
[ -f "$lock" ] || { echo -e "${RED}✗${NC} pm/lock.json not found" >&2; exit 1; }
# Read the shared mirror location before Python is available.
mirror_origin="$(awk -F '"' '/^ "origin"/ { print $4; exit }' "$SCRIPT_DIR/pm/artifact-mirror.json")"
mirror_prefix="$(awk -F '"' '/^ "prefix"/ { print $4; exit }' "$SCRIPT_DIR/pm/artifact-mirror.json")"
# Read the shared mirror location before Python is available. Match object
# keys, not indentation — same rule as pin() below.
mirror_origin="$(awk -F '"' '$2 == "origin" { print $4; exit }' "$SCRIPT_DIR/pm/artifact-mirror.json")"
mirror_prefix="$(awk -F '"' '$2 == "prefix" { print $4; exit }' "$SCRIPT_DIR/pm/artifact-mirror.json")"
mirror_url_for() { # $1 = lowercase sha256
[ -n "$mirror_origin" ] && [ -n "$mirror_prefix" ] || return 1
printf '%s/%s%s' "$mirror_origin" "$mirror_prefix" "$1"

View File

@@ -25,7 +25,8 @@ REPO = Path(__file__).resolve().parents[2]
@pytest.mark.parametrize("indent,blank_lines", [(2, False), (4, False), (0, False), ("\t", False), (4, True)],
ids=["two-spaces", "four-spaces", "no-indent", "tabs", "blank-lines"])
def test_setup_reads_pins_independent_of_indentation(tmp_path, served, indent, blank_lines):
@pytest.mark.parametrize("mirror_indent", [2, 7], ids=["mirror-two-spaces", "mirror-seven-spaces"])
def test_setup_reads_pins_independent_of_indentation(tmp_path, served, indent, blank_lines, mirror_indent):
bash = shutil.which("bash")
assert bash, "the shell bootstrap contract requires Bash"
core = tmp_path / "checkout with spaces"
@@ -72,7 +73,7 @@ def test_setup_reads_pins_independent_of_indentation(tmp_path, served, indent, b
content = content.replace("\n", "\n \t\n")
(core / "pm" / "lock.json").write_text(content + "\n", encoding="utf-8")
(core / "pm" / "artifact-mirror.json").write_text(
json.dumps({"origin": base_url, "prefix": "mirror/"}, indent=2), encoding="utf-8",
json.dumps({"origin": base_url, "prefix": "mirror/"}, indent=mirror_indent), encoding="utf-8",
)
env = {"PATH": os.environ["PATH"], "HOME": str(home),
"HERMES_HOME": str(home / ".hermes"), "HERMES_RUNTIME_DIR": str(runtime),
@@ -93,3 +94,71 @@ def test_setup_reads_pins_independent_of_indentation(tmp_path, served, indent, b
assert not (home / ".hermes" / "skills").exists()
print(f"runtime-only bootstrap: indent={indent!r}, blank_lines={blank_lines}: exit {result.returncode}")
print(result.stdout)
def test_setup_mirror_fallback_reads_any_layout(tmp_path, served):
"""The mirror fallback consumes artifact-mirror.json read pre-Python.
The primary URL is a dead port (curl exit 7, in the retriable set), so the
staged artifact must come from the mirror; the mirror json is written as a
compact single line to prove the reader follows keys, not indentation.
"""
bash = shutil.which("bash")
assert bash, "the shell bootstrap contract requires Bash"
core = tmp_path / "checkout"
(core / "pm").mkdir(parents=True)
shutil.copy2(REPO / "setup-hermes.sh", core / "setup-hermes.sh")
home = tmp_path / "home"
home.mkdir()
runtime = tmp_path / "runtime"
interpreter = str(Path(sys._base_executable).resolve())
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
uv_version = "fixture-pin"
receipt = core / "handoff.json"
(core / "pm" / "__init__.py").touch()
(core / "pm" / "cli.py").write_text(
"import json, pathlib, sys\n"
"assert sys.argv[1:] == ['install'], sys.argv\n"
f"pathlib.Path({str(receipt)!r}).write_text(json.dumps(sys.argv[1:]))\n",
encoding="utf-8",
)
uv_script = (
f"#!{bash}\nset -eu\n"
f'case "$*" in\n'
f' --version) printf \'%s\\n\' "uv {uv_version}" ;;\n'
f' "python install --no-bin {py_version}") ;;\n'
f' "python find --managed-python {py_version}") printf \'%s\\n\' {shlex.quote(interpreter)} ;;\n'
f' *) exit 91 ;;\n'
f'esac\n'
)
docroot, base_url = served
mirror_dir = docroot / "mirror"
mirror_dir.mkdir()
filename, digest = make_tar(docroot, "uv.tar.gz", {"uv-fixture/uv": uv_script})
shutil.copy2(docroot / filename, mirror_dir / digest)
# Dead port: curl cannot connect (exit 7), which fetch_pinned treats as
# retriable, forcing the mirror branch.
dead = "http://127.0.0.1:1/uv.tar.gz"
artifact = {"sha256": digest, "url": dead}
target = current_target()
data = {"packages": {
"python": {"artifacts": {target: {"sha256": "0" * 64, "url": dead}},
"version": f"{py_version}.7+fixture"},
"uv": {"artifacts": {target: artifact}, "version": uv_version},
}}
(core / "pm" / "lock.json").write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
# Hostile indentation (one member per line, 9 spaces): the reader must
# follow keys, not the 2-space layout the real file ships with.
(core / "pm" / "artifact-mirror.json").write_text(
json.dumps({"origin": base_url, "prefix": "mirror/"}, indent=9) + "\n", encoding="utf-8",
)
env = {"PATH": os.environ["PATH"], "HOME": str(home),
"HERMES_HOME": str(home / ".hermes"), "HERMES_RUNTIME_DIR": str(runtime),
"PYTHONNOUSERSITE": "1"}
result = subprocess.run(
[bash, str(core / "setup-hermes.sh"), "--runtime-only"], cwd=tmp_path,
env=env, capture_output=True, text=True, timeout=30,
)
assert result.returncode == 0, result.stdout + result.stderr
assert (runtime / f"uv-{uv_version}-{target}" / "uv").read_text() == uv_script
assert json.loads(receipt.read_text()) == ["install"]

View File

@@ -0,0 +1,69 @@
"""install.sh reads the python pin from pm/lock.json by structure, not layout.
bootstrap_python's pre-Python awk reader must follow object names and braces
(the same contract setup-hermes.sh's pin() established), so any indentation the
lock writer produces resolves the same pinned version.
"""
import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import pytest
ROOT = Path(__file__).resolve().parents[1]
pytestmark = pytest.mark.platforms("posix")
@pytest.mark.parametrize("indent,blank_lines", [(2, False), (4, False), (0, False), ("\t", False), (4, True)],
ids=["two-spaces", "four-spaces", "no-indent", "tabs", "blank-lines"])
def test_bootstrap_python_reads_pin_independent_of_indentation(tmp_path, indent, blank_lines):
bash = shutil.which("bash")
assert bash, "the shell bootstrap contract requires Bash"
core = tmp_path / "checkout"
(core / "pm").mkdir(parents=True)
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
interpreter = str(Path(sys._base_executable).resolve())
calls = tmp_path / "uv-calls"
uv = tmp_path / "uv"
uv.write_text(
f"#!{bash}\nset -eu\n"
f"printf '%s\\n' \"$*\" >> {shlex.quote(str(calls))}\n"
'case "$*" in\n'
f' "python install --no-bin {py_version}") ;;\n'
f' "python find --managed-python {py_version}") printf \'%s\\n\' {shlex.quote(interpreter)} ;;\n'
' *) exit 91 ;;\n'
'esac\n',
encoding="utf-8",
)
uv.chmod(0o755)
decoy = {"sha256": "0" * 64, "url": "http://127.0.0.1:1/wrong.tar.gz"}
data = {"packages": {
"before": {"artifacts": {"any": decoy}, "version": "wrong-before"},
"python": {"artifacts": {"any": decoy}, "version": f"{py_version}.7+fixture"},
"after": {"artifacts": {"any": decoy}, "version": "wrong-after"},
}}
content = json.dumps(data, indent=indent)
if blank_lines:
content = content.replace("\n", "\n \t\n")
(core / "pm" / "lock.json").write_text(content + "\n", encoding="utf-8")
script = (
f'source "{(ROOT / "scripts/install.sh").as_posix()}" --manifest\n'
f'ensure_uv() {{ UV_CMD="{uv.as_posix()}"; }}\n'
f'INSTALL_DIR="{core.as_posix()}"\n'
"bootstrap_python\n"
)
env = dict(os.environ, HOME=str(tmp_path), HERMES_HOME=str(tmp_path / ".hermes"))
result = subprocess.run(["bash", "-c", script], env=env, capture_output=True, text=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr
# A layout-sensitive reader resolves no version, falls back to "3.14", and
# the fake uv exits 91 on the unexpected argument — so the recorded calls
# pinning THIS interpreter's version are the contract.
assert calls.read_text().splitlines() == [
f"python install --no-bin {py_version}",
f"python find --managed-python {py_version}",
]