Files
FreeCADWasmDev/tools/generate_web_object_inventory.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

113 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Inventory source-backed FreeCAD object/type registration evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import subprocess
from collections import defaultdict
from pathlib import Path
SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"}
TYPE_PATTERNS = {
"typesystem": re.compile(r"TYPESYSTEM_SOURCE(?:_WITH_OVERRIDE)?\s*\(\s*([A-Za-z_][\w:]*)"),
"property_header": re.compile(r"PROPERTY_HEADER(?:_WITH_OVERRIDE)?\s*\(\s*([A-Za-z_][\w:]*)"),
"add_object": re.compile(r"\b(?:addObject|addObjectWithName)\s*\(\s*\"([A-Za-z_][\w:]*)\""),
}
def revision(source_root: Path) -> str:
try:
return subprocess.run(
["git", "-C", str(source_root), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
except (OSError, subprocess.CalledProcessError):
return "unknown"
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def collect(source_root: Path, module: str) -> list[dict[str, object]]:
app_root = source_root / "src/Mod" / module / "App"
evidence: dict[str, list[dict[str, object]]] = defaultdict(list)
if not app_root.is_dir():
return []
for path in sorted(p for p in app_root.rglob("*") if p.is_file() and p.suffix in SOURCE_SUFFIXES):
text = path.read_text(encoding="utf-8", errors="replace")
lines = text.splitlines()
for kind, pattern in TYPE_PATTERNS.items():
for line_number, line in enumerate(lines, start=1):
for match in pattern.finditer(line):
name = match.group(1)
evidence[name].append(
{
"kind": kind,
"path": str(path.relative_to(source_root)),
"line": line_number,
"text": line.strip(),
}
)
rows = []
for name, matches in sorted(evidence.items()):
rows.append({"name": name, "module": module, "evidence": matches})
return rows
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source-root", type=Path, required=True)
parser.add_argument("--workbench-inventory", type=Path, required=True)
parser.add_argument("--json-output", type=Path, required=True)
parser.add_argument("--markdown-output", type=Path, required=True)
args = parser.parse_args()
source_root = args.source_root.resolve()
workbench = json.loads(args.workbench_inventory.read_text(encoding="utf-8"))
rows: list[dict[str, object]] = []
for module in workbench["modules"]:
rows.extend(collect(source_root, str(module["id"])))
names = {(row["module"], row["name"]) for row in rows}
report = {
"schema": "freecad-web-object-inventory/v1",
"source_root": str(source_root),
"source_revision": revision(source_root),
"workbench_inventory": str(args.workbench_inventory),
"object_count": len(rows),
"unique_name_count": len({row["name"] for row in rows}),
"duplicate_module_name_count": len(rows) - len(names),
"objects": rows,
}
args.json_output.parent.mkdir(parents=True, exist_ok=True)
args.json_output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
lines = [
"# FreeCAD Web Object Inventory",
"",
f"- Source revision: `{report['source_revision']}`",
f"- Object evidence rows: `{report['object_count']}`",
f"- Unique names: `{report['unique_name_count']}`",
"",
"| Module | Object/type name | Evidence count | First evidence |",
"|---|---|---:|---|",
]
for row in rows:
first = row["evidence"][0]
lines.append(
f"| `{row['module']}` | `{row['name']}` | {len(row['evidence'])} | "
f"`{first['path']}:{first['line']}` ({first['kind']}) |"
)
args.markdown_output.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(json.dumps({"schema": report["schema"], "object_count": report["object_count"], "unique_name_count": report["unique_name_count"]}))
return 0
if __name__ == "__main__":
raise SystemExit(main())