Compare commits

...

26 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
1d7bc19965 style: Updates VS Code editor color scheme
- Updates the VS Code settings to customize the color scheme for
  the activity bar and activity bar badge.
- Enhances the visual appearance and user experience of the editor.
2025-07-16 11:04:58 +02:00
175bf4882a refactor: improves project configuration
- Removes unnecessary fields and configures the project.
- Streamlines tool options for clarity.
- Simplifies dependencies.
2025-07-16 11:04:44 +02:00
0d26c42f8a chore: remove build-deb.yml workflow file
Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-01 17:44:36 +02:00
fa7e738b7e docs(readme): expand README with detailed usage and setup
- Add comprehensive overview of HDLBuild features and functionality
- Include installation prerequisites and step-by-step instructions
- Document CLI commands with examples for common tasks
- Provide example `project.yml` configuration for customization
- Add sections on contributing, license, and GitHub Actions
- Enhance clarity and usability for new users and contributors

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-01 17:43:12 +02:00
60c4ca19b1 Remove push trigger for main branch in build-deb.yml 2025-04-28 10:37:37 +02:00
38d4c7d5f3 Add twine to development dependencies in pyproject.toml
Some checks failed
Build Wheels / build (push) Has been cancelled
Build and Publish / build-and-publish (push) Successful in 30s
2025-04-28 10:36:30 +02:00
9d33b6e8be Update workflow to use Ubuntu 22.04 for build and publish job 2025-04-28 10:35:13 +02:00
dde3b5dc5d Add GitHub Actions workflow for building and publishing to Gitea 2025-04-28 10:34:33 +02:00
6d6b731033 Bump version from 0.5.2 to 0.5.3 in pyproject.toml
All checks were successful
Build Wheels / build (push) Successful in 3m36s
2025-04-27 18:56:40 +00:00
8196eb3929 Update gitignore to include .locale and vhdl_ls.toml 2025-04-27 18:56:34 +00:00
5083852c29 Bump version from 0.5.1 to 0.5.2 in pyproject.toml
All checks were successful
Build Wheels / build (push) Successful in 4m8s
2025-04-27 18:52:00 +00:00
215e4aa545 Refactors project initialization logic
Simplifies file handling by consolidating template copying logic
into a single loop. Corrects the resolution of the templates
directory path for improved reliability.

Improves maintainability and reduces redundancy in code.
2025-04-27 18:51:46 +00:00
d78bfcc408 Bump version from 0.5.0 to 0.5.1 in pyproject.toml
All checks were successful
Build Wheels / build (push) Successful in 3m38s
2025-04-27 18:40:23 +00:00
8ed550f451 Defers project loading to execution phase
Removes project loading from command initialization and shifts it to the execution phase for better resource management and initialization performance.

Improves flexibility by ensuring the project is only loaded when needed.
2025-04-27 18:39:58 +00:00
30 changed files with 978 additions and 275 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

@@ -0,0 +1,62 @@
name: Build and Publish nightly package
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- 'CHANGELOG.md'
jobs:
build-and-publish:
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- 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: Set version from VERSION file (with nightly suffix)
run: ./.gitea/scripts/set_poetry_version.sh nightly
- name: Build Package
working-directory: .
run: |
poetry build
- 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: 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 }}

View File

@@ -1,97 +0,0 @@
name: Build Wheels
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
jobs:
build:
runs-on: ubuntu-latest
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"
- name: 📦 Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
echo "$HOME/.local/bin" >> $GITHUB_PATH
poetry self add poetry-plugin-export
- name: 🔧 Install Project Dependencies
run: |
poetry install
- name: 📝 Export requirements.txt
run: |
poetry export --without-hashes --format=requirements.txt > requirements.txt
- name: 📥 Download Wheels for Dependencies
run: |
mkdir -p dist/wheels
poetry run pip install wheel
poetry run pip download --only-binary=:all: --dest dist -r requirements.txt
- name: 🛠 Build Project Wheel
run: |
poetry build -f wheel
- name: 🔌 Install pipx and wheel2deb
run: |
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx install wheel2deb
- name: 📦 Install wheel2deb Dependencys
run: |
sudo apt update
sudo apt install -y apt-file dpkg-dev fakeroot build-essential devscripts debhelper
sudo apt-file update
- name: 🛠 Convert Wheel to DEB
run: |
mkdir -p dist/debs
wheel2deb default -v -x dist -o dist/debs
- name: 📦 Clone `packages`-Branch in ./packages
run: |
REPO_URL_BASE="$(echo $GITHUB_SERVER_URL/${{ secrets.TEA_USER }}/debrepo.git | sed 's/^https\?:\/\///')"
REPO_URL="https://${{ secrets.TEA_USER }}:${{ secrets.TEA_PAT }}@$REPO_URL_BASE"
echo "::add-mask::$REPO_URL"
echo "📡 Klone Repository von: $REPO_URL"
git clone --depth 1 --branch packages "$REPO_URL" packages
- name: 📂 Ensure `./packages/hdlbuild` Exists and Copy DEBs
run: |
mkdir -p ./packages/hdlbuild
find dist/debs -type f -name "*.deb" -exec cp {} ./packages/hdlbuild/ \;
- name: 📝 Commit & Push immer
run: |
cd packages
git config user.name "Gitea CI"
git config user.email "ci@yourdomain"
git add hdlbuild/
git commit -m "🔄 Update hdlbuild $(date -u +'%Y-%m-%d %H:%M UTC')" || echo "🧊 Keine Änderungen – leerer Commit"
REPO_URL_BASE="$(echo $GITHUB_SERVER_URL/${{ secrets.TEA_USER }}/debrepo.git | sed 's/^https\?:\/\///')"
REPO_URL="https://${{ secrets.TEA_USER }}:${{ secrets.TEA_PAT }}@$REPO_URL_BASE"
echo "::add-mask::$REPO_URL"
git remote set-url origin "$REPO_URL"
git push origin packages

11
.vscode/settings.json vendored
View File

@@ -2,5 +2,14 @@
"python.envFile": "${workspaceFolder}/.env",
"python.analysis.extraPaths": [
"src"
]
],
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#8dc4ff",
"activityBar.background": "#8dc4ff",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#ff007b",
"activityBarBadge.foreground": "#e7e7e7"
},
"peacock.color": "#5aaaff"
}

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

168
README.md
View File

@@ -1,2 +1,168 @@
# hdlbuild
# HDLBuild
HDLBuild is a flexible build management tool for FPGA projects. It simplifies the process of managing dependencies, building, testing, and deploying FPGA designs using Xilinx ISE tools.
## Features
- **Dependency Management**: Automatically resolves and manages project dependencies from Git repositories.
- **Build Automation**: Supports synthesis, implementation, and bitstream generation for FPGA designs.
- **Testbench Execution**: Automates the process of building and running testbenches.
- **Customizable Tool Options**: Provides extensive configuration options for Xilinx ISE tools.
- **Project Initialization**: Quickly set up new projects with predefined templates.
- **Rich Console Output**: Provides detailed and interactive console feedback using `rich`.
---
## Installation
### Prerequisites
- Python 3.10 or higher
- Poetry (for dependency management)
- Xilinx ISE (14.7) installed and configured
### Steps
1. Clone the repository:
```bash
git clone https://github.com/your-repo/hdlbuild.git
cd hdlbuild
```
2. Install dependencies:
```bash
poetry install
```
3. Add the `hdlbuild` CLI to your PATH:
```bash
poetry shell
```
---
## Usage
### CLI Commands
HDLBuild provides a command-line interface (CLI) for managing FPGA projects. Below are the available commands:
#### 1. **Initialize a New Project**
```bash
hdlbuild init
```
- Creates a new project with a project.yml configuration file and a .gitignore file.
#### 2. **Resolve Dependencies**
```bash
hdlbuild dep
```
- Clones and resolves all project dependencies defined in project.yml.
#### 3. **Build the Project**
```bash
hdlbuild build
```
- Runs the full build process, including synthesis, implementation, and bitstream generation.
- To only synthesize the design:
```bash
hdlbuild build synth
```
#### 4. **Run Testbenches**
```bash
hdlbuild test <testbench_name>
```
- Builds and runs the specified testbench.
#### 5. **Clean Build Artifacts**
```bash
hdlbuild clean
```
- Removes build artifacts.
- To clean all generated files:
```bash
hdlbuild clean all
```
---
## Configuration
The project is configured using a project.yml file. Below is an example configuration:
```yml
name: MyFPGAProject
topmodule: top_module
target_device: xc3s1200e-4-fg320
xilinx_path: /opt/Xilinx/14.7/ISE_DS/ISE
constraints: constraints.ucf
sources:
vhdl:
- path: src/*.vhd
library: work
testbenches:
vhdl:
- path: tests/*.vhd
library: work
dependencies:
- git: "https://github.com/example/dependency.git"
rev: "main"
build:
build_dir: working
report_dir: reports
copy_target_dir: output
tool_options:
xst:
- "-opt_mode Speed"
- "-opt_level 2"
map:
- "-detail"
- "-timing"
par: []
bitgen:
- "-g StartupClk:JtagClk"
```
---
## Development
### Building the Package
To build the Python package:
```bash
poetry build
```
---
## GitHub Actions
The project includes GitHub workflows for building and deploying the package:
1. **Build and Publish**: build-and-deploy.yml
---
## License
This project is licensed under the MIT License.
---
## Contributing
Contributions are welcome! Please follow these steps:
1. Fork the repository.
2. Create a new branch for your feature or bugfix.
3. Submit a pull request.

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,27 +1,23 @@
name: VGA
topmodule: VGA_Test
name:
topmodule:
target_device: xc3s1200e-4-fg320
xilinx_path: /opt/Xilinx/14.7/ISE_DS/ISE
constraints:
sources:
vhdl:
- path: src/*.vhd
library: work
verilog: []
dependencies:
- git: "https://git.0xmax42.io/maxp/Asynchronous-FIFO-AXI-Handshake.git"
rev: "hdlbuild"
testbenches:
vhdl:
- path: tests/*.vhd
library: work
verilog: []
constraints: src/VGA_test.ucf
dependencies:
# - git: "https://git.0xmax42.io/maxp/Asynchronous-FIFO-AXI-Handshake.git"
# rev: "hdlbuild"
build:
build_dir: working
@@ -39,7 +35,8 @@ tool_options:
map:
- "-detail"
- "-timing"
- "-ol high"
- "-ol"
- "high"
par: []

View File

@@ -1,14 +1,12 @@
[tool.poetry]
name = "hdlbuild"
version = "0.5.0"
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,11 @@ 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"
[build-system]
requires = ["poetry-core"]

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 @@
import typer
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
class BuildCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
self.project = load_project_config()
cli = typer.Typer(rich_help_panel="🔨 Build Commands")
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)"
@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",
)
parser.set_defaults(func=self.execute)
) -> 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()
def execute(self, args):
"""Starts the build process."""
if args.target == "synth":
self.console_utils.print("Starting synth process...")
ensure_directories_exist(True)
xilinx_ise_synth(self.project)
if target == "synth":
console.print("Starting synthesis …")
xilinx_ise_synth(project)
else:
self.console_utils.print("Starting build process...")
ensure_directories_exist(True)
xilinx_ise_all(self.project)
console.print("Starting full build ")
xilinx_ise_all(project)

View File

@@ -1,27 +1,35 @@
import typer
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.directory_manager import clear_build_directories, clear_directories
class CleanCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
cli = typer.Typer(rich_help_panel="🧹 Clean Commands")
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)"
@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,
)
parser.set_defaults(func=self.execute)
) -> None:
"""
Remove build artefacts (`build/*`) or *everything* (`all`).
def execute(self, args):
"""Cleans the build artifacts."""
if args.target == "all":
self.console_utils.print("Starting clean all process...")
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()
self.console_utils.print("All cleaned.")
console.print("All artefacts removed.")
else:
self.console_utils.print("Clearing build artifacts...")
console.print("Removing build artefacts")
clear_build_directories()
self.console_utils.print("Build artifacts cleaned.")
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 @@
import typer
from hdlbuild.dependencies.resolver import DependencyResolver
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.project_loader import load_project_config
class DepCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
self.project = load_project_config()
cli = typer.Typer(rich_help_panel="🔗 Dependency Commands")
def register(self, subparsers):
parser = subparsers.add_parser("dep", help="Start the dependencies process")
parser.set_defaults(func=self.execute)
@cli.callback(invoke_without_command=True)
def dep() -> None:
"""
Resolve all project dependencies.
def execute(self, args):
"""Starts the dependencies process."""
self.console_utils.print("Starting dependencies process...")
DependencyResolver(self.project).resolve_all()
```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,41 +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
from hdlbuild.utils.project_loader import load_project_config
class InitCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
self.project = load_project_config()
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."""
Copies `.gitignore` and `project.yml` from the template folder.
"""
console = ConsoleUtils("hdlbuild")
project_dir = Path.cwd()
# Paths to templates
template_dir = Path(__file__).parent / "templates"
script_dir = Path(__file__).parent.resolve()
template_dir = (script_dir / ".." / "templates").resolve()
# .gitignore
gitignore_template = template_dir / "gitignore.template"
gitignore_target = project_dir / ".gitignore"
files = [
("gitignore.template", ".gitignore"),
("project.yml.template", "project.yml"),
]
if not gitignore_target.exists():
shutil.copy(gitignore_template, gitignore_target)
self.console_utils.print("Created .gitignore")
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)
console.print(f"Created {target_name}")
else:
self.console_utils.print(".gitignore already exists, skipping.")
# project.yml
project_yml_template = template_dir / "project.yml.template"
project_yml_target = project_dir / "project.yml"
if not project_yml_target.exists():
shutil.copy(project_yml_template, project_yml_target)
self.console_utils.print("Created project.yml")
else:
self.console_utils.print("project.yml already exists, skipping.")
console.print(f"{target_name} already exists skipping.")

View File

@@ -1,23 +1,31 @@
import typer
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
class TestCommand:
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
self.project = load_project_config()
cli = typer.Typer(rich_help_panel="🧪 Test Commands")
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"
@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,
)
parser.set_defaults(func=self.execute)
) -> None:
"""
Build and run testbenches.
def execute(self, args):
"""Starts the test process."""
self.console_utils.print("Starting test process...")
build_testbench(self.project, args.target)
run_testbench(self.project, args.target)
```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“

View File

@@ -3,3 +3,4 @@
reports/
output/
.locale/
vhdl_ls.toml