mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-06 12:00:40 +02:00
112 lines
4.5 KiB
Python
112 lines
4.5 KiB
Python
"""Turn a "Working configuration" issue (.github/ISSUE_TEMPLATE/working-config.yml) into catalog/games/<pkg>.yaml.
|
|
|
|
Used by .github/workflows/catalog-from-issue.yml after a maintainer labels the issue `catalog-accepted`; can also be
|
|
run by hand: gh issue view 12 --json body -q .body | python scripts/catalog_from_issue.py --issue 12
|
|
The issue text is untrusted: only the YAML block of the "Recipe" section is read, parsed with yaml.safe_load, reduced
|
|
to known catalog fields and checked (package id, status, types) before anything is written.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
|
|
from frameport.recommend.catalog import CatalogEntry, to_yaml # noqa: E402
|
|
|
|
PKG_RE = re.compile(r"^(rift\.[a-z0-9_]{1,80}|[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z0-9_]+){1,8})$")
|
|
STATUSES = {"works", "issues", "unsupported", "unknown"}
|
|
LIST_FIELDS = {"overport_extra", "overport_remove", "alt_overport", "frame", "frame_remove", "pcvr", "pcvr_remove",
|
|
"device"}
|
|
# FramePort's own patch ids must exist (OVRPort's are listed by its CLI at runtime, so not checked here)
|
|
OWN_PATCH_FIELDS = ("frame", "frame_remove", "pcvr", "pcvr_remove", "device")
|
|
DICT_FIELDS = {"adapter", "device_files", "lepton_env", "proton_env", "verified"}
|
|
ID_RE = re.compile(r"^[A-Za-z0-9_.\-]{1,80}$")
|
|
|
|
|
|
class Invalid(ValueError):
|
|
pass
|
|
|
|
|
|
def section(body: str, heading: str) -> str:
|
|
m = re.search(r"^###\s+" + re.escape(heading) + r"\s*$(.*?)(?=^###\s|\Z)", body, re.M | re.S)
|
|
if not m:
|
|
raise Invalid(f"no '{heading}' section")
|
|
return m.group(1).strip()
|
|
|
|
|
|
def recipe_from_body(body: str) -> dict:
|
|
text = section(body, "Recipe")
|
|
m = re.search(r"```(?:ya?ml)?\s*\n(.*?)```", text, re.S)
|
|
raw = m.group(1) if m else text
|
|
try:
|
|
d = yaml.safe_load(raw)
|
|
except yaml.YAMLError as exc:
|
|
raise Invalid(f"recipe isn't valid YAML: {exc}") from None
|
|
if not isinstance(d, dict):
|
|
raise Invalid("recipe must be a YAML mapping")
|
|
return d
|
|
|
|
|
|
def validate(d: dict) -> CatalogEntry:
|
|
pkg = d.get("package")
|
|
if not isinstance(pkg, str) or not PKG_RE.match(pkg):
|
|
raise Invalid(f"bad package id {pkg!r}")
|
|
if not isinstance(d.get("title"), str) or not d["title"].strip():
|
|
raise Invalid("missing title")
|
|
if d.get("status", "unknown") not in STATUSES:
|
|
raise Invalid(f"bad status {d.get('status')!r}")
|
|
unknown = sorted(set(d) - set(CatalogEntry.__dataclass_fields__) - {"origin"})
|
|
if unknown:
|
|
raise Invalid(f"unknown fields: {', '.join(unknown)}")
|
|
for k in LIST_FIELDS & set(d):
|
|
if not isinstance(d[k], list) or not all(isinstance(x, str) and ID_RE.match(x) for x in d[k]):
|
|
raise Invalid(f"{k} must be a list of patch ids")
|
|
from frameport.patches import base
|
|
|
|
base.load_all()
|
|
for k in OWN_PATCH_FIELDS:
|
|
unknown_ids = [x for x in d.get(k) or [] if x not in base.REGISTRY]
|
|
if unknown_ids:
|
|
raise Invalid(f"{k}: unknown patch id(s) {', '.join(unknown_ids)}")
|
|
for k in DICT_FIELDS & set(d):
|
|
if not isinstance(d[k], dict):
|
|
raise Invalid(f"{k} must be a mapping")
|
|
for k in ("device_files",):
|
|
for path in (d.get(k) or {}):
|
|
if not isinstance(path, str) or path.startswith("/") or ".." in Path(path).parts:
|
|
raise Invalid(f"{k}: bad path {path!r}")
|
|
d = {k: v for k, v in d.items() if k != "origin"}
|
|
return CatalogEntry.from_dict(d, "bundled")
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("body", nargs="?", help="file with the issue body (default: stdin)")
|
|
ap.add_argument("--issue", type=int, help="issue number (recorded in verified.issue)")
|
|
ap.add_argument("--out", type=Path, default=ROOT / "catalog" / "games")
|
|
a = ap.parse_args(argv)
|
|
body = Path(a.body).read_text(encoding="utf-8") if a.body else sys.stdin.read()
|
|
try:
|
|
entry = validate(recipe_from_body(body))
|
|
except Invalid as exc:
|
|
print(f"error: {exc}", file=sys.stderr)
|
|
return 1
|
|
if a.issue:
|
|
entry.verified = {**entry.verified, "issue": a.issue}
|
|
path = a.out / f"{entry.package}.yaml"
|
|
existed = path.exists()
|
|
path.write_text(to_yaml(entry), encoding="utf-8")
|
|
print(f"{'updated' if existed else 'added'} {path.relative_to(ROOT) if path.is_relative_to(ROOT) else path}")
|
|
print(f"package={entry.package}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|