diff --git a/setup-hermes.sh b/setup-hermes.sh index f9b36a8cd0..969b82bef9 100755 --- a/setup-hermes.sh +++ b/setup-hermes.sh @@ -52,7 +52,7 @@ esac if [ "$os" = win32 ]; then # PROCESSOR_ARCHITECTURE lies under an emulated shell (x64 msys on a # WoA box reports AMD64); the registry carries the machine's truth. - winarch="$(reg.exe query 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' /v PROCESSOR_ARCHITECTURE 2>/dev/null | tr -d '\r' | awk '/PROCESSOR_ARCHITECTURE/ {print $NF}')" + winarch="$(MSYS2_ARG_CONV_EXCL='*' reg.exe query 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' /v PROCESSOR_ARCHITECTURE 2>/dev/null | tr -d '\r' | awk '/PROCESSOR_ARCHITECTURE/ {print $NF}')" case "${winarch:-${PROCESSOR_ARCHITECTURE:-}}" in ARM64) arch=arm64 ;; *) arch=x64 ;; @@ -69,19 +69,20 @@ target="$os-$arch" # lock.json is machine-written (sorted keys, 2-space indent): read the uv # pin's version + this target's url/sha256 with awk — no python yet. pin() { # $1 = field (url | sha256) - awk -v target="$target" -v field="$1" ' + awk -F '"' -v target="$target" -v field="$1" ' /^ "uv": \{/ { in_uv = 1 } - in_uv && $0 ~ "^ \"" target "\": \\{" { in_t = 1 } - in_t && $0 ~ "^ \"" field "\":" { - gsub(/.*: "|,?$/, ""); print; exit - }' "$lock" + in_uv && /^ }/ { exit } + in_uv && /^ "/ { in_t = ($2 == target) } + in_t && $2 == field { print $4; exit }' "$lock" } -uv_version="$(awk ' +uv_version="$(awk -F '"' ' /^ "uv": \{/ { in_uv = 1 } - in_uv && /^ "version":/ { gsub(/.*: "|,?$/, ""); print; exit }' "$lock")" -py_version="$(awk ' + in_uv && /^ }/ { exit } + in_uv && $2 == "version" { print $4; exit }' "$lock")" +py_version="$(awk -F '"' ' /^ "python": \{/ { in_py = 1 } - in_py && /^ "version":/ { gsub(/.*: "|"$|",$/, ""); print; exit }' "$lock" \ + in_py && /^ }/ { exit } + in_py && $2 == "version" { print $4; exit }' "$lock" \ | cut -d+ -f1 | cut -d. -f1,2)" [ -n "$uv_version" ] || { echo -e "${RED}✗${NC} no uv pin in pm/lock.json" >&2; exit 1; } diff --git a/tests/hermes_cli/test_setup_pin_values.py b/tests/hermes_cli/test_setup_pin_values.py new file mode 100644 index 0000000000..75c91c26e7 --- /dev/null +++ b/tests/hermes_cli/test_setup_pin_values.py @@ -0,0 +1,58 @@ +"""Cold setup consumes lock values before executing any downloaded tool.""" +import json +import os +from pathlib import Path +import shutil +import subprocess + +import pytest + +from pm.store import current_target + + +ROOT = Path(__file__).resolve().parents[2] + + +@pytest.mark.platforms("windows", "posix") +@pytest.mark.parametrize("missing_target", [False, True]) +def test_cold_setup_uses_exact_lock_values(tmp_path, missing_target): + checkout = tmp_path / "checkout" + (checkout / "pm").mkdir(parents=True) + shutil.copy2(ROOT / "setup-hermes.sh", checkout / "setup-hermes.sh") + lock = json.loads((ROOT / "pm" / "lock.json").read_text(encoding="utf-8")) + target = current_target() + uv_pin = lock["packages"]["uv"] + artifact = uv_pin["artifacts"][target] + if missing_target: + del uv_pin["artifacts"][target] + (checkout / "pm" / "lock.json").write_text(json.dumps(lock, indent=2, sort_keys=True) + "\n", encoding="utf-8") + corrupt = tmp_path / "corrupt-download" + corrupt.write_bytes(b"intentional digest mismatch; this must never be executed\n") + args = tmp_path / "curl-args" + hook = tmp_path / "transport.sh" + hook.write_text('''curl() { + printf '%s\\n' "$@" > "$PROBE_ARGS" + while [ "$#" -gt 0 ]; do + if [ "$1" = -o ]; then + cp "$PROBE_DOWNLOAD" "$2" + return + fi + shift + done + return 37 +} +''', encoding="utf-8") + env = dict(os.environ, HOME=tmp_path.as_posix(), HERMES_HOME=(tmp_path / "home").as_posix(), + HERMES_RUNTIME_DIR=(tmp_path / "tools").as_posix(), BASH_ENV=hook.as_posix(), + PROBE_ARGS=args.as_posix(), PROBE_DOWNLOAD=corrupt.as_posix()) + result = subprocess.run(["bash", str(checkout / "setup-hermes.sh")], cwd=tmp_path, + env=env, capture_output=True, text=True, encoding="utf-8", timeout=30) + assert result.returncode != 0, result.stdout + result.stderr + if missing_target: + assert not args.exists(), "a missing target must not select a sibling artifact" + assert f"no uv artifact for {target}" in result.stderr + else: + assert args.read_text(encoding="utf-8").splitlines()[-1] == artifact["url"] + assert f"pinned uv {uv_pin['version']} ({target})" in result.stdout + assert f"pinned {artifact['sha256']})" in result.stderr + assert not (checkout / ".env").exists()