From af0477f8e74a9471c3e7d36877069592e41f651c Mon Sep 17 00:00:00 2001 From: "Max P." Date: Thu, 17 Jul 2025 10:53:48 +0200 Subject: [PATCH] feat(cli): add template generation commands - Introduces `gen` subcommand for HDL template generation - Adds Jinja2 dependency for template rendering - Updates project model to support template configurations - Implements template listing and rendering functionality --- pyproject.toml | 1 + src/hdlbuild/cli.py | 3 +- src/hdlbuild/commands/gen.py | 53 +++++++++ src/hdlbuild/generate/template_generator.py | 120 ++++++++++++++++++++ src/hdlbuild/models/project.py | 3 + src/hdlbuild/models/templates.py | 14 +++ 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/hdlbuild/commands/gen.py create mode 100644 src/hdlbuild/generate/template_generator.py create mode 100644 src/hdlbuild/models/templates.py diff --git a/pyproject.toml b/pyproject.toml index f745d19..e3e5972 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ pydantic = "^2.11.3" rich = "^14.0.0" gitpython = "^3.1.44" typer = "^0.16.0" +jinja2 = "^3.1.6" [tool.poetry.group.dev.dependencies] twine = "^6.1.0" diff --git a/src/hdlbuild/cli.py b/src/hdlbuild/cli.py index a0a0a19..d19b66c 100644 --- a/src/hdlbuild/cli.py +++ b/src/hdlbuild/cli.py @@ -1,6 +1,7 @@ import typer from importlib.metadata import version, PackageNotFoundError +from hdlbuild.commands.gen import cli as gen_cli from hdlbuild.commands.build import cli as build_cli from hdlbuild.commands.clean import cli as clean_cli from hdlbuild.commands.dep import cli as dep_cli @@ -18,12 +19,12 @@ app = typer.Typer( help=f"hdlbuild v{get_version()} – Build‑Management for FPGA projects" ) -# Unter‑Kommandos registrieren (entspricht add_subparsers) app.add_typer(build_cli, name="build", help="Build the project") app.add_typer(clean_cli, name="clean", help="Clean build artifacts") app.add_typer(dep_cli, name="dep", help="Resolve dependencies") app.add_typer(test_cli, name="test", help="Run simulations/testbenches") app.add_typer(init_cli, name="init", help="Initialize project") +app.add_typer(gen_cli, name="gen", help="Generate HDL files from templates") def main(): app() diff --git a/src/hdlbuild/commands/gen.py b/src/hdlbuild/commands/gen.py new file mode 100644 index 0000000..4eb147a --- /dev/null +++ b/src/hdlbuild/commands/gen.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import typer + +from hdlbuild.generate.template_generator import TemplateGenerator +from hdlbuild.utils.console_utils import ConsoleUtils +from hdlbuild.utils.project_loader import load_project_config + +cli = typer.Typer(rich_help_panel="🧬 Template Commands") + +@cli.command("list") +def list_templates() -> None: + """ + List all available template names from *project.yml*. + + ```bash + hdlbuild gen list + ``` + """ + console = ConsoleUtils("hdlbuild") + project = load_project_config() + TemplateGenerator.list_templates(project, console) + + +@cli.callback(invoke_without_command=True) +def gen( + ctx: typer.Context, + name: str = typer.Option( + None, + "--name", + "-n", + help="Name of the template to generate (from project.yml)", + show_default=False, + ), + dry_run: bool = typer.Option( + False, + "--dry-run", + help="Only show the output without writing file", + ), +) -> None: + """ + Render HDL files from Jinja2 templates. + + * `hdlbuild gen` → render all templates + * `hdlbuild gen ` → render a specific template + * `hdlbuild gen --dry-run` → only show output without saving + """ + console = ConsoleUtils("hdlbuild") + project = load_project_config() + + # Only executed when no subcommand (e.g., "list") is active. + if ctx.invoked_subcommand is None: + TemplateGenerator.generate(project, name, dry_run, console) diff --git a/src/hdlbuild/generate/template_generator.py b/src/hdlbuild/generate/template_generator.py new file mode 100644 index 0000000..0e02f3f --- /dev/null +++ b/src/hdlbuild/generate/template_generator.py @@ -0,0 +1,120 @@ +""" +hdlbuild.generate.template_generator +==================================== + +Enthält die Klasse :class:`TemplateGenerator`, die das Auflisten und Rendern +von in *project.yml* definierten Jinja2-Templates kapselt. +""" + +from __future__ import annotations + +import os +from typing import Optional + +from jinja2 import Environment, FileSystemLoader + +from hdlbuild.models.templates import TemplateInstance +from hdlbuild.utils.console_utils import ConsoleUtils + + +class TemplateGenerator: + """ + Hilfsklasse zum Auflisten und Rendern der im Projekt konfigurierten + Jinja2-Templates. + """ + + # --------------------------------------------------------------------- # + # Öffentliche API + # --------------------------------------------------------------------- # + + @staticmethod + def list_templates(project, console: ConsoleUtils) -> None: + """ + Alle in *project.yml* definierten Templates auflisten. + """ + if not project.templates: + console.print("[yellow]No templates defined in project.yml") + return + + console.print("[bold underline]Available Templates:") + for name in project.templates.root.keys(): + console.print(f"• {name}") + + @classmethod + def generate( + cls, + project, + name: Optional[str], + dry_run: bool, + console: ConsoleUtils, + ) -> None: + """ + Templates erzeugen. + + Parameters + ---------- + project + Geladenes Projekt-Model. + name + Name eines einzelnen Templates oder *None*, um alle Templates + zu erzeugen. + dry_run + Wenn *True*, wird das gerenderte Ergebnis nur ausgegeben, + jedoch nicht auf die Festplatte geschrieben. + console + Farbige Konsolen-Ausgaben. + """ + if not project.templates: + console.print("[red]No templates defined in project.yml") + return + + templates = project.templates.root + + if name: + # Ein bestimmtes Template + if name not in templates: + console.print(f"[red]Template '{name}' not found.") + return + cls._render_template(name, templates[name], dry_run, console) + else: + # Alle Templates durchlaufen + for tname, template in templates.items(): + cls._render_template(tname, template, dry_run, console) + + # --------------------------------------------------------------------- # + # Interne Helfer + # --------------------------------------------------------------------- # + + @staticmethod + def _render_template( + name: str, + template: TemplateInstance, + dry_run: bool, + console: ConsoleUtils, + ) -> None: + """ + Einzelnes Template rendern und wahlweise speichern. + """ + template_path = template.template + output_path = template.output + variables = template.variables + + env = Environment( + loader=FileSystemLoader(os.path.dirname(template_path)), + trim_blocks=True, + lstrip_blocks=True, + ) + j2 = env.get_template(os.path.basename(template_path)) + result = j2.render(**variables) + + if dry_run: + console.print(f"[green]--- Template: {name} (dry-run) ---") + console.print(result) + console.print(f"[green]--- End of {name} ---") + return + + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, "w") as f: + f.write(result) + + console.print(f"[cyan]✔ Rendered template '{name}' → {output_path}") diff --git a/src/hdlbuild/models/project.py b/src/hdlbuild/models/project.py index 4d75853..6154329 100644 --- a/src/hdlbuild/models/project.py +++ b/src/hdlbuild/models/project.py @@ -1,6 +1,8 @@ from pydantic import BaseModel, Field from typing import List, Optional +from hdlbuild.models.templates import ProjectTemplates + class SourceFile(BaseModel): path: str library: str = "work" # Default auf 'work' @@ -43,6 +45,7 @@ class ProjectConfig(BaseModel): sources: Sources testbenches: Optional[Testbenches] = None constraints: Optional[str] = None + templates: Optional[ProjectTemplates] = None build: Optional[BuildOptions] = None dependencies: Optional[List[Dependency]] = Field(default_factory=list) tool_options: Optional[ToolOptions] = ToolOptions() diff --git a/src/hdlbuild/models/templates.py b/src/hdlbuild/models/templates.py new file mode 100644 index 0000000..49aad55 --- /dev/null +++ b/src/hdlbuild/models/templates.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, Field, RootModel +from typing import Dict, Any + +class TemplateInstance(BaseModel): + template: str # Pfad zur Jinja2-Vorlage + output: str # Zielpfad + variables: Dict[str, Any] = Field(default_factory=dict) # Variablen für Rendering + +class ProjectTemplates(RootModel): + """ + Pydantic-RootModel, das die Mapping-Struktur *name → TemplateInstance* + kapselt. In Pydantic v2 ersetzt `RootModel` die frühere `__root__`-Syntax. + """ + root: Dict[str, TemplateInstance] # key = Name wie „alu“, „control_unit“