mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
Setup writes absolute paths into the run config (cwd and --expert-profile into the Control bundle that ran it, the picture reader and library folders into
an engine package, the DeepSeek package). An update keeps the new Control/engine and the one before, so the second update after setup removed the folder
the config names: the model showed as not installed and a start failed ("Qwen breaks each time Strata is updated").
strata_control/cfg_paths.py is the one resolver: a string that names a file in components/control/<ver>, components/engine/<ver>-<kind> or
engine-dsv41/<ver>-<kind> whose version folder is gone is mapped to the same file in the active Control / the newest engine of its own kind. Paths the
person typed, packs, models, output paths (--expert-profile-save, log) and folders that still exist are left alone.
- profiles.read_run_config applies it in memory, so the doctor, the model list, the launch plan and the host see the current folders.
- runtime.start writes it once into every saved run config, one line in the host log, keeps a caller's expected source digest valid, and stops with the
file named when it exists in no current version.
Tests: tests/terminal/test_cfg_paths.py (real update_control twice, real runtime.start for the Terminal and the app, DeepSeek); 9 of 11 fail without the wiring.
153 lines
6.7 KiB
Python
153 lines
6.7 KiB
Python
"""Paths in a saved run config that name a version folder of Strata's own components, and what to do when an update has removed that folder.
|
|
|
|
Setup writes absolute paths into the run config: ``cwd`` and ``--expert-profile`` into the Control bundle that ran it
|
|
(``<components>/control/<version>/...``), the picture reader and library folders into an engine package
|
|
(``<components>/engine/<version>-<platform>-<backend>/...``), the DeepSeek engine into ``engine-dsv41/<version>-.../``. An update puts the new version
|
|
beside the old one and removes the versions older than the one before, so the second update after a model was set up deleted the folders its config
|
|
names: the model showed as "not installed" and a start failed (#4891; "Qwen breaks each time Strata is updated").
|
|
|
|
One resolver, used by both sides:
|
|
|
|
* ``resolve_config(cfg)`` (pure; ``profiles.read_run_config`` calls it, so the doctor, the model list, the launch plan and the runtime host see the
|
|
current folders without a file being written);
|
|
* ``repair(configs)`` (runtime.start calls it for the profile being started and every saved profile): the same, written once into the file, one plain
|
|
line for each file it changed.
|
|
|
|
Only paths in Strata's own folders are touched (``components/control``, ``components/engine``, ``engine-dsv41``), and only when the version folder they
|
|
name is GONE; a path the person typed themselves, a model or pack folder, or a version folder that still exists is left exactly as it is. A path whose
|
|
file is nowhere in the current version either is not guessed: ``unresolved`` names it.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
|
|
_SEP = r"[\\/]"
|
|
_COMPONENT = re.compile(rf"^(?P<head>.*?{_SEP}components{_SEP})(?P<kind>control|engine){_SEP}(?P<ver>[^\\/]+)(?P<rest>(?:{_SEP}.*)?)$", re.S)
|
|
_DSV41 = re.compile(rf"^(?P<head>.*?{_SEP}engine-dsv41{_SEP})(?P<ver>[^\\/]+)(?P<rest>(?:{_SEP}.*)?)$", re.S)
|
|
_VERSION = re.compile(r"^\d+(?:\.\d+)+")
|
|
SKIP_KEYS = ("log",) # paths the engine writes, not reads
|
|
SKIP_FLAGS = ("--expert-profile-save",)
|
|
|
|
|
|
def _key(name: str) -> tuple:
|
|
head = str(name).split("-")[0]
|
|
return tuple(int(x) for x in head.split(".") if x.isdigit()), str(name)
|
|
|
|
|
|
def _kind_of(name: str) -> str:
|
|
return str(name).split("-", 1)[1] if "-" in str(name) else ""
|
|
|
|
|
|
def _newest(folder: Path, kind: str | None = None) -> Path | None:
|
|
try:
|
|
found = [d for d in folder.iterdir() if d.is_dir() and _VERSION.match(d.name) and (kind is None or _kind_of(d.name) == kind)]
|
|
except OSError:
|
|
return None
|
|
return max(found, key=lambda d: _key(d.name), default=None)
|
|
|
|
|
|
def _active_control(control: Path) -> Path | None:
|
|
try:
|
|
data = json.loads((control / "active.json").read_text(encoding="utf-8-sig"))
|
|
folder = control / str(data["folder"])
|
|
if folder.is_dir():
|
|
return folder
|
|
except (OSError, ValueError, KeyError, TypeError):
|
|
pass
|
|
return _newest(control)
|
|
|
|
|
|
def _components() -> Path:
|
|
from strata_control import engine_component as ec
|
|
return ec.components_dir()
|
|
|
|
|
|
def resolve_path(value: str) -> tuple[str, bool] | None:
|
|
"""For a string that names a file in a version folder of Strata's own that is gone: (the same file in the current version, True) when it is there,
|
|
(value, False) when it is nowhere; None when the string is not such a path or its version folder still exists (nothing to do)."""
|
|
if not isinstance(value, str) or len(value) < 8 or "\n" in value:
|
|
return None
|
|
m = _COMPONENT.match(value)
|
|
if m:
|
|
old = Path(m["head"] + m["kind"] + os.sep + m["ver"])
|
|
if old.exists() or not _VERSION.match(m["ver"]):
|
|
return None
|
|
root = _components() / m["kind"]
|
|
current = _active_control(root) if m["kind"] == "control" else _newest(root, _kind_of(m["ver"]))
|
|
rest = m["rest"].lstrip("\\/")
|
|
else:
|
|
m = _DSV41.match(value)
|
|
if not m:
|
|
return None
|
|
old = Path(m["head"] + m["ver"])
|
|
if old.exists() or not _VERSION.match(m["ver"]):
|
|
return None
|
|
current = _newest(Path(m["head"]), _kind_of(m["ver"]))
|
|
rest = m["rest"].lstrip("\\/")
|
|
if current is None:
|
|
return value, False
|
|
new = current / rest if rest else current
|
|
return (str(new), True) if new.exists() else (value, False)
|
|
|
|
|
|
def _walk(obj, found: list, lost: list, key: str = ""):
|
|
if isinstance(obj, str):
|
|
if key in SKIP_KEYS:
|
|
return obj
|
|
res = resolve_path(obj)
|
|
if res is None:
|
|
return obj
|
|
(found if res[1] else lost).append((obj, res[0]))
|
|
return res[0]
|
|
if isinstance(obj, list):
|
|
out, skip = [], False
|
|
for item in obj:
|
|
out.append(item if skip else _walk(item, found, lost, key))
|
|
skip = isinstance(item, str) and item in SKIP_FLAGS
|
|
return out
|
|
if isinstance(obj, dict):
|
|
return {k: _walk(v, found, lost, str(k)) for k, v in obj.items()}
|
|
return obj
|
|
|
|
|
|
def resolve_config(cfg: dict) -> tuple[dict, list, list]:
|
|
"""(the config with every path of a removed version folder pointed at the current one, [(old, new)], [(old, "") that exist nowhere])."""
|
|
found: list = []
|
|
lost: list = []
|
|
return _walk(cfg, found, lost), found, [(old, "") for old, _ in lost]
|
|
|
|
|
|
def repair(configs) -> tuple[list[str], list[str]]:
|
|
"""For a START: every run config in `configs` is resolved and, when a path changed, written once (atomically, nothing else changed).
|
|
Returns (one plain line for each file changed, one plain line for each path that exists nowhere). Never raises."""
|
|
notes: list[str] = []
|
|
problems: list[str] = []
|
|
seen = set()
|
|
for c in configs:
|
|
c = Path(c)
|
|
if c in seen:
|
|
continue
|
|
seen.add(c)
|
|
try:
|
|
cfg = json.loads(c.read_text(encoding="utf-8-sig"))
|
|
except (OSError, ValueError):
|
|
continue
|
|
if not isinstance(cfg, dict):
|
|
continue
|
|
new, found, lost = resolve_config(cfg)
|
|
for old, _ in lost:
|
|
problems.append(f"{c.name}: {old} was removed by an update and no file of that name is in the current version; run setup again for this model")
|
|
if not found:
|
|
continue
|
|
try:
|
|
from strata_control import profiles
|
|
profiles._atomic(c, json.dumps(new, indent=2).encode("utf-8"))
|
|
except (OSError, ValueError):
|
|
continue
|
|
notes.append(f"{c.name}: {len(found)} path{'s' if len(found) != 1 else ''} named folders of an earlier Strata version that an update removed ("
|
|
+ ", ".join(sorted({Path(o).name or o for o, _ in found})[:3]) + "); they now name the current version.")
|
|
return notes, problems
|