Compare commits

...

12 Commits

Author SHA1 Message Date
ef8dda20d8 chore(changelog): update changelog for v0.7.0
All checks were successful
Build and Publish nightly package / build-and-publish (release) Successful in 1m21s
2025-07-17 08:57:14 +00:00
8adaa916ff feat(version): bump version to 0.7.0
All checks were successful
Auto Changelog & (Release) / release (push) Successful in 13s
Build and Publish nightly package / build-and-publish (push) Successful in 1m29s
2025-07-17 10:56:58 +02:00
af0477f8e7 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
2025-07-17 10:56:58 +02:00
5d8d995a04 chore(changelog): update changelog for v0.6.1
All checks were successful
Build and Publish nightly package / build-and-publish (release) Successful in 2m11s
2025-07-16 09:50:07 +00:00
36f6e7314a chore(version): bump to 0.6.1
All checks were successful
Auto Changelog & (Release) / release (push) Successful in 1m8s
Build and Publish nightly package / build-and-publish (push) Successful in 2m24s
2025-07-16 11:48:54 +02:00
bc8ae46b4a chore(changelog): update unreleased changelog 2025-07-16 09:47:41 +00:00
dde2363ad7 chore(config): add git-cliff configuration file
All checks were successful
Auto Changelog & (Release) / release (push) Successful in 1m15s
Build and Publish nightly package / build-and-publish (push) Successful in 2m35s
2025-07-16 11:46:06 +02:00
32e3b51a77 chore(version): add version file for tracking 2025-07-16 11:41:54 +02:00
da387f2ee6 feat(ci): add workflows for nightly builds and releases
- Introduce separate workflows for nightly and release builds
- Add scripts for version management, asset uploads, and cleanup
- Improve Python environment setup and dependency caching
2025-07-16 11:41:35 +02:00
08679c2680 style(pyproject): simplify include array formatting 2025-07-16 11:41:18 +02:00
7b6f9ef224 chore: Updates project version to 0.6.0
All checks were successful
Build and Publish / build-and-publish (push) Successful in 1m55s
- Updates the project version in pyproject.toml
- Reflects changes for a new release
2025-07-16 11:30:18 +02:00
6ca389d5cb refactor: use typer for CLI argument parsing
- Replaces `argparse` with `typer` for command-line argument
  parsing to improve CLI interface and maintainability.
- Converts commands to subcommands using `typer.Typer()`.
- Streamlines CLI structure and enhances user experience.
2025-07-16 11:29:31 +02:00
25 changed files with 750 additions and 170 deletions

View File

@@ -0,0 +1,5 @@
chore(pr): ${PullRequestTitle} ${PullRequestReference}
${PullRequestDescription}
Merged from ${HeadBranch} into ${BaseBranch}

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
# cleanup_dev_versions.sh - Delete old PyPI dev versions from Gitea package registry
# Required environment variables
USERNAME="${TWINE_USERNAME}"
TOKEN="${TWINE_PASSWORD}"
REPO="${GITHUB_REPOSITORY}" # e.g., maxp/repocat
API_BASE="${GITHUB_API_URL%/}" # Strip trailing slash if present
OWNER="${REPO%%/*}"
PACKAGE_NAME="${REPO##*/}"
API_URL="${API_BASE}/packages/${OWNER}/pypi/${PACKAGE_NAME}"
# Fetch the list of versions
response=$(curl -s -u "$USERNAME:$TOKEN" "$API_URL")
# Extract all .dev versions, sort by creation time
mapfile -t versions_to_delete < <(echo "$response" | jq -r '
map(select(.version | test("\\.dev"))) |
sort_by(.created_at) |
.[0:-1][] |
.version')
# Determine latest version to keep
latest_version=$(echo "$response" | jq -r '
map(select(.version | test("\\.dev"))) |
sort_by(.created_at) |
last.version')
if [[ -z "$latest_version" || ${#versions_to_delete[@]} -eq 0 ]]; then
echo "No old .dev versions to delete."
exit 0
fi
echo "Keeping latest .dev version: $latest_version"
# Delete old .dev versions
for version in "${versions_to_delete[@]}"; do
echo "Deleting old .dev version: $version"
curl -s -X DELETE -u "$USERNAME:$TOKEN" "$API_URL/$version"
done
echo "Cleanup complete."

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
# Eingaben
TAG="$1"
TOKEN="${ACTIONS_RUNTIME_TOKEN:-<fallback_token>}"
REPO="${GITHUB_REPOSITORY:-owner/example}"
API="${GITHUB_API_URL:-https://gitea.example.tld/api/v1}"
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
RESPONSE=$(curl -sf \
-H "Authorization: token $TOKEN" \
"$API/repos/$OWNER/$NAME/releases/tags/$TAG")
RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id')
echo "Release-ID für $TAG ist: $RELEASE_ID"
# Für GitHub Actions als Umgebungsvariable
echo "GT_RELEASE_ID=$RELEASE_ID" >> "$GITHUB_ENV"

View File

@@ -0,0 +1,14 @@
#!/bin/bash
BASE_VERSION=$(cat VERSION)
NIGHTLY_SUFFIX=""
if [[ "$1" == "nightly" ]]; then
# Beispiel: 20240511.1358 → 11. Mai, 13:58 Uhr
NIGHTLY_SUFFIX=".dev$(date +%Y%m%d%H%M)"
fi
FULL_VERSION="${BASE_VERSION}${NIGHTLY_SUFFIX}"
echo "Using version: $FULL_VERSION"
poetry version "$FULL_VERSION"

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
# Stelle sicher, dass wir im Projektverzeichnis sind
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT_DIR"
PYPROJECT="pyproject.toml"
VERSION_FILE="VERSION"
# Extrahiere die Version mit grep + sed (keine externen Abhängigkeiten nötig)
VERSION=$(grep -E '^version\s*=' "$PYPROJECT" | head -n1 | sed -E 's/.*=\s*"([^"]+)".*/\1/')
if [[ -z "$VERSION" ]]; then
echo "❌ Version konnte nicht aus $PYPROJECT gelesen werden."
exit 1
fi
printf "%s" "$VERSION" > "$VERSION_FILE"
echo "✅ Version synchronisiert: $VERSION$VERSION_FILE"

40
.gitea/scripts/upload-asset.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
# Eingabeparameter
FILE_PATH="$1" # z. B. ./dist/build.zip
CUSTOM_NAME="${2:-}" # optional: anderer Name unter dem das Asset gespeichert werden soll
RELEASE_ID="${GT_RELEASE_ID:-}" # aus Umgebung
# Validierung
if [[ -z "$RELEASE_ID" ]]; then
echo "❌ RELEASE_ID ist nicht gesetzt. Abbruch."
exit 1
fi
if [[ ! -f "$FILE_PATH" ]]; then
echo "❌ Datei '$FILE_PATH' existiert nicht. Abbruch."
exit 1
fi
# Default-Konfiguration
TOKEN="${ACTIONS_RUNTIME_TOKEN:-<fallback_token>}"
REPO="${GITHUB_REPOSITORY:-owner/example}"
API="${GITHUB_API_URL:-https://gitea.example.tld/api/v1}"
# Owner/Repo auflösen
OWNER=$(echo "$REPO" | cut -d/ -f1)
NAME=$(echo "$REPO" | cut -d/ -f2)
# Dateiname setzen
FILENAME="${CUSTOM_NAME:-$(basename "$FILE_PATH")}"
echo "🔼 Uploading '$FILE_PATH' as '$FILENAME' to release ID $RELEASE_ID"
# Upload
curl -sf -X POST \
-H "Authorization: token $TOKEN" \
-F "attachment=@$FILE_PATH" \
"$API/repos/$OWNER/$NAME/releases/$RELEASE_ID/assets?name=$FILENAME"
echo "✅ Upload abgeschlossen: $FILENAME"

View File

@@ -1,30 +1,33 @@
# .gitea/workflows/publish.yml
name: Build and Publish
name: Build and Publish nightly package
on:
workflow_dispatch:
push:
paths:
- "pyproject.toml" # Nur bei Änderungen an dieser Datei
workflow_dispatch: # Manuelles Anstoßen zulassen
branches:
- main
paths-ignore:
- 'CHANGELOG.md'
jobs:
build-and-publish:
runs-on: ubuntu-22.04
steps:
- name: ⚙️ Prepare Environment Variables
run: |
echo "AGENT_TOOLSDIRECTORY=/home/runner/toolcache" >> $GITHUB_ENV
mkdir -p /home/runner/toolcache
- name: Checkout Repository
uses: actions/checkout@v4
- name: 🐍 Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: 🔄 Restore cache
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: poetry-v1-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
paths: |
~/.cache/pypoetry
~/.cache/pip
- name: Install Poetry
run: |
@@ -35,6 +38,9 @@ jobs:
run: |
poetry install
- name: Set version from VERSION file (with nightly suffix)
run: ./.gitea/scripts/set_poetry_version.sh nightly
- name: Build Package
working-directory: .
run: |
@@ -47,3 +53,10 @@ jobs:
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
run: |
poetry run twine upload --repository-url ${{ secrets.TWINE_URL }} dist/*
- name: Cleanup old dev versions
run: |
.gitea/scripts/cleanup_versions.sh '\.dev'
env:
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}

View File

@@ -0,0 +1,64 @@
name: Build and Publish nightly package
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: 🐍 Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: 🔄 Restore cache
uses: https://git.0xmax42.io/actions/cache@v1
with:
key: poetry-v1-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
paths: |
~/.cache/pypoetry
~/.cache/pip
- name: Install Poetry
run: |
pip install poetry
- name: Install Project Dependencies
working-directory: .
run: |
poetry install
- name: Build Package
working-directory: .
run: |
poetry build
- name: Get built wheel filename
id: get_whl
run: |
echo "whl_file=$(basename dist/*.whl)" >> $GITHUB_OUTPUT
echo "sdist_file=$(basename dist/*.tar.gz)" >> $GITHUB_OUTPUT
- name: Publish to Gitea Package Registry
working-directory: .
env:
TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
run: |
poetry run twine upload --repository-url ${{ secrets.TWINE_URL }} dist/*
- name: Get Release ID from tag
run: .gitea/scripts/get-release-id.sh "${{ github.event.release.tag_name }}"
- name: Upload assets
run: |
.gitea/scripts/upload-asset.sh ./dist/${{ steps.get_whl.outputs.whl_file }}
.gitea/scripts/upload-asset.sh ./dist/${{ steps.get_whl.outputs.sdist_file }}

View File

@@ -0,0 +1,18 @@
name: Auto Changelog & (Release)
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Release
uses: https://git.0xmax42.io/actions/auto-changelog-release-action@v0
with:
token: ${{ secrets.RELEASE_PUBLISH_TOKEN }}

38
CHANGELOG.md Normal file
View File

@@ -0,0 +1,38 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.7.0](https://git.0xmax42.io/maxp/hdlbuild/compare/v0.6.1..v0.7.0) - 2025-07-17
### 🚀 Features
- *(version)* Bump version to 0.7.0 - ([8adaa91](https://git.0xmax42.io/maxp/hdlbuild/commit/8adaa916ff4e736e9da707c232d6f57b788e57e8))
- *(cli)* Add template generation commands - ([af0477f](https://git.0xmax42.io/maxp/hdlbuild/commit/af0477f8e74a9471c3e7d36877069592e41f651c))
## [0.6.1] - 2025-07-16
### 🚀 Features
- *(ci)* Add workflows for nightly builds and releases - ([da387f2](https://git.0xmax42.io/maxp/hdlbuild/commit/da387f2ee602390d616c79bf4057ccf941e21462))
### 🚜 Refactor
- Use typer for CLI argument parsing - ([6ca389d](https://git.0xmax42.io/maxp/hdlbuild/commit/6ca389d5cbbeff53faab9d61376a8c77ed097b6c))
- Improves project configuration - ([175bf48](https://git.0xmax42.io/maxp/hdlbuild/commit/175bf4882a8f172ee536d726b31136690572be36))
### 📚 Documentation
- *(readme)* Expand README with detailed usage and setup - ([fa7e738](https://git.0xmax42.io/maxp/hdlbuild/commit/fa7e738b7eade5a627218741a6fb4bd1617f7801))
### 🎨 Styling
- *(pyproject)* Simplify include array formatting - ([08679c2](https://git.0xmax42.io/maxp/hdlbuild/commit/08679c2680b49119e0414688a80e8dc2659236b4))
- Updates VS Code editor color scheme - ([1d7bc19](https://git.0xmax42.io/maxp/hdlbuild/commit/1d7bc1996522ab54970348b5118ad319849a6a1f))
### ⚙️ Miscellaneous Tasks
- *(config)* Add git-cliff configuration file - ([dde2363](https://git.0xmax42.io/maxp/hdlbuild/commit/dde2363ad7dd2fd2d48c6154e3b88c1c4a6867fd))
- Updates project version to 0.6.0 - ([7b6f9ef](https://git.0xmax42.io/maxp/hdlbuild/commit/7b6f9ef2240864b103903e79c895a76db59c14fa))
- Remove build-deb.yml workflow file - ([0d26c42](https://git.0xmax42.io/maxp/hdlbuild/commit/0d26c42f8ae419d509aa47d3f7a23bfdd08cf79b))

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.7.0

104
cliff.toml Normal file
View File

@@ -0,0 +1,104 @@
# CLIFF_VERSION=2.8.0
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[remote.gitea]
owner = "maxp"
repo = "hdlbuild"
[changelog]
# postprocessors
postprocessors = [
{ pattern = '<GITEA_URL>', replace = "https://git.0xmax42.io" }, # replace gitea url
]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{%- macro remote_url() -%}
<GITEA_URL>/{{ remote.gitea.owner }}/{{ remote.gitea.repo }}
{%- endmacro -%}
{% if version %}\
{% if previous.version %}\
## [{{ version | trim_start_matches(pat="v") }}]\
({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}\
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }} - \
([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% endfor %}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
"""
# remove the leading and trailing s
trim = true
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^chore\\(version\\)", skip = true },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps.*\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
{ message = ".*", group = "<!-- 10 -->💼 Other" },
]
# Regex to select git tags that represent releases.
tag_pattern = "v[0-9]+\\.[0-9]+\\.[0-9]+"
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

View File

@@ -1,14 +1,12 @@
[tool.poetry]
name = "hdlbuild"
version = "0.5.3"
version = "0.7.0"
description = "Flexible FPGA Build System"
authors = ["0xMax42 <Mail@0xMax42.io>"]
license = "MIT"
readme = "README.md"
packages = [{ include = "hdlbuild", from = "src" }]
include = [
"src/hdlbuild/templates/*"
]
include = ["src/hdlbuild/templates/*"]
[tool.poetry.scripts]
hdlbuild = "hdlbuild.cli:main"
@@ -19,6 +17,8 @@ pyyaml = "^6.0.2"
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"

View File

@@ -1,32 +1,33 @@
import argparse
import typer
from importlib.metadata import version, PackageNotFoundError
from hdlbuild.commands import register_commands
def get_version():
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
from hdlbuild.commands.test import cli as test_cli
from hdlbuild.commands.init import cli as init_cli
def get_version() -> str:
try:
return version("hdlbuild") # Paketname aus pyproject.toml
return version("hdlbuild")
except PackageNotFoundError:
return "unknown"
app = typer.Typer(
rich_help_panel="ℹ️ HDLBuild – FPGA‑Build‑Tool",
help=f"hdlbuild v{get_version()} – Build‑Management for FPGA projects"
)
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():
version_str = get_version()
parser = argparse.ArgumentParser(
description=f"hdlbuild v{version_str} - Build management tool for FPGA projects",
formatter_class=argparse.RawTextHelpFormatter
)
subparsers = parser.add_subparsers(
title="Commands",
description="Available commands",
dest="command",
required=True
)
# Register all commands
register_commands(subparsers)
args = parser.parse_args()
args.func(args)
app()
if __name__ == "__main__":
main()

View File

@@ -1 +0,0 @@
from .commands import register_commands

View File

@@ -1,30 +1,34 @@
from hdlbuild.tools.xilinx_ise.main import xilinx_ise_all, xilinx_ise_synth
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.directory_manager import ensure_directories_exist
from hdlbuild.utils.project_loader import load_project_config
import typer
class BuildCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
from hdlbuild.tools.xilinx_ise.main import xilinx_ise_all, xilinx_ise_synth
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.directory_manager import ensure_directories_exist
from hdlbuild.utils.project_loader import load_project_config
def register(self, subparsers):
parser = subparsers.add_parser("build", help="Start the build process")
parser.add_argument(
"target",
nargs="?",
choices=["synth"],
help="Specify 'synth' to only synthesize the design (optional)"
)
parser.set_defaults(func=self.execute)
cli = typer.Typer(rich_help_panel="🔨 Build Commands")
def execute(self, args):
"""Starts the build process."""
self.project = load_project_config()
if args.target == "synth":
self.console_utils.print("Starting synth process...")
ensure_directories_exist(True)
xilinx_ise_synth(self.project)
else:
self.console_utils.print("Starting build process...")
ensure_directories_exist(True)
xilinx_ise_all(self.project)
@cli.callback(invoke_without_command=True)
def build(
target: str = typer.Argument(
None,
help="Optional: 'synth' to run synthesis only",
show_default=False,
rich_help_panel="🔨 Build Commands",
)
) -> None:
"""
Run the full build flow or synthesis only.
* `hdlbuild build` → full flow
* `hdlbuild build synth` → synthesis only
"""
console = ConsoleUtils("hdlbuild")
project = load_project_config()
ensure_directories_exist(True)
if target == "synth":
console.print("Starting synthesis …")
xilinx_ise_synth(project)
else:
console.print("Starting full build …")
xilinx_ise_all(project)

View File

@@ -1,27 +1,35 @@
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.directory_manager import clear_build_directories, clear_directories
import typer
class CleanCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.directory_manager import clear_build_directories, clear_directories
def register(self, subparsers):
parser = subparsers.add_parser("clean", help="Clean build artifacts")
parser.add_argument(
"target",
nargs="?",
choices=["all"],
help="Specify 'all' to clean everything (optional)"
)
parser.set_defaults(func=self.execute)
cli = typer.Typer(rich_help_panel="🧹 Clean Commands")
def execute(self, args):
"""Cleans the build artifacts."""
if args.target == "all":
self.console_utils.print("Starting clean all process...")
clear_directories()
self.console_utils.print("All cleaned.")
else:
self.console_utils.print("Clearing build artifacts...")
clear_build_directories()
self.console_utils.print("Build artifacts cleaned.")
@cli.callback(invoke_without_command=True)
def clean(
target: str = typer.Argument(
None,
help="Optional: 'all' → wipe *all* artefacts, otherwise only the build directory",
show_default=False,
)
) -> None:
"""
Remove build artefacts (`build/*`) or *everything* (`all`).
Examples
--------
```bash
hdlbuild clean # build/* and temporary files only
hdlbuild clean all # also caches, logs, etc.
```
"""
console = ConsoleUtils("hdlbuild")
if target == "all":
console.print("Starting clean‑all …")
clear_directories()
console.print("All artefacts removed.")
else:
console.print("Removing build artefacts …")
clear_build_directories()
console.print("Build artefacts removed.")

View File

@@ -1,19 +0,0 @@
from hdlbuild.commands.build import BuildCommand
from hdlbuild.commands.clean import CleanCommand
from hdlbuild.commands.dep import DepCommand
from hdlbuild.commands.init import InitCommand
from hdlbuild.commands.test import TestCommand
def register_commands(subparsers):
"""Registers all available commands."""
commands = [
CleanCommand(),
BuildCommand(),
DepCommand(),
TestCommand(),
InitCommand(),
]
for command in commands:
command.register(subparsers)

View File

@@ -1,17 +1,23 @@
from hdlbuild.dependencies.resolver import DependencyResolver
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.project_loader import load_project_config
import typer
class DepCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
from hdlbuild.dependencies.resolver import DependencyResolver
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.project_loader import load_project_config
def register(self, subparsers):
parser = subparsers.add_parser("dep", help="Start the dependencies process")
parser.set_defaults(func=self.execute)
cli = typer.Typer(rich_help_panel="🔗 Dependency Commands")
def execute(self, args):
"""Starts the dependencies process."""
self.project = load_project_config()
self.console_utils.print("Starting dependencies process...")
DependencyResolver(self.project).resolve_all()
@cli.callback(invoke_without_command=True)
def dep() -> None:
"""
Resolve all project dependencies.
```bash
hdlbuild dep
```
"""
console = ConsoleUtils("hdlbuild")
project = load_project_config()
console.print("Resolving dependencies …")
DependencyResolver(project).resolve_all()
console.print("Dependencies resolved.")

View 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)

View File

@@ -1,36 +1,35 @@
from pathlib import Path
import shutil
from hdlbuild.dependencies.resolver import DependencyResolver
from pathlib import Path
import typer
from hdlbuild.utils.console_utils import ConsoleUtils
class InitCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
cli = typer.Typer(rich_help_panel="🆕 Init Commands")
def register(self, subparsers):
parser = subparsers.add_parser("init", help="Initialize a new HDLBuild project")
parser.set_defaults(func=self.execute)
@cli.callback(invoke_without_command=True)
def init() -> None:
"""
Initialise a new HDLBuild project in the current directory.
def execute(self, args):
"""Initialize a new HDLBuild project."""
project_dir = Path.cwd()
Copies `.gitignore` and `project.yml` from the template folder.
"""
console = ConsoleUtils("hdlbuild")
project_dir = Path.cwd()
# Correctly resolve path to templates directory
script_dir = Path(__file__).parent.resolve()
template_dir = (script_dir / ".." / "templates").resolve()
script_dir = Path(__file__).parent.resolve()
template_dir = (script_dir / ".." / "templates").resolve()
# Files to copy
files = [
("gitignore.template", ".gitignore"),
("project.yml.template", "project.yml"),
]
files = [
("gitignore.template", ".gitignore"),
("project.yml.template", "project.yml"),
]
for template_name, target_name in files:
template_path = template_dir / template_name
target_path = project_dir / target_name
for template_name, target_name in files:
template_path = template_dir / template_name
target_path = project_dir / target_name
if not target_path.exists():
shutil.copy(template_path, target_path)
self.console_utils.print(f"Created {target_name}")
else:
self.console_utils.print(f"{target_name} already exists, skipping.")
if not target_path.exists():
shutil.copy(template_path, target_path)
console.print(f"Created {target_name}")
else:
console.print(f"{target_name} already exists skipping.")

View File

@@ -1,23 +1,31 @@
from hdlbuild.tools.xilinx_ise.isim import build_testbench, run_testbench
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.project_loader import load_project_config
import typer
class TestCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
from hdlbuild.tools.xilinx_ise.isim import build_testbench, run_testbench
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.project_loader import load_project_config
def register(self, subparsers):
parser = subparsers.add_parser("test", help="Start the Tests process")
parser.add_argument(
"target",
nargs="?",
help="Select the target to test"
)
parser.set_defaults(func=self.execute)
cli = typer.Typer(rich_help_panel="🧪 Test Commands")
def execute(self, args):
"""Starts the test process."""
self.project = load_project_config()
self.console_utils.print("Starting test process...")
build_testbench(self.project, args.target)
run_testbench(self.project, args.target)
@cli.callback(invoke_without_command=True)
def test(
target: str = typer.Argument(
None,
help="Name of the test target (leave empty to run all)",
show_default=False,
)
) -> None:
"""
Build and run testbenches.
```bash
hdlbuild test # run all TBs
hdlbuild test alu # run TB 'alu' only
```
"""
console = ConsoleUtils("hdlbuild")
project = load_project_config()
console.print("Starting test flow …")
build_testbench(project, target)
run_testbench(project, target)
console.print("Tests finished.")

View 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}")

View File

@@ -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()

View 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“