Files
chemenu/tools/chemenu/toolpaths.py
T
torben 210e0c8286
CI / verify (push) Successful in 2m16s
CI / pwsh (push) Successful in 1m24s
Release / release (push) Successful in 37s
feat: PowerShell 7 preflight and launcher - preflight.ps1, wikitool.ps1, doctor policy and Mark of the Web checks, pwsh CI job (#151, B)
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
2026-10-01 09:52:05 +02:00

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")