Files
fn_registry/python/functions/infra/codegen_app_modules.py
T
egutierrez b9716a7cd6 chore: snapshot WIP previo + flow 0008 + 7 sub-issues (0112-0119)
Snapshot de WIP acumulado de sesiones previas antes de merge wave 1
del flow 0008 (kanban_cpp + agent_runner_api + DoD schema).

Incluye:
- dev/flows/0008-kanban-cpp-and-agent-workflows.md
- dev/issues/0112-0119*.md (7 sub-issues)
- WIP previo en cmd/fn/doctor.go, registry/*, modules/, cpp/, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:17:08 +02:00

174 lines
5.7 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 = []
# Toda app C++ procesada por add_imgui_app enlaza fn_framework. Inyectamos
# framework_cpp implicito si la app es C++ y no lo declara explicitamente.
# Asi el array embebido refleja la version real del framework linkeado,
# sin pedir a cada app.md que lo declare a mano.
lang = str(fm.get("lang", "")).lower()
if lang == "cpp":
already = [str(m) for m in uses_modules]
if not any(m.startswith("framework_") for m in already):
uses_modules = ["framework_cpp"] + already
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;")
# Header badge identity — auto-derivado del bloque `icon:` del app.md.
# Permite que el framework muestre el badge accent en viewports secundarios
# sin tocar main.cpp. Coherente con App Hub (mismo hex que la tarjeta).
icon_block = fm.get("icon") or {}
accent_hex = str(icon_block.get("accent", "") or "")
glyph_name = str(icon_block.get("phosphor", "") or "")
lines.append(
f'const char* const app_header_accent_hex = "{_escape_c_string(accent_hex)}";'
)
lines.append(
f'const char* const app_header_glyph_name = "{_escape_c_string(glyph_name)}";'
)
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())