138 lines
6.7 KiB
Python
138 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Index LinuxCNC GUI sources, simulation descriptors and visual assets.
|
|
|
|
This is an audit input for FE-M4. It does not claim that a desktop GUI file is
|
|
already implemented in Web; each row retains its source hash and adaptation
|
|
boundary for later React/Worker tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
REPORT = ROOT / "coverage/gui-source-inventory-fe-m4.report.json"
|
|
CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"
|
|
|
|
SOURCE_ROOTS = (
|
|
Path("linuxcnc/src/emc/usr_intf/axis"),
|
|
Path("linuxcnc/src/emc/usr_intf/gmoccapy"),
|
|
Path("linuxcnc/src/emc/usr_intf/gscreen"),
|
|
Path("linuxcnc/src/emc/usr_intf/touchy"),
|
|
Path("linuxcnc/src/emc/usr_intf/qtvcp"),
|
|
Path("linuxcnc/src/emc/usr_intf/qtplasmac"),
|
|
Path("linuxcnc/src/emc/usr_intf/pyui"),
|
|
Path("linuxcnc/src/emc/usr_intf/gremlin"),
|
|
Path("linuxcnc/lib/python/qtvcp"),
|
|
Path("linuxcnc/lib/python/gmoccapy"),
|
|
Path("linuxcnc/lib/python/gscreen"),
|
|
Path("linuxcnc/lib/python/touchy"),
|
|
Path("linuxcnc/lib/python/gladevcp"),
|
|
Path("linuxcnc/lib/python/plasmac"),
|
|
Path("linuxcnc/lib/python/vismach.py"),
|
|
)
|
|
SIM_ROOT = Path("linuxcnc/configs/sim")
|
|
GUI_FAMILIES = ("axis", "gmoccapy", "gscreen", "touchy", "qtvcp", "qtdragon", "qtplasmac", "pyvcp", "gladevcp", "ngcgui", "vismach", "plasma", "laser")
|
|
ASSET_SUFFIXES = {".png", ".gif", ".svg", ".jpg", ".jpeg", ".ui", ".glade", ".gladep", ".xml", ".pref", ".halscope", ".halshow"}
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def canonical(value: Any) -> str:
|
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
|
|
|
|
def family(path: str) -> str:
|
|
lower = path.lower()
|
|
for name in GUI_FAMILIES:
|
|
if f"/{name}/" in lower or lower.endswith(f"/{name}") or name in Path(lower).name:
|
|
return name
|
|
return "shared"
|
|
|
|
|
|
def adaptation(path: str) -> str:
|
|
lower = path.lower()
|
|
if "/src/emc/usr_intf/" in lower and any(token in lower for token in ("pncconf", "stepconf")):
|
|
return "REAL-EXCLUDED/WEB-EQUIV"
|
|
if lower.endswith((".glade", ".gladep", ".ui", ".xml")):
|
|
return "WEB-EQUIV/DESCRIPTOR"
|
|
if "/python/" in lower or lower.endswith(".py"):
|
|
return "PY-WASM/WEB-EQUIV"
|
|
if any(lower.endswith(suffix) for suffix in ASSET_SUFFIXES):
|
|
return "WEB-EQUIV/SOURCE-ASSET"
|
|
return "WEB-EQUIV/SOURCE-MAP"
|
|
|
|
|
|
def rows() -> list[dict[str, Any]]:
|
|
selected: dict[str, Path] = {}
|
|
for root in SOURCE_ROOTS:
|
|
path = ROOT / root
|
|
if path.is_file():
|
|
selected[root.as_posix()] = path
|
|
elif path.is_dir():
|
|
for item in path.rglob("*"):
|
|
if item.is_file() and "__pycache__" not in item.parts and item.suffix != ".pyc":
|
|
selected[item.relative_to(ROOT).as_posix()] = item
|
|
for item in (ROOT / SIM_ROOT).rglob("*"):
|
|
if not item.is_file() or "__pycache__" in item.parts or item.suffix == ".pyc":
|
|
continue
|
|
relative = item.relative_to(ROOT).as_posix()
|
|
lower = relative.lower()
|
|
if any(f"/{name}/" in lower for name in GUI_FAMILIES) or item.suffix.lower() in ASSET_SUFFIXES:
|
|
selected[relative] = item
|
|
result = []
|
|
for relative, path in sorted(selected.items()):
|
|
result.append({"source_id": f"gui:{relative}", "path": relative, "family": family(relative), "kind": "asset" if path.suffix.lower() in ASSET_SUFFIXES else "source", "adaptation": adaptation(relative), "bytes": path.stat().st_size, "sha256": sha256(path), "status": "planned"})
|
|
return result
|
|
|
|
|
|
def generate() -> dict[str, Any]:
|
|
records = rows()
|
|
manifest_path = ROOT / "coverage/manifest.jsonl"
|
|
manifest_sources: set[str] = set()
|
|
if manifest_path.is_file():
|
|
for line in manifest_path.read_text(encoding="utf-8").splitlines():
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if isinstance(value, dict):
|
|
source = value.get("source")
|
|
if isinstance(source, str): manifest_sources.add(source.removeprefix("config:").removeprefix("source:").removeprefix("doc:"))
|
|
for record in records:
|
|
record["manifest_indexed"] = record["path"] in manifest_sources
|
|
family_counts = Counter(record["family"] for record in records)
|
|
kind_counts = Counter(record["kind"] for record in records)
|
|
report: dict[str, Any] = {
|
|
"schema": "gui-source-inventory-fe-m4-report.v1",
|
|
"task_id": "FE-M4-GUI-01",
|
|
"context_id": CONTEXT_ID,
|
|
"status": "verified" if records and all(record["sha256"] for record in records) else "blocked",
|
|
"implementation_status": "audited",
|
|
"scope": {"source_roots": [path.as_posix() for path in SOURCE_ROOTS], "simulation_root": SIM_ROOT.as_posix(), "families": list(GUI_FAMILIES)},
|
|
"counts": {"records": len(records), "families": dict(sorted(family_counts.items())), "kinds": dict(sorted(kind_counts.items())), "manifest_indexed": sum(record["manifest_indexed"] for record in records), "manifest_unindexed": sum(not record["manifest_indexed"] for record in records)},
|
|
"records": records,
|
|
"checks": {"source_hashes": {"status": "verified" if all(record["sha256"] for record in records) else "blocked"}, "exact_paths": {"status": "verified" if all((ROOT / record["path"]).is_file() for record in records) else "blocked"}, "manifest_reverse_index": {"status": "verified" if all(record["manifest_indexed"] for record in records) else "blocked", "unindexed": [record["path"] for record in records if not record["manifest_indexed"]][:100]}},
|
|
"boundaries": ["desktop GTK/Qt/Tk windows are not copied into Web", "Python/VCP files require PY-WASM and capability checks", "REAL-EXCLUDED hardware configuration editors remain explicit", "full GUI acceptance requires Worker workflow, source oracle and browser evidence per family"],
|
|
}
|
|
report["report_sha256"] = hashlib.sha256(canonical(report).encode("utf-8")).hexdigest()
|
|
REPORT.parent.mkdir(parents=True, exist_ok=True)
|
|
REPORT.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
|
return report
|
|
|
|
|
|
if __name__ == "__main__":
|
|
report = generate()
|
|
print(f"gui_source_inventory={report['status']} records={report['counts']['records']} families={len(report['counts']['families'])} unindexed={report['counts']['manifest_unindexed']} report_sha256={report['report_sha256']}")
|