Files
fn_registry/python/functions/infra/codegen_app_modules.py
T

150 lines
4.6 KiB
Python

"""Generate <app>_modules_generated.cpp from app.md uses_modules + modules/*/module.md.
Stand-alone — no dependencies beyond PyYAML. Invoked from CMake at configure time.
Reads YAML frontmatter directly (no registry.db dependency, no Go binary).
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import Optional
import yaml
def _read_frontmatter(md_path: Path) -> dict:
if not md_path.exists():
return {}
text = md_path.read_text(encoding="utf-8")
if not text.startswith("---\n") and not text.startswith("---\r\n"):
return {}
end = text.find("\n---", 4)
if end < 0:
return {}
raw = text[4:end]
try:
return yaml.safe_load(raw) or {}
except yaml.YAMLError:
return {}
def _escape_c_string(s: str) -> str:
out = []
for ch in s or "":
if ch == "\\":
out.append("\\\\")
elif ch == '"':
out.append('\\"')
elif ch == "\n":
out.append("\\n")
elif ch == "\r":
out.append("\\r")
elif ch == "\t":
out.append("\\t")
elif ord(ch) < 32:
out.append(f"\\x{ord(ch):02x}")
else:
out.append(ch)
return "".join(out)
def _resolve_module(modules_root: Path, mod_id: str) -> Optional[dict]:
"""mod_id is e.g. `data_table_cpp`. Lookup module.md by name (strip _<lang>)."""
name = mod_id
for suffix in ("_cpp", "_py", "_ts", "_bash", "_go"):
if name.endswith(suffix):
name = name[: -len(suffix)]
break
md = modules_root / name / "module.md"
fm = _read_frontmatter(md)
if not fm:
return None
return {
"name": fm.get("name", name),
"version": fm.get("version", "0.0.0"),
"description": fm.get("description", ""),
}
def generate(app_md: Path, modules_root: Path, app_name: str, out_path: Path) -> int:
fm = _read_frontmatter(app_md)
uses_modules = fm.get("uses_modules") or []
if not isinstance(uses_modules, list):
uses_modules = []
entries: list[dict] = []
missing: list[str] = []
for mid in uses_modules:
info = _resolve_module(modules_root, str(mid))
if info is None:
missing.append(str(mid))
continue
entries.append(info)
lines: list[str] = []
lines.append(f"// Auto-generated by codegen_app_modules.py — do not edit.")
lines.append(f"// App: {app_name}")
lines.append(f"// Source of truth: {app_md.as_posix()} (uses_modules)")
lines.append("")
lines.append('#include "app_modules.h"')
lines.append("")
lines.append("namespace fn {")
if entries:
lines.append("const ModuleInfo app_modules_array[] = {")
for e in entries:
lines.append(
' { "%s", "%s", "%s" },'
% (
_escape_c_string(e["name"]),
_escape_c_string(e["version"]),
_escape_c_string(e["description"]),
)
)
lines.append("};")
lines.append(f"const unsigned long app_modules_count = {len(entries)};")
else:
lines.append("const ModuleInfo app_modules_array[1] = { { nullptr, nullptr, nullptr } };")
lines.append("const unsigned long app_modules_count = 0;")
lines.append("} // namespace fn")
lines.append("")
new_content = "\n".join(lines)
# Idempotent: skip rewrite when content matches.
if out_path.exists() and out_path.read_text(encoding="utf-8") == new_content:
return 0 if not missing else 2
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(new_content, encoding="utf-8")
if missing:
sys.stderr.write(
f"codegen_app_modules: WARNING — module(s) not found: {', '.join(missing)} "
f"(app {app_name})\n"
)
return 2
return 0
def main() -> int:
ap = argparse.ArgumentParser(description="Generate <app>_modules_generated.cpp from app.md")
ap.add_argument("--app-md", required=True, help="Path to app.md")
ap.add_argument("--modules-root", required=True, help="Path to modules/ root")
ap.add_argument("--app-name", required=True, help="App name (for comment header)")
ap.add_argument("--out", required=True, help="Output path for generated .cpp")
args = ap.parse_args()
rc = generate(
app_md=Path(args.app_md),
modules_root=Path(args.modules_root),
app_name=args.app_name,
out_path=Path(args.out),
)
return 0 if rc in (0, 2) else rc
if __name__ == "__main__":
sys.exit(main())