refactor(tooling): T-1286 — generate, pr and dev become reach domains
Twelve scripts retired, three domains registered. `reach` now covers nine. generate: `generate-brands` and `generate-corporations` were the second and third copies of the same 24-line build-if-missing-then-exec bash `tooling/atlas` carried, so they collapsed into `core.process.cargo_binary` rather than being ported. `import_economics` shelled out to the first of those, so it now calls that helper — `generated_brands.toml` comes back byte-identical, and the stamp registry swaps the retired wrapper for `core/process.py`. pr: `watchlist-diff` derives its watched set from `generator_sources.py` instead of restating it, so it cannot drift from the stamp check. dev: the environment scripts split decision from performing, per D-263's guarded-exec rule. `godot_plan()` and `worktree_plan()` decide what would happen; `install_godot()`, `install_rust()` and `setup_worktree()` do it. `tooling/test_environment.py` pins the version pin, both override precedences, the already-current skip, the platform refusal and both worktree refusals — none of them performed. `make setup` now installs reach first, since the targets that install rust and godot are reach verbs. Two live bugs found while porting: - The clerk read its decision index from `decisions/README.md`, a path that stopped existing when the DQR tree moved to `governance/`. Every clerk agent has been grepping blind; its prompt pointed at the same dead directory. - The conformance exec-check matched any `x.system()` regardless of receiver, so `platform.system()` read as `os.system()`. Narrowed and re-proved against a real mutant. `process.run` gains `input=`, `timeout=` and a `ProcessTimeout` subclass so a killed run stays distinguishable from a verdict. The pre-push hook no longer merges the clerk's stderr into its stdout — under streaming the last merged line is a JSONL event, which would read as an unrecognised verdict and block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"""Environment setup — the decisions, separated from the performing (D-263).
|
||||
|
||||
These three ported worst and mattered most to get right. `install-godot`
|
||||
downloads and unzips a pinned build, `install-rust` drives `rustup`,
|
||||
`worktree-setup` manipulates git worktrees — none of which can be exercised in
|
||||
a gate without actually doing it.
|
||||
|
||||
So the shape here is deliberate: **every function that decides is pure and
|
||||
importable, and every function that acts is a thin call through
|
||||
`core.process.run`.** `godot_plan()` answers "what would be downloaded, and is
|
||||
it needed" without touching the network; `install_godot()` performs it. The
|
||||
test of a correct port is whether the decisions can be exercised WITHOUT
|
||||
performing them — a rewrite that cannot be tested has to be trusted instead,
|
||||
and trusting an installer is how a working environment becomes an
|
||||
unreproducible one.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from tooling.core import config, console, process
|
||||
from tooling.core.errors import ReachError
|
||||
|
||||
DEFAULT_GODOT_VERSION = "4.6"
|
||||
INSTALL_DIR = Path.home() / "bin"
|
||||
GODOT_BINARY = INSTALL_DIR / "godot4"
|
||||
|
||||
# Godot publishes one archive per platform triple; an unsupported pair is a
|
||||
# clear failure rather than a download that 404s.
|
||||
PLATFORMS = {
|
||||
("Linux", "x86_64"): "linux.x86_64",
|
||||
("Linux", "aarch64"): "linux.arm64",
|
||||
("Darwin", "x86_64"): "macos.universal",
|
||||
("Darwin", "arm64"): "macos.universal",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GodotPlan:
|
||||
"""What installing Godot would do — decided without doing any of it."""
|
||||
|
||||
wanted: str
|
||||
installed: str | None
|
||||
platform_tag: str
|
||||
url: str
|
||||
filename: str
|
||||
|
||||
@property
|
||||
def already_current(self) -> bool:
|
||||
return self.installed == self.wanted
|
||||
|
||||
|
||||
def godot_plan(wanted: str | None = None) -> GodotPlan:
|
||||
"""Decide what an install would fetch. Pure apart from reading the binary."""
|
||||
wanted = wanted or os.environ.get("GODOT_VERSION", DEFAULT_GODOT_VERSION)
|
||||
system, machine = platform.system(), platform.machine()
|
||||
tag = PLATFORMS.get((system, machine))
|
||||
if tag is None:
|
||||
raise ReachError(
|
||||
f"unsupported platform: {system} {machine}",
|
||||
fix="install Godot manually from https://godotengine.org/download",
|
||||
)
|
||||
|
||||
filename = f"Godot_v{wanted}-stable_{tag}.zip"
|
||||
return GodotPlan(
|
||||
wanted=wanted,
|
||||
installed=installed_godot_version(),
|
||||
platform_tag=tag,
|
||||
filename=filename,
|
||||
url=(
|
||||
"https://github.com/godotengine/godot/releases/download/"
|
||||
f"{wanted}-stable/{filename}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def installed_godot_version() -> str | None:
|
||||
"""The major.minor already on disk, or None. Never raises."""
|
||||
if not GODOT_BINARY.is_file() or not os.access(GODOT_BINARY, os.X_OK):
|
||||
return None
|
||||
result = process.run([str(GODOT_BINARY), "--version"], check=False)
|
||||
first = (result.stdout or "").splitlines()
|
||||
if not first:
|
||||
return None
|
||||
return ".".join(first[0].split(".")[:2])
|
||||
|
||||
|
||||
def install_godot(wanted: str | None = None) -> GodotPlan:
|
||||
"""Perform the install. The decision is `godot_plan`; this is the doing."""
|
||||
plan = godot_plan(wanted)
|
||||
if plan.already_current:
|
||||
console.verdict(f"Godot {plan.wanted} already installed at {GODOT_BINARY}")
|
||||
return plan
|
||||
if plan.installed:
|
||||
console.event(
|
||||
f"Godot found but version is {plan.installed}, want {plan.wanted}",
|
||||
level="warn",
|
||||
)
|
||||
|
||||
INSTALL_DIR.mkdir(parents=True, exist_ok=True)
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
archive = Path(tmp) / plan.filename
|
||||
console.event(f"downloading Godot {plan.wanted} for {plan.platform_tag}", phase="godot")
|
||||
process.run(
|
||||
["curl", "-fSL", "--progress-bar", "-o", str(archive), plan.url],
|
||||
capture=False,
|
||||
fix=f"check that GODOT_VERSION={plan.wanted} is a real release",
|
||||
missing_fix="install curl",
|
||||
)
|
||||
console.event("extracting", phase="godot")
|
||||
process.run(
|
||||
["unzip", "-q", "-o", str(archive), "-d", tmp],
|
||||
fix="the archive may be truncated — re-run to download it again",
|
||||
missing_fix="install unzip",
|
||||
)
|
||||
extracted = next(
|
||||
(p for p in Path(tmp).iterdir() if p.is_file() and p.name.startswith("Godot")),
|
||||
None,
|
||||
)
|
||||
if extracted is None:
|
||||
raise ReachError(
|
||||
"the Godot archive contained no binary",
|
||||
fix="the download may be corrupt — delete it and re-run",
|
||||
)
|
||||
shutil.move(str(extracted), GODOT_BINARY)
|
||||
GODOT_BINARY.chmod(0o755)
|
||||
|
||||
console.verdict(f"Godot {plan.wanted} installed at {GODOT_BINARY}")
|
||||
return plan
|
||||
|
||||
|
||||
def install_rust() -> None:
|
||||
"""Ensure rustup, clippy and rustfmt are present."""
|
||||
if shutil.which("cargo") is None:
|
||||
raise ReachError(
|
||||
"cargo is not installed",
|
||||
fix="install Rust from https://rustup.rs, then re-run — this command "
|
||||
"adds the components but does not bootstrap the toolchain",
|
||||
)
|
||||
for component in ("clippy", "rustfmt"):
|
||||
console.event(f"ensuring {component}", phase="rust")
|
||||
process.run(
|
||||
["rustup", "component", "add", component],
|
||||
check=False,
|
||||
missing_fix="install rustup from https://rustup.rs",
|
||||
)
|
||||
console.verdict("Rust toolchain ready — clippy and rustfmt present")
|
||||
|
||||
|
||||
def worktree_plan(branch: str) -> Path:
|
||||
"""Where a worktree for `branch` would go. Raises if it cannot be created.
|
||||
|
||||
Both refusals are the original's and both are worth keeping: a worktree
|
||||
created from inside a worktree nests confusingly, and silently reusing an
|
||||
existing directory is how two branches end up sharing one tree.
|
||||
"""
|
||||
root = config.repo_root()
|
||||
if (root / ".git").is_file():
|
||||
raise ReachError(
|
||||
"this is a worktree, not the main checkout",
|
||||
fix="run this from the main checkout — nesting worktrees confuses "
|
||||
"both git and the tools that resolve the repo root",
|
||||
)
|
||||
target = root / ".worktrees" / branch
|
||||
if target.exists():
|
||||
raise ReachError(
|
||||
f"{target} already exists",
|
||||
fix=f"reuse it, or remove it first with: git worktree remove {target}",
|
||||
)
|
||||
return target
|
||||
|
||||
|
||||
def setup_worktree(branch: str, start: str = "HEAD") -> Path:
|
||||
"""Create a worktree and link the venv into it."""
|
||||
target = worktree_plan(branch)
|
||||
root = config.repo_root()
|
||||
|
||||
process.run(
|
||||
["git", "worktree", "add", str(target), "-b", branch, start],
|
||||
cwd=root,
|
||||
fix=f"check that {start} is a valid ref and {branch} is not already a branch",
|
||||
)
|
||||
|
||||
venv = root / ".venv"
|
||||
if venv.is_dir() and not (target / ".venv").exists():
|
||||
(target / ".venv").symlink_to(venv)
|
||||
console.event(f"linked .venv -> {venv}")
|
||||
|
||||
console.verdict(
|
||||
f"worktree ready: {target}\n"
|
||||
f" pql in this worktree needs --vault {target} (FR-4)\n"
|
||||
" tea runs from the main checkout — its go-git cannot read a linked worktree\n"
|
||||
" reach follows whichever checkout it was installed from; `make reach-repoint` "
|
||||
"if you want it to follow this one"
|
||||
)
|
||||
return target
|
||||
Reference in New Issue
Block a user