updates 33.2 112
This commit is contained in:
184
build.py
184
build.py
@@ -23,6 +23,7 @@ Generated structure for managed rooms:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
@@ -532,6 +533,148 @@ def _append_cabinet_env(output_dir: Path, cabinets: list[dict]):
|
||||
example.write_text(existing.rstrip("\n") + "\n" + "\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def load_plexuses(room: str) -> list[dict]:
|
||||
"""The plexuses a room asked for. Same shape as its sibling data/*.json."""
|
||||
path = SPR_ROOT / "cfg" / room / "data" / "plexuses.json"
|
||||
if not path.exists():
|
||||
return []
|
||||
try:
|
||||
raw = json.loads(path.read_text())
|
||||
except ValueError as e:
|
||||
log.warning(f" plexuses.json is not valid JSON, ignoring: {e}")
|
||||
return []
|
||||
|
||||
entries = raw.get("items", raw) if isinstance(raw, dict) else raw
|
||||
out = []
|
||||
for entry in entries if isinstance(entries, list) else []:
|
||||
if isinstance(entry, str):
|
||||
entry = {"name": entry}
|
||||
if isinstance(entry, dict) and entry.get("name"):
|
||||
out.append(entry)
|
||||
return out
|
||||
|
||||
|
||||
def _theme_css(theme: str) -> str:
|
||||
"""The token contract plus one theme, flattened for inlining.
|
||||
|
||||
Only the named theme ships alongside the others it can switch to, because
|
||||
the export has to work with no server: there is no /theme.css to fetch.
|
||||
"""
|
||||
theme_dir = SPR_ROOT / "soleprint" / "common" / "theme"
|
||||
parts = []
|
||||
tokens = theme_dir / "tokens.css"
|
||||
if tokens.exists():
|
||||
parts.append(tokens.read_text())
|
||||
# Every theme, so the switcher in the page has something to switch to.
|
||||
for sheet in sorted((theme_dir / "themes").glob("*.css")):
|
||||
parts.append(sheet.read_text())
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def _inline_svg(name: str, theme: str) -> str:
|
||||
"""A rendered graph, stripped of its XML prolog so it can sit in HTML.
|
||||
|
||||
Inlined rather than <img>-linked so the page's CSS can recolour it when the
|
||||
theme switches — graphviz writes class="node accent" into the SVG, and CSS
|
||||
outranks the presentation attributes it bakes in.
|
||||
"""
|
||||
graphs = SPR_ROOT / "docs" / "graphs"
|
||||
for candidate in (graphs / f"{name}.{theme}.svg", graphs / f"{name}.svg"):
|
||||
if candidate.exists():
|
||||
svg = candidate.read_text()
|
||||
start = svg.find("<svg")
|
||||
return svg[start:] if start >= 0 else svg
|
||||
log.warning(f" no rendered graph '{name}' — run docs/graphs/render.sh")
|
||||
return "<p>diagram not rendered</p>"
|
||||
|
||||
|
||||
def build_plexuses(output_dir: Path, room: str):
|
||||
"""Export each plexus the room declared to a single self-contained file.
|
||||
|
||||
A plexus is exported, not served. The output is one index.html carrying its
|
||||
theme, its data and its diagram, so it survives a locked-down machine, a zip
|
||||
attachment and a double-click — which is the whole point of the format.
|
||||
"""
|
||||
requested = load_plexuses(room)
|
||||
if not requested:
|
||||
return
|
||||
|
||||
source_root = SPR_ROOT / "soleprint" / "artery" / "plexuses"
|
||||
built = []
|
||||
|
||||
for entry in requested:
|
||||
name = entry["name"]
|
||||
source = source_root / name
|
||||
manifest_path = source / "plexus.json"
|
||||
if not manifest_path.exists():
|
||||
available = sorted(
|
||||
p.name for p in source_root.iterdir() if p.is_dir()
|
||||
) if source_root.exists() else []
|
||||
log.warning(
|
||||
f" no such plexus: {name} (available: {', '.join(available) or 'none'})"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
except ValueError as e:
|
||||
log.warning(f" plexus {name} has invalid plexus.json: {e}")
|
||||
continue
|
||||
|
||||
# The room may override anything the plexus declares — theme first.
|
||||
manifest.update({k: v for k, v in entry.items() if k != "name"})
|
||||
|
||||
template_path = source / "app" / "index.html"
|
||||
if not template_path.exists():
|
||||
log.warning(f" plexus {name} has no app/index.html")
|
||||
continue
|
||||
|
||||
theme = manifest.get("theme", "soleprint")
|
||||
data = {k: v for k, v in manifest.items() if not k.startswith("_")}
|
||||
|
||||
# A plexus may ship a showcase.py exposing collect(): anything it returns
|
||||
# is merged into the page's data. The bundle uses it to run the real
|
||||
# tools over the real fixtures at build time, so what the page shows
|
||||
# cannot drift from what the tools do.
|
||||
collector = source / "showcase.py"
|
||||
if collector.exists():
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"plexus_{name}_showcase", collector
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
data["showcase"] = module.collect()
|
||||
except Exception as e:
|
||||
log.warning(f" {name}: showcase.py failed ({type(e).__name__}: {e})")
|
||||
|
||||
page = template_path.read_text()
|
||||
for token, value in (
|
||||
("%%TITLE%%", manifest.get("title", name)),
|
||||
("%%DESCRIPTION%%", manifest.get("description", "")),
|
||||
("%%DEFAULT_THEME%%", theme),
|
||||
("%%BUILT%%", f"{room} · built by soleprint build.py"),
|
||||
("%%THEME_CSS%%", _theme_css(theme)),
|
||||
("%%GRAPH%%", _inline_svg(manifest.get("graph", "system_overview"), theme)),
|
||||
("%%BUNDLE%%", json.dumps(data, indent=2)),
|
||||
):
|
||||
page = page.replace(token, value)
|
||||
|
||||
target = output_dir / "plexuses" / name
|
||||
ensure_dir(target)
|
||||
(target / "index.html").write_text(page)
|
||||
|
||||
# Anything else in app/ rides along, for a plexus that outgrows one file.
|
||||
for extra in (source / "app").iterdir():
|
||||
if extra.name != "index.html":
|
||||
copy_path(extra, target / extra.name, quiet=True)
|
||||
|
||||
built.append(f"{name} ({theme})")
|
||||
|
||||
if built:
|
||||
log.info(f" plexuses: {', '.join(built)}")
|
||||
|
||||
|
||||
def build_soleprint(output_dir: Path, room: str):
|
||||
"""Build soleprint folder with core + room config merged."""
|
||||
soleprint = SPR_ROOT / "soleprint"
|
||||
@@ -568,6 +711,11 @@ def build_soleprint(output_dir: Path, room: str):
|
||||
log.info("Composing cabinets...")
|
||||
compose_cabinets(output_dir, room)
|
||||
|
||||
# Plexuses are exported rather than served, so this is a compile step like
|
||||
# the cabinet merge above — not something run.py does at request time.
|
||||
log.info("Exporting plexuses...")
|
||||
build_plexuses(output_dir, room)
|
||||
|
||||
# Generate models
|
||||
log.info("Generating models...")
|
||||
if not generate_models(output_dir, room):
|
||||
@@ -624,6 +772,33 @@ def build_models_only():
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def build_plexuses_only(room: str):
|
||||
"""Compile just the plexus UIs, without rebuilding the room around them.
|
||||
|
||||
The equivalent of `vite build` for this repo: the iteration loop when you
|
||||
are working on the UI itself is edit, compile, reopen the file — and a full
|
||||
room build to see a CSS change is a slow way to do that.
|
||||
"""
|
||||
output_dir = SPR_ROOT / "gen" / room
|
||||
if not output_dir.exists():
|
||||
log.error(f"Room '{room}' is not built — run: python build.py --cfg {room}")
|
||||
sys.exit(1)
|
||||
|
||||
log.info(f"Compiling plexus UIs for {room}...")
|
||||
build_plexuses(output_dir, room)
|
||||
|
||||
built = sorted((output_dir / "plexuses").glob("*/index.html"))
|
||||
if not built:
|
||||
log.warning(
|
||||
f" nothing compiled — does cfg/{room}/data/plexuses.json list one?"
|
||||
)
|
||||
return
|
||||
for page in built:
|
||||
log.info(f" {page.relative_to(SPR_ROOT)} ({page.stat().st_size // 1024} KB)")
|
||||
log.info("\n✓ Open directly — no server needed:")
|
||||
log.info(f" xdg-open {built[0]}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Soleprint Build Tool")
|
||||
|
||||
@@ -631,10 +806,17 @@ def main():
|
||||
parser.add_argument("--cfg", "-c", type=str, help="Room config name")
|
||||
parser.add_argument("--all", action="store_true", help="Build all rooms")
|
||||
parser.add_argument("--models", action="store_true", help="Only regenerate models")
|
||||
parser.add_argument(
|
||||
"--plexuses",
|
||||
action="store_true",
|
||||
help="Only compile the plexus UIs into an already-built room",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.models:
|
||||
if args.plexuses:
|
||||
build_plexuses_only(args.cfg or "standalone")
|
||||
elif args.models:
|
||||
build_models_only()
|
||||
elif args.all:
|
||||
build(SPR_ROOT / "gen" / "standalone", None)
|
||||
|
||||
Reference in New Issue
Block a user