b9716a7cd6
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>
174 lines
5.7 KiB
Python
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())
|