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
This commit is contained in:
@@ -18,6 +18,7 @@ pydantic = "^2.11.3"
|
|||||||
rich = "^14.0.0"
|
rich = "^14.0.0"
|
||||||
gitpython = "^3.1.44"
|
gitpython = "^3.1.44"
|
||||||
typer = "^0.16.0"
|
typer = "^0.16.0"
|
||||||
|
jinja2 = "^3.1.6"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
twine = "^6.1.0"
|
twine = "^6.1.0"
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import typer
|
import typer
|
||||||
from importlib.metadata import version, PackageNotFoundError
|
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.build import cli as build_cli
|
||||||
from hdlbuild.commands.clean import cli as clean_cli
|
from hdlbuild.commands.clean import cli as clean_cli
|
||||||
from hdlbuild.commands.dep import cli as dep_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"
|
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(build_cli, name="build", help="Build the project")
|
||||||
app.add_typer(clean_cli, name="clean", help="Clean build artifacts")
|
app.add_typer(clean_cli, name="clean", help="Clean build artifacts")
|
||||||
app.add_typer(dep_cli, name="dep", help="Resolve dependencies")
|
app.add_typer(dep_cli, name="dep", help="Resolve dependencies")
|
||||||
app.add_typer(test_cli, name="test", help="Run simulations/testbenches")
|
app.add_typer(test_cli, name="test", help="Run simulations/testbenches")
|
||||||
app.add_typer(init_cli, name="init", help="Initialize project")
|
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():
|
def main():
|
||||||
app()
|
app()
|
||||||
|
53
src/hdlbuild/commands/gen.py
Normal file
53
src/hdlbuild/commands/gen.py
Normal file
@@ -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 <name>` → render a specific template
|
||||||
|
* `hdlbuild gen <name> --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)
|
120
src/hdlbuild/generate/template_generator.py
Normal file
120
src/hdlbuild/generate/template_generator.py
Normal file
@@ -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}")
|
@@ -1,6 +1,8 @@
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from hdlbuild.models.templates import ProjectTemplates
|
||||||
|
|
||||||
class SourceFile(BaseModel):
|
class SourceFile(BaseModel):
|
||||||
path: str
|
path: str
|
||||||
library: str = "work" # Default auf 'work'
|
library: str = "work" # Default auf 'work'
|
||||||
@@ -43,6 +45,7 @@ class ProjectConfig(BaseModel):
|
|||||||
sources: Sources
|
sources: Sources
|
||||||
testbenches: Optional[Testbenches] = None
|
testbenches: Optional[Testbenches] = None
|
||||||
constraints: Optional[str] = None
|
constraints: Optional[str] = None
|
||||||
|
templates: Optional[ProjectTemplates] = None
|
||||||
build: Optional[BuildOptions] = None
|
build: Optional[BuildOptions] = None
|
||||||
dependencies: Optional[List[Dependency]] = Field(default_factory=list)
|
dependencies: Optional[List[Dependency]] = Field(default_factory=list)
|
||||||
tool_options: Optional[ToolOptions] = ToolOptions()
|
tool_options: Optional[ToolOptions] = ToolOptions()
|
||||||
|
14
src/hdlbuild/models/templates.py
Normal file
14
src/hdlbuild/models/templates.py
Normal file
@@ -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“
|
Reference in New Issue
Block a user