113 lines
4.3 KiB
Python
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())
|