Files changed: - .gitea/workflows/ci.yml - CHANGES.md - INSTALL.md - README.md - VERSION - docs/why-gates-are-code.md - instructions/bootstrap.md - instructions/bug-report.md - instructions/preflight.md - instructions/setup-instance.md - instructions/upgrade-instance.md - tools/CONTRACT.md - tools/README.md - tools/bugreport.py - tools/chemenu/cli.py - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/doctor.py - tools/chemenu/prerequisites.py - tools/chemenu/tests/conftest.py - tools/chemenu/tests/test_bugreport.py - tools/chemenu/tests/test_doctor.py - tools/chemenu/tests/test_preflight.py - tools/chemenu/tests/test_preflight_pwsh.py - tools/chemenu/toolpaths.py - tools/preflight.ps1 - tools/wikitool - tools/wikitool.ps1
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
"""Where the programs this package starts live: `.wikitool-tools.json`.
|
|
|
|
The preflight (`tools/preflight.sh`, `tools/preflight.ps1`) finds `git`, `rg` and
|
|
the rest once, checks they run, and records their absolute native paths in this
|
|
file beside `tools/`. Everything here that starts one of them asks `resolve()`
|
|
for the path instead of trusting whatever `PATH` the calling process inherited -
|
|
a harness session on Windows can hold a `PATH` from before the tool was
|
|
installed, and then the same machine answers "missing" in one terminal and
|
|
"present" in the next.
|
|
|
|
The file belongs to the checkout the code runs from, not to the corpus it is
|
|
pointed at: `CHEMENU_ROOT` may name another tree, and that tree's tools are this
|
|
installation's tools. So it is read from `config._PACKAGE_ROOT`, never `ROOT`.
|
|
|
|
Enforcement lives in the launcher, which refuses to start (exit 42) until the
|
|
preflight has written a complete file. Here, an absent file falls back to the
|
|
bare program name - the case of the test suite and of `python -m chemenu.cli`
|
|
run by hand. A file that is present but wrong never falls back: a tool it does
|
|
not name, or names at a path that has gone, is a `ToolPathError` that tells the
|
|
user to run the preflight again.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from chemenu import config
|
|
from chemenu.errors import ChemenuError
|
|
|
|
FILE_NAME = ".wikitool-tools.json"
|
|
SCHEMA = 1
|
|
PREFLIGHT_PWSH = "pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1"
|
|
PREFLIGHT = f"run the preflight again: tools/preflight.sh (PowerShell 7: {PREFLIGHT_PWSH})"
|
|
|
|
|
|
class ToolPathError(ChemenuError):
|
|
"""The recorded tool paths cannot be used. Deliberately not an `OSError`:
|
|
call sites that treat a failed start as "tool absent" must not swallow
|
|
this - the fix is a command for the user, not a degraded answer."""
|
|
|
|
|
|
def tools_file() -> Path:
|
|
return config._PACKAGE_ROOT / FILE_NAME
|
|
|
|
|
|
def load(path: Optional[Path] = None) -> Optional[dict]:
|
|
"""The parsed file, or None when it does not exist. Raises `ToolPathError`
|
|
for a file that exists but is not one this version writes."""
|
|
path = tools_file() if path is None else path
|
|
try:
|
|
text = path.read_text(encoding="utf-8")
|
|
except FileNotFoundError:
|
|
return None
|
|
except OSError as exc:
|
|
raise ToolPathError(f"{FILE_NAME} cannot be read ({exc}) - {PREFLIGHT}") from exc
|
|
try:
|
|
data = json.loads(text)
|
|
except ValueError as exc:
|
|
raise ToolPathError(f"{FILE_NAME} is not valid JSON ({exc}) - {PREFLIGHT}") from exc
|
|
if not isinstance(data, dict) or data.get("schema") != SCHEMA or not isinstance(data.get("tools"), dict):
|
|
raise ToolPathError(
|
|
f"{FILE_NAME} is not in the shape this version reads (schema {SCHEMA} with a "
|
|
f"`tools` object) - {PREFLIGHT}"
|
|
)
|
|
return data
|
|
|
|
|
|
def resolve(name: str) -> str:
|
|
"""The program to start for `name`: its recorded path, or `name` itself
|
|
when no file has been written."""
|
|
data = load()
|
|
if data is None:
|
|
return name
|
|
path = data["tools"].get(name)
|
|
if not isinstance(path, str) or not path:
|
|
raise ToolPathError(f"`{name}` is not recorded in {FILE_NAME} - {PREFLIGHT}")
|
|
if not os.path.isfile(path):
|
|
raise ToolPathError(f"`{name}` is recorded at {path}, which no longer exists - {PREFLIGHT}")
|
|
return path
|
|
|
|
|
|
def git() -> str:
|
|
return resolve("git")
|
|
|
|
|
|
def rg() -> str:
|
|
return resolve("rg")
|