Repo-side rename only (Phases 1-3 of the migration plan); the live box (~/.castle, systemd units, /data/castle, domains) is a separate cutover. - Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api), dist names, entry point `wildpc = wildpc_cli.main:main`. - Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*. - Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml -> wildpc.yaml, /data/castle -> /data/wildpc. - Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc. - Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend. - Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated; redundant nested uv.lock files dropped (workspace root lock is authoritative). Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds. Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
"""Toolchain resolution — map a program's declared runtime version to a concrete
|
|
binary directory on this box.
|
|
|
|
Today this covers **node**. Programs pin their node version the ecosystem-standard
|
|
way — a ``.node-version`` / ``.nvmrc`` file, or ``package.json`` ``engines.node`` /
|
|
``volta.node`` — so a program stays wildpc-independent (the prime directive: regular
|
|
programs never depend on wildpc). Wild PC *reads* that pin and resolves it to an
|
|
absolute ``.../bin`` directory it can put on PATH, at both execution sites that run a
|
|
program's node:
|
|
|
|
- **build time** — the dev-verb subprocess (``stacks._build_env``), so ``wildpc
|
|
program build`` uses the program's node regardless of who triggers it (your shell
|
|
or the wildpc-api build executor);
|
|
- **run time** — a ``launcher: node`` service's systemd unit PATH (via
|
|
``deploy._build_deployed`` → the generated unit's ``Environment=PATH``).
|
|
|
|
Resolution scans nvm's versioned install layout (``WILDPC_NODE_VERSIONS_DIR``,
|
|
default ``~/.nvm/versions/node``) — the versioned dir the default tool PATH
|
|
intentionally omits. A pinned-but-not-installed version fails loud with an
|
|
actionable ``nvm install`` hint rather than surfacing later as ``node: not found``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
# A pin that is a bare, exact-ish version: "24", "24.14", "24.14.1" (optional "v").
|
|
_EXACT_RE = re.compile(r"v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$")
|
|
# An installed nvm dir: v24.14.1
|
|
_INSTALLED_RE = re.compile(r"v(\d+)\.(\d+)\.(\d+)$")
|
|
# Wildcards/aliases that mean "newest installed".
|
|
_NEWEST_ALIASES = {"", "*", "x", "node", "latest", "current", "lts", "lts/*"}
|
|
|
|
|
|
class ToolchainError(Exception):
|
|
"""A program pins a toolchain version that isn't installed on this box."""
|
|
|
|
|
|
def node_versions_dir() -> Path:
|
|
"""The directory nvm installs versioned node under (env-overridable)."""
|
|
override = os.environ.get("WILDPC_NODE_VERSIONS_DIR")
|
|
return Path(override) if override else Path.home() / ".nvm" / "versions" / "node"
|
|
|
|
|
|
def read_node_pin(source: Path) -> str | None:
|
|
"""The node version a program pins, read the ecosystem-standard way.
|
|
|
|
Precedence: ``.node-version`` → ``.nvmrc`` → ``package.json`` (``engines.node``,
|
|
else ``volta.node``). Returns the raw spec string, or ``None`` if unpinned."""
|
|
for fname in (".node-version", ".nvmrc"):
|
|
f = source / fname
|
|
if f.is_file():
|
|
val = f.read_text().strip()
|
|
if val:
|
|
return val
|
|
pkg = source / "package.json"
|
|
if pkg.is_file():
|
|
try:
|
|
data = json.loads(pkg.read_text())
|
|
except (json.JSONDecodeError, OSError):
|
|
return None
|
|
for section in ("engines", "volta"):
|
|
node = (data.get(section) or {}).get("node")
|
|
if isinstance(node, str) and node.strip():
|
|
return node.strip()
|
|
return None
|
|
|
|
|
|
def _installed() -> list[tuple[tuple[int, int, int], Path]]:
|
|
"""Installed (version, bin-dir) pairs, newest first."""
|
|
root = node_versions_dir()
|
|
out: list[tuple[tuple[int, int, int], Path]] = []
|
|
if not root.is_dir():
|
|
return out
|
|
for child in root.iterdir():
|
|
m = _INSTALLED_RE.match(child.name)
|
|
if m and (child / "bin" / "node").exists():
|
|
out.append(((int(m[1]), int(m[2]), int(m[3])), child / "bin"))
|
|
out.sort(reverse=True)
|
|
return out
|
|
|
|
|
|
def resolve_node_bin(source: Path | None) -> Path | None:
|
|
"""Resolve a program's pinned node version to its ``.../bin`` dir on this box.
|
|
|
|
Returns ``None`` when the program pins nothing — wildpc injects no node, it does
|
|
not guess. Raises :class:`ToolchainError` when a version *is* pinned but no
|
|
matching version is installed.
|
|
|
|
Matching: an exact-ish pin (``24`` / ``24.14`` / ``24.14.1``) matches installed
|
|
versions by that precision, newest wins. A range or alias (``>=24``, ``^24.1``,
|
|
``lts/*``) is pinned by its leading major (or newest installed if it names none)
|
|
— precise pins belong in ``.node-version``."""
|
|
if source is None:
|
|
return None
|
|
pin = read_node_pin(source)
|
|
if not pin:
|
|
return None
|
|
installed = _installed()
|
|
|
|
def newest_or_raise() -> Path:
|
|
if installed:
|
|
return installed[0][1]
|
|
raise ToolchainError(
|
|
f"node {pin!r} pinned (in {source}) but no node is installed under "
|
|
f"{node_versions_dir()} — run `nvm install {pin}`"
|
|
)
|
|
|
|
low = pin.strip().lower()
|
|
if low in _NEWEST_ALIASES:
|
|
return newest_or_raise()
|
|
|
|
exact = _EXACT_RE.match(low)
|
|
if exact:
|
|
parts = [int(g) for g in exact.groups() if g is not None]
|
|
else:
|
|
# A range/alias (>=24, ^24.1, ~24, 24.x): pin by leading major only.
|
|
major = re.search(r"\d+", low)
|
|
if not major:
|
|
return newest_or_raise()
|
|
parts = [int(major.group())]
|
|
precision = len(parts)
|
|
|
|
for ver, bindir in installed: # newest first
|
|
if list(ver[:precision]) == parts:
|
|
return bindir
|
|
raise ToolchainError(
|
|
f"node {pin!r} pinned (in {source}) but not installed under "
|
|
f"{node_versions_dir()} — run `nvm install {pin}`"
|
|
)
|