Files
RetroDebian/builder/py/retrobuilder/context.py
2026-04-02 23:08:41 +02:00

118 lines
3.9 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Mapping
def _norm(name: str) -> str:
return "".join(ch if ch.isalnum() else "_" for ch in name.upper())
class Node(dict):
__getattr__ = dict.get
@classmethod
def wrap(cls, value: Any) -> Any:
if isinstance(value, dict):
return cls({k: cls.wrap(v) for k, v in value.items()})
if isinstance(value, (list, tuple)):
return [cls.wrap(v) for v in value]
return value
@dataclass(frozen=True)
class BuildContext:
project_root: Path
live_dir: Path
artifacts_root: Path
phase: str
kind: str
name: str
profile_name: str = ""
base_name: str = ""
feature_name: str = ""
profile: Mapping[str, Any] = field(default_factory=dict)
base: Mapping[str, Any] = field(default_factory=dict)
features: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
def __post_init__(self) -> None:
object.__setattr__(self, "profile", Node.wrap(dict(self.profile)))
object.__setattr__(self, "base", Node.wrap(dict(self.base)))
object.__setattr__(self, "features", {k: Node.wrap(dict(v)) for k, v in self.features.items()})
def to_env(self) -> dict[str, str]:
env = {
"PROJECT_ROOT": str(self.project_root),
"LIVE_DIR": str(self.live_dir),
"ARTIFACTS_ROOT": str(self.artifacts_root),
"PHASE": self.phase,
"CURRENT_KIND": self.kind,
"CURRENT_NAME": self.name,
"PROFILE_NAME": self.profile_name,
"BASE_NAME": self.base_name,
"FEATURE_NAME": self.feature_name,
"FEATURE_NAMES": "|".join(self.features.keys()),
}
env.update(_flatten("PROFILE", self.profile))
env.update(_flatten("BASE", self.base))
for feat_name, feat_cfg in self.features.items():
env.update(_flatten(f"FEAT__{_norm(feat_name)}", feat_cfg))
return env
@classmethod
def from_env(cls, env: Mapping[str, str]) -> "BuildContext":
feature_names = [x for x in env.get("FEATURE_NAMES", "").split("|") if x]
profile = _inflate("PROFILE", env)
base = _inflate("BASE", env)
features = {name: _inflate(f"FEAT__{_norm(name)}", env) for name in feature_names}
return cls(
project_root=Path(env["PROJECT_ROOT"]),
live_dir=Path(env["LIVE_DIR"]),
artifacts_root=Path(env["ARTIFACTS_ROOT"]),
phase=env["PHASE"],
kind=env["CURRENT_KIND"],
name=env["CURRENT_NAME"],
profile_name=env.get("PROFILE_NAME", ""),
base_name=env.get("BASE_NAME", ""),
feature_name=env.get("FEATURE_NAME", ""),
profile=profile,
base=base,
features=features,
)
def _stringify(value: Any) -> str:
if isinstance(value, bool):
return "True" if value else "False"
if value is None:
return ""
if isinstance(value, (list, tuple)):
return "|".join(_stringify(v) for v in value)
return str(value)
def _flatten(prefix: str, data: Mapping[str, Any]) -> dict[str, str]:
out: dict[str, str] = {}
for key, value in data.items():
name = f"{prefix}__{_norm(str(key))}"
if isinstance(value, dict):
out.update(_flatten(name, value))
else:
out[name] = _stringify(value)
return out
def _inflate(prefix: str, env: Mapping[str, str]) -> dict[str, Any]:
root: dict[str, Any] = {}
marker = f"{prefix}__"
for key, value in env.items():
if not key.startswith(marker):
continue
parts = key[len(marker):].split("__")
node = root
for part in parts[:-1]:
node = node.setdefault(part.lower(), {})
node[parts[-1].lower()] = value
return root