Compare commits

...

4 Commits

Author SHA1 Message Date
dc0d8242a8 Refines package structure and script entry point
Updates package inclusion to reflect new directory structure.
Adjusts script entry point path to align with updated module organization.
2025-04-26 18:52:51 +00:00
28406028c6 Refactors project structure for modularity
Renames and reorganizes modules under a new 'hdlbuild' namespace for improved clarity and maintainability. Updates import paths across the codebase to reflect the new structure.

No logic changes introduced.
2025-04-26 18:52:44 +00:00
f9d5e3c535 Adds CLI functionality with commands for clearing, building, synthesizing, and managing dependencies 2025-04-26 18:26:45 +00:00
4f1f2e7d51 Adds dependency resolver for recursive project handling
Introduces a dependency resolver to clone and manage project dependencies recursively.
Updates CLI to include a new "dep" command for dependency resolution.
Enhances source resolver to handle dependencies and expand all sources.
Improves console utilities with live mode for real-time feedback.
Marks dependency names in project configuration as optional.

Enables streamlined multi-repository workflows and dependency management.
2025-04-26 18:11:31 +00:00
20 changed files with 290 additions and 75 deletions

View File

@@ -5,6 +5,12 @@ description = "Flexible FPGA Build System"
authors = ["0xMax42 <Mail@0xMax42.io>"]
license = "MIT"
readme = "README.md"
packages = [
{ include = "hdlbuild", from = "src" }
]
[tool.poetry.scripts]
hdlbuild = "hdlbuild.cli:main"
[tool.poetry.dependencies]
python = "^3.10"
@@ -12,6 +18,7 @@ pyyaml = "^6.0.2"
doit = "^0.36.0"
pydantic = "^2.11.3"
rich = "^14.0.0"
gitpython = "^3.1.44"
[build-system]

0
src/hdlbuild/__init__.py Normal file
View File

View File

@@ -1,10 +1,11 @@
import argparse
import sys
from tools.xilinx_ise.main import xilinx_ise_all, xilinx_ise_synth
from utils.console_utils import ConsoleUtils
from utils.directory_manager import clear_build_directories, clear_directories, ensure_directories_exist
from utils.project_loader import load_project_config
from hdlbuild.dependencies.resolver import DependencyResolver
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 clear_build_directories, clear_directories, ensure_directories_exist
from hdlbuild.utils.project_loader import load_project_config
project = load_project_config()
console_utils = ConsoleUtils("hdlbuild")
@@ -32,6 +33,12 @@ def synth(args):
ensure_directories_exist()
xilinx_ise_synth(project)
def dep(args):
"""Starts the dependencies process."""
console_utils.print("Starting dependencies process...")
DependencyResolver(project).resolve_all()
def main():
parser = argparse.ArgumentParser(
description="hdlbuild - Build management tool for FPGA projects",
@@ -63,6 +70,10 @@ def main():
parser_build = subparsers.add_parser("synth", help="Start the synth process")
parser_build.set_defaults(func=synth)
# Dependencies command
parser_build = subparsers.add_parser("dep", help="Start the dependencies process")
parser_build.set_defaults(func=dep)
args = parser.parse_args()
args.func(args)

View File

@@ -0,0 +1,99 @@
# src/hdlbuild/dependency/resolver.py
from typing import List, Set
from git import Repo
from hdlbuild.models.config import DIRECTORIES, GIT
from hdlbuild.models.project import ProjectConfig
from hdlbuild.models.dependency import ResolvedDependency
import os
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.project_loader import load_project_config
class DependencyResolver:
def __init__(self, root_project: ProjectConfig, offline_mode: bool = False):
self.root_project = root_project
self.offline_mode = offline_mode
self.resolved: List[ResolvedDependency] = []
self.visited_urls: Set[str] = set()
self.console = ConsoleUtils(live=True)
self.console.start_live()
def resolve_all(self):
"""Startet das Auflösen aller Abhängigkeiten (rekursiv)."""
self._resolve_project(self.root_project)
self.console.stop_live("[bold green]Alle Abhängigkeiten aufgelöst.[/bold green]")
def _resolve_project(self, project: ProjectConfig):
"""Löst die Abhängigkeiten eines einzelnen Projekts auf."""
for dep in project.dependencies or []:
if dep.git in self.visited_urls:
continue
self.visited_urls.add(dep.git)
local_path = self._clone_or_use_existing(dep.git, dep.rev)
dep_project = self._load_project_config(os.path.join(local_path, "project.yml"))
# Speichern als ResolvedDependency
self.resolved.append(ResolvedDependency(project=dep_project, local_path=local_path))
self._resolve_project(dep_project)
def _clone_or_use_existing(self, git_url: str, rev: str) -> str:
folder_name = os.path.basename(git_url.rstrip("/")).replace(".git", "")
local_path = os.path.join(DIRECTORIES.dependency, folder_name)
if os.path.exists(local_path):
# Lokales Repo vorhanden
self.console.print(f"[bold green]Benutze vorhandenes Repository: {folder_name}[/bold green]")
repo = Repo(local_path)
if not self.offline_mode:
try:
self.console.print(f"[bold green]Aktualisiere {folder_name}...[/bold green]")
# Fetch Remote Updates
repo.remotes.origin.fetch()
# Prüfen, ob HEAD und origin/branch unterschiedlich sind
local_commit = repo.head.commit
remote_ref = repo.remotes.origin.refs[repo.active_branch.name]
remote_commit = remote_ref.commit
if local_commit.hexsha != remote_commit.hexsha:
self.console.print(f"[bold yellow]Änderungen erkannt! Force-Pull wird durchgeführt...[/bold yellow]")
repo.git.reset('--hard', remote_commit.hexsha)
else:
self.console.print(f"[bold green]Repository {folder_name} ist aktuell.[/bold green]")
except Exception as e:
self.console.print(f"[bold red]Warnung beim Aktualisieren: {e}[/bold red]")
else:
# Lokales Repo fehlt → nur dann klonen
if self.offline_mode:
raise FileNotFoundError(f"Repository {folder_name} existiert lokal nicht und offline_mode ist aktiv.")
else:
self.console.print(f"[bold green]Klone {git_url}...[/bold green]")
repo = Repo.clone_from(git_url, local_path)
# Immer: Auf den richtigen Commit/Branch wechseln
self.console.print(f"[bold green]Checkout auf[/bold green] [yellow]{rev}[/yellow] in {folder_name}")
repo.git.checkout(rev)
return local_path
def _load_project_config(self, path: str) -> ProjectConfig:
"""
Lädt eine project.yml aus einem lokalen Ordner.
Args:
path (str): Basisverzeichnis des geklonten Projekts.
Returns:
ProjectConfig: Das geladene Projekt.
"""
self.console.print(f"Lade project.yml aus {path}...")
return load_project_config(path)

View File

@@ -0,0 +1,8 @@
# models/dependency.py
from pydantic import BaseModel
from hdlbuild.models.project import ProjectConfig
class ResolvedDependency(BaseModel):
project: ProjectConfig
local_path: str

View File

@@ -16,7 +16,7 @@ class ToolOptions(BaseModel):
fuse: List[str] = Field(default_factory=list)
class Dependency(BaseModel):
name: str
name: Optional[str] = None # Name ist jetzt optional
git: str
rev: str
library: str = "work" # Default auf 'work'

View File

@@ -2,9 +2,9 @@ import subprocess
import os
import shutil
from typing import Optional
from models.project import ProjectConfig
from models.config import DIRECTORIES
from tools.xilinx_ise.common import copy_file, run_tool
from hdlbuild.models.project import ProjectConfig
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.tools.xilinx_ise.common import copy_file, run_tool
def run_bitgen(project: ProjectConfig):
run_tool(

View File

@@ -1,9 +1,9 @@
import shutil
import os
from typing import Optional, List
from models.project import ProjectConfig
from models.config import DIRECTORIES
from utils.console_utils import ConsoleTask, ConsoleUtils
from hdlbuild.models.project import ProjectConfig
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.utils.console_utils import ConsoleTask, ConsoleUtils
from rich.console import Console
def run_tool(

View File

@@ -1,11 +1,11 @@
from models.config import DIRECTORIES
from models.project import ProjectConfig
from tools.xilinx_ise.bitgen import copy_bitstream_file, run_bitgen
from tools.xilinx_ise.map import copy_map_report, run_map
from tools.xilinx_ise.ngdbuild import run_ngdbuild
from tools.xilinx_ise.par import copy_par_report, copy_pinout_report, run_par
from tools.xilinx_ise.trace import copy_trace_report, run_trace
from tools.xilinx_ise.xst import copy_synthesis_report, generate_xst_project_file, generate_xst_script_file, run_xst
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.models.project import ProjectConfig
from hdlbuild.tools.xilinx_ise.bitgen import copy_bitstream_file, run_bitgen
from hdlbuild.tools.xilinx_ise.map import copy_map_report, run_map
from hdlbuild.tools.xilinx_ise.ngdbuild import run_ngdbuild
from hdlbuild.tools.xilinx_ise.par import copy_par_report, copy_pinout_report, run_par
from hdlbuild.tools.xilinx_ise.trace import copy_trace_report, run_trace
from hdlbuild.tools.xilinx_ise.xst import copy_synthesis_report, generate_xst_project_file, generate_xst_script_file, run_xst
def xilinx_ise_synth(project: ProjectConfig):

View File

@@ -2,9 +2,9 @@ import subprocess
import os
import shutil
from typing import Optional
from models.project import ProjectConfig
from models.config import DIRECTORIES
from tools.xilinx_ise.common import copy_file, run_tool
from hdlbuild.models.project import ProjectConfig
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.tools.xilinx_ise.common import copy_file, run_tool
def run_map(project: ProjectConfig):
run_tool(

View File

@@ -1,9 +1,9 @@
import subprocess
import os
from typing import Optional
from models.project import ProjectConfig
from models.config import DIRECTORIES
from tools.xilinx_ise.common import run_tool
from hdlbuild.models.project import ProjectConfig
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.tools.xilinx_ise.common import run_tool
def run_ngdbuild(project: ProjectConfig):
run_tool(

View File

@@ -2,9 +2,9 @@ import subprocess
import shutil
import os
from typing import Optional
from models.project import ProjectConfig
from models.config import DIRECTORIES
from tools.xilinx_ise.common import copy_file, run_tool
from hdlbuild.models.project import ProjectConfig
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.tools.xilinx_ise.common import copy_file, run_tool
def run_par(project: ProjectConfig):
run_tool(

View File

@@ -2,9 +2,9 @@ import subprocess
import os
import shutil
from typing import Optional
from models.project import ProjectConfig
from models.config import DIRECTORIES
from tools.xilinx_ise.common import copy_file, run_tool
from hdlbuild.models.project import ProjectConfig
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.tools.xilinx_ise.common import copy_file, run_tool
def run_trace(project: ProjectConfig):
run_tool(

View File

@@ -1,8 +1,9 @@
from typing import Optional
from models.config import DIRECTORIES
from tools.xilinx_ise.common import copy_file, run_tool
from utils.source_resolver import expand_sources
from models.project import ProjectConfig
from hdlbuild.dependencies.resolver import DependencyResolver
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.tools.xilinx_ise.common import copy_file, run_tool
from hdlbuild.utils.source_resolver import expand_all_sources
from hdlbuild.models.project import ProjectConfig
import subprocess
import os
import shutil
@@ -12,12 +13,15 @@ def generate_xst_project_file(project: ProjectConfig, output_path: str):
Generiert die XST .prj-Datei mit allen Quellcodes.
"""
with open(output_path, "w") as f:
# VHDL-Sources
for lib, src in expand_sources(project.sources.vhdl):
f.write(f"vhdl {lib} \"{DIRECTORIES.get_relative_prefix()}/{src}\"\n")
# Verilog-Sources
for lib, src in expand_sources(project.sources.verilog):
f.write(f"verilog {lib} \"{DIRECTORIES.get_relative_prefix()}/{src}\"\n")
resolver = DependencyResolver(project, offline_mode=True)
resolver.resolve_all()
vhdl_sources, verilog_sources = expand_all_sources(project, resolver.resolved)
for lib, file in vhdl_sources:
f.write(f"vhdl {lib} \"{DIRECTORIES.get_relative_prefix()}{file}\"\n")
for lib, file in verilog_sources:
f.write(f"verilog {lib} \"{DIRECTORIES.get_relative_prefix()}{file}\"\n")
# Optionale Dependencies
if project.dependencies:

View File

@@ -1,3 +1,4 @@
import sys
import threading
import time
import subprocess
@@ -6,6 +7,7 @@ from typing import List, Optional
from rich.console import Console
from rich.live import Live
from rich.text import Text
from rich.markup import render
class ConsoleTask:
def __init__(self, prefix:str, title: str, step_number: Optional[int] = None, total_steps: Optional[int] = None, max_log_lines: int = 10):
@@ -18,6 +20,7 @@ class ConsoleTask:
self.stop_event = threading.Event()
self.spinner_thread: Optional[threading.Thread] = None
self.output_lines: List[str] = []
self.all_lines: List[str] = []
self._stdout_lock = threading.Lock()
self.console = Console()
self.live: Optional[Live] = None
@@ -58,6 +61,7 @@ class ConsoleTask:
def log(self, message: str):
with self._stdout_lock:
self.all_lines.append(message)
self.output_lines.append(message)
if len(self.output_lines) > self.max_log_lines:
self.output_lines = self.output_lines[-self.max_log_lines:]
@@ -89,12 +93,7 @@ class ConsoleTask:
self.log(line.rstrip())
success = (process.returncode == 0)
if not success:
raise subprocess.CalledProcessError(process.returncode, cmd)
except subprocess.CalledProcessError:
success = False
raise
finally:
self.stop_event.set()
if self.spinner_thread:
@@ -102,11 +101,18 @@ class ConsoleTask:
duration = time.time() - start_time
# Finalize output
with self._stdout_lock:
self._finalize_output(success, duration)
return 0 if success else 1
if not success:
# Schöne Fehlerausgabe und kontrolliertes Beenden
self.console.print("\n[bold red]❌ Fehler beim Ausführen des Kommandos:[/bold red]")
for line in self.all_lines:
self.console.print(f"[red]{line}[/red]")
sys.exit(1) # ❗ Hier: hartes, aber sauberes Beenden des Programms
return 0
def _finalize_output(self, success: bool, duration: float):
if self.live:
@@ -122,18 +128,49 @@ class ConsoleTask:
self.console.print(final_line)
class ConsoleUtils:
def __init__(self,
prefix: str = "",
def __init__(
self,
prefix: str = "hdlbuild",
step_number: Optional[int] = None,
total_steps: Optional[int] = None
total_steps: Optional[int] = None,
live: bool = False
):
self.prefix = prefix
self.step_number = step_number
self.total_steps = total_steps
self.console = Console()
self.live_mode = live
self.live: Optional[Live] = None
self.messages: List[str] = []
def start_live(self):
"""Startet den Live-Modus."""
if self.live_mode and self.live is None:
self.live = Live(console=self.console, refresh_per_second=10, transient=True)
self.live.start()
def print(self, message: str):
prefix = f"[grey50]\[{self.prefix}][/grey50]" if self.prefix else ""
step_info = f"[bold blue]Step {self.step_number}/{self.total_steps}[/bold blue]" if self.step_number and self.total_steps else ""
message_text = f"{prefix} {step_info} {message}"
self.console.print(message_text)
full_message = f"{prefix} {step_info} {message}"
if self.live_mode and self.live:
self.messages.append(full_message)
rendered_lines = [Text.from_markup(line) for line in self.messages]
combined = Text()
for line in rendered_lines:
combined.append(line)
combined.append("\n")
self.live.update(combined)
else:
self.console.print(full_message)
def stop_live(self, final_message: Optional[str] = None):
"""Beendet den Live-Modus, löscht alte Ausgaben und zeigt eine Abschlussnachricht."""
if self.live_mode and self.live:
self.live.stop()
self.live = None
self.messages.clear() # Alte Messages verwerfen
if final_message:
self.console.print(final_message)

View File

@@ -1,7 +1,7 @@
import os
import shutil
from models.config import DIRECTORIES
from utils.console_utils import ConsoleUtils
from hdlbuild.models.config import DIRECTORIES
from hdlbuild.utils.console_utils import ConsoleUtils
def ensure_directories_exist(silent: bool = False):
"""

View File

@@ -1,5 +1,5 @@
import yaml
from models.project import ProjectConfig
from hdlbuild.models.project import ProjectConfig
def load_project_config(path: str = "project.yml") -> ProjectConfig:
"""

View File

@@ -0,0 +1,68 @@
# src/hdlbuild/utils/source_resolver.py
import glob
import os
from typing import List, Tuple
from hdlbuild.models.project import SourceFile, ProjectConfig
from hdlbuild.models.dependency import ResolvedDependency
def _expand_project_sources(project: ProjectConfig, project_root: str) -> Tuple[List[Tuple[str, str]], List[Tuple[str, str]]]:
"""
Expandiert die Quellen eines einzelnen Projekts, getrennt nach VHDL und Verilog.
Args:
project (ProjectConfig): Das Projekt, dessen Quellen expandiert werden sollen.
project_root (str): Basisverzeichnis, von dem aus die Pfade aufgelöst werden.
Returns:
Tuple: (List of (library, filepath) für VHDL, List of (library, filepath) für Verilog)
"""
vhdl_expanded = []
verilog_expanded = []
# VHDL-Sources
for source in project.sources.vhdl:
full_pattern = os.path.join(project_root, source.path)
matched_files = glob.glob(full_pattern, recursive=True)
for file in matched_files:
normalized_path = os.path.normpath(file)
vhdl_expanded.append((source.library, normalized_path))
# Verilog-Sources
for source in project.sources.verilog:
full_pattern = os.path.join(project_root, source.path)
matched_files = glob.glob(full_pattern, recursive=True)
for file in matched_files:
normalized_path = os.path.normpath(file)
verilog_expanded.append((source.library, normalized_path))
return vhdl_expanded, verilog_expanded
def expand_all_sources(root_project: ProjectConfig, resolved_dependencies: List[ResolvedDependency]) -> Tuple[List[Tuple[str, str]], List[Tuple[str, str]]]:
"""
Expandiert alle Quellen aus dem Root-Projekt und allen Dependencies, getrennt nach VHDL und Verilog.
Args:
root_project (ProjectConfig): Das Hauptprojekt
resolved_dependencies (List[ResolvedDependency]): Alle rekursiv aufgelösten Dependencies
Returns:
Tuple:
- List of (library, filepath) für VHDL
- List of (library, filepath) für Verilog
"""
all_vhdl_sources = []
all_verilog_sources = []
# Root-Projekt expandieren
vhdl_sources, verilog_sources = _expand_project_sources(root_project, ".")
all_vhdl_sources.extend(vhdl_sources)
all_verilog_sources.extend(verilog_sources)
# Dependencies expandieren
for dep in resolved_dependencies:
vhdl_dep, verilog_dep = _expand_project_sources(dep.project, dep.local_path)
all_vhdl_sources.extend(vhdl_dep)
all_verilog_sources.extend(verilog_dep)
return all_vhdl_sources, all_verilog_sources

View File

@@ -1,19 +0,0 @@
import glob
import os
from typing import List, Tuple
from models.project import SourceFile
def expand_sources(sources: List[SourceFile]) -> List[Tuple[str, str]]:
"""
Expandiert eine Liste von SourceFile-Objekten mit Wildcards in echte Pfade.
Returns:
List of (library, filepath)
"""
expanded = []
for source in sources:
matched_files = glob.glob(source.path, recursive=True)
for file in matched_files:
normalized_path = os.path.normpath(file)
expanded.append((source.library, normalized_path))
return expanded