Files
FreeCADWasmDev/tools/inspect_web_module_artifacts.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

71 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Inspect one module's published files without loading its CAD runtime."""
import argparse
from datetime import datetime, timezone
import hashlib
import json
from pathlib import Path
import time
from urllib.parse import unquote, urlsplit
ROOT = Path(__file__).resolve().parents[1]
def inspect(module: str, root: Path = ROOT) -> dict:
started = time.perf_counter()
public = (root / "web/public").resolve()
manifest = json.loads((public / "modules/manifest.json").read_text(encoding="utf-8"))
descriptor = manifest.get("modules", {}).get(module)
if not descriptor:
raise ValueError(f"module not published: {module}")
files = {}
for worker, artifact in descriptor.get("workers", {}).items():
for kind, field in (("runtime", "runtimeUrl"), ("wasm", "wasmUrl"), ("data", "dataUrl"), ("pthreadWorker", "pthreadWorkerUrl")):
url = artifact.get(field)
if not url and kind not in {"runtime", "wasm"}:
continue
if not isinstance(url, str):
raise ValueError(f"missing {field}: {worker}")
parts = urlsplit(url)
path = (public / unquote(parts.path).lstrip("/")).resolve()
if parts.scheme or parts.netloc or not path.is_relative_to(public):
raise ValueError(f"artifact must be inside local public directory: {url}")
expected = artifact.get("hashes", {}).get(kind)
if url not in files:
if not path.is_file():
files[url] = {"url": url, "present": False, "matches": False}
else:
with path.open("rb") as stream:
checksum = hashlib.file_digest(stream, "sha256").hexdigest()
files[url] = {"url": url, "path": str(path.relative_to(root)), "present": True,
"bytes": path.stat().st_size, "sha256": checksum, "matches": True}
files[url]["matches"] = files[url]["matches"] and isinstance(expected, str) and files[url].get("sha256") == expected
passed = bool(files) and all(row["present"] and row["matches"] for row in files.values())
return {"schema": "freecad-web-module-artifact-inspection/v1", "module": module, "status": "passed" if passed else "failed",
"executedAt": datetime.now(timezone.utc).isoformat(), "seconds": round(time.perf_counter() - started, 6),
"files": list(files.values()), "scope": "Published file presence and hash inspection; not a CAD business acceptance result."}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--module", required=True)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
try:
result = inspect(args.module)
except (OSError, ValueError) as error:
print(json.dumps({"status": "failed", "error": str(error)}))
return 1
if args.output:
output = (ROOT / args.output).resolve()
if not output.is_relative_to(ROOT):
parser.error("output must remain inside the project")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
print(json.dumps(result))
return 0 if result["status"] == "passed" else 1
if __name__ == "__main__":
raise SystemExit(main())