Compare commits
15 Commits
5c4cab2bb1
...
3cf3fc1437
Author | SHA1 | Date | |
---|---|---|---|
3cf3fc1437 | |||
12d9f4b6c9 | |||
90859715c4 | |||
cc82d883c0 | |||
72699ed32f | |||
690decb33b | |||
2e2d86cfc2 | |||
decc18ac83 | |||
5c52c0cf59 | |||
91f4f03d97 | |||
7a03040926 | |||
f0100234c9 | |||
cfca986f72 | |||
defd2345b1 | |||
8b735c0d97 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -174,3 +174,14 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# hdlbuild specific
|
||||
|
||||
.hdlbuild_deps/
|
||||
.working/
|
||||
reports/
|
||||
output/
|
||||
vhdl/
|
||||
poetry.lock
|
||||
project.yml
|
||||
.project/
|
||||
.devcontainer/
|
41
project.example.yml
Normal file
41
project.example.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
name: VGA_Test
|
||||
topmodule: VGA_Test_Top
|
||||
target_device: xc3s1200e-4-fg320
|
||||
xilinx_path: /opt/Xilinx/14.7/ISE_DS/ISE
|
||||
|
||||
sources:
|
||||
vhdl:
|
||||
- path: src/vga/*.vhd
|
||||
library: work
|
||||
- path: src/common/*.vhd
|
||||
library: work
|
||||
- path: src/VGA_test_top.vhd
|
||||
library: work
|
||||
|
||||
verilog:
|
||||
- path: src/old_modules/*.v
|
||||
library: work
|
||||
|
||||
dependencies:
|
||||
- name: AsyncFIFO
|
||||
git: "https://github.com/0xMax32/Asynchronous-FIFO-AXI-Handshake.git"
|
||||
rev: "main"
|
||||
library: asyncfifo
|
||||
- name: GrayCounter
|
||||
git: "https://github.com/0xMax32/Gray-Counter.git"
|
||||
rev: "v1.0.0"
|
||||
library: graycounter
|
||||
|
||||
testbenches:
|
||||
vhdl:
|
||||
- path: src/tests/*.vhd
|
||||
library: work
|
||||
|
||||
verilog: []
|
||||
|
||||
constraints: constraints/VGA_Test.ucf
|
||||
|
||||
build:
|
||||
build_dir: working
|
||||
report_dir: reports
|
||||
copy_target_dir: output
|
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[tool.poetry]
|
||||
name = "hdlbuild"
|
||||
version = "0.1.0"
|
||||
description = "Flexible FPGA Build System"
|
||||
authors = ["0xMax42 <Mail@0xMax42.io>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
pyyaml = "^6.0.2"
|
||||
doit = "^0.36.0"
|
||||
pydantic = "^2.11.3"
|
||||
rich = "^14.0.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
70
src/hdlbuild.py
Normal file
70
src/hdlbuild.py
Normal file
@@ -0,0 +1,70 @@
|
||||
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
|
||||
|
||||
project = load_project_config()
|
||||
console_utils = ConsoleUtils("hdlbuild")
|
||||
|
||||
def clear(args):
|
||||
"""Clears the build artifacts."""
|
||||
if args.target == "all":
|
||||
console_utils.print("Starting clear all process...")
|
||||
clear_directories()
|
||||
console_utils.print("All cleared.")
|
||||
else:
|
||||
console_utils.print("Clearing build artifacts...")
|
||||
clear_build_directories()
|
||||
console_utils.print("Build artifacts cleared.")
|
||||
|
||||
def build(args):
|
||||
"""Starts the build process."""
|
||||
console_utils.print("Starting build process...")
|
||||
ensure_directories_exist(True)
|
||||
xilinx_ise_all(project)
|
||||
|
||||
def synth(args):
|
||||
"""Starts the build process."""
|
||||
console_utils.print("Starting build process...")
|
||||
ensure_directories_exist()
|
||||
xilinx_ise_synth(project)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="hdlbuild - Build management tool for FPGA projects",
|
||||
formatter_class=argparse.RawTextHelpFormatter
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(
|
||||
title="Commands",
|
||||
description="Available commands",
|
||||
dest="command",
|
||||
required=True
|
||||
)
|
||||
|
||||
# Clear command
|
||||
parser_clear = subparsers.add_parser("clear", help="Clear build artifacts")
|
||||
parser_clear.add_argument(
|
||||
"target",
|
||||
nargs="?",
|
||||
choices=["all"],
|
||||
help="Specify 'all' to clear everything (optional)"
|
||||
)
|
||||
parser_clear.set_defaults(func=clear)
|
||||
|
||||
# Build command
|
||||
parser_build = subparsers.add_parser("build", help="Start the build process")
|
||||
parser_build.set_defaults(func=build)
|
||||
|
||||
# Synth command
|
||||
parser_build = subparsers.add_parser("synth", help="Start the synth process")
|
||||
parser_build.set_defaults(func=synth)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
26
src/models/config.py
Normal file
26
src/models/config.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
from pydantic import BaseModel
|
||||
|
||||
class DirectoryConfig(BaseModel):
|
||||
dependency: str = ".hdlbuild_deps"
|
||||
build: str = ".working"
|
||||
report: str = "reports"
|
||||
copy_target: str = "output"
|
||||
|
||||
def get_relative_prefix(self) -> str:
|
||||
"""
|
||||
Gibt den relativen Pfad von build-Verzeichnis zurück zum Hauptverzeichnis.
|
||||
|
||||
Beispiel:
|
||||
".working" -> "../"
|
||||
".build/deep" -> "../../"
|
||||
"""
|
||||
depth = len(os.path.normpath(self.build).split(os.sep))
|
||||
return "../" * depth
|
||||
|
||||
DIRECTORIES = DirectoryConfig()
|
||||
|
||||
class GitConfig(BaseModel):
|
||||
timeout: int = 10
|
||||
|
||||
GIT = GitConfig()
|
47
src/models/project.py
Normal file
47
src/models/project.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
class SourceFile(BaseModel):
|
||||
path: str
|
||||
library: str = "work" # Default auf 'work'
|
||||
|
||||
class ToolOptions(BaseModel):
|
||||
common: List[str] = Field(default_factory=list)
|
||||
xst: List[str] = Field(default_factory=list)
|
||||
ngdbuild: List[str] = Field(default_factory=list)
|
||||
map: List[str] = Field(default_factory=list)
|
||||
par: List[str] = Field(default_factory=list)
|
||||
bitgen: List[str] = Field(default_factory=list)
|
||||
trace: List[str] = Field(default_factory=list)
|
||||
fuse: List[str] = Field(default_factory=list)
|
||||
|
||||
class Dependency(BaseModel):
|
||||
name: str
|
||||
git: str
|
||||
rev: str
|
||||
library: str = "work" # Default auf 'work'
|
||||
|
||||
class Sources(BaseModel):
|
||||
vhdl: List[SourceFile] = Field(default_factory=list)
|
||||
verilog: List[SourceFile] = Field(default_factory=list)
|
||||
|
||||
class Testbenches(BaseModel):
|
||||
vhdl: List[SourceFile] = Field(default_factory=list)
|
||||
verilog: List[SourceFile] = Field(default_factory=list)
|
||||
|
||||
class BuildOptions(BaseModel):
|
||||
build_dir: Optional[str] = "working"
|
||||
report_dir: Optional[str] = "reports"
|
||||
copy_target_dir: Optional[str] = "output"
|
||||
|
||||
class ProjectConfig(BaseModel):
|
||||
name: str
|
||||
topmodule: Optional[str]
|
||||
target_device: str
|
||||
xilinx_path: str
|
||||
sources: Sources
|
||||
testbenches: Optional[Testbenches] = None
|
||||
constraints: Optional[str] = None
|
||||
build: Optional[BuildOptions] = None
|
||||
dependencies: Optional[List[Dependency]] = Field(default_factory=list)
|
||||
tool_options: Optional[ToolOptions] = ToolOptions()
|
29
src/tools/xilinx_ise/bitgen.py
Normal file
29
src/tools/xilinx_ise/bitgen.py
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
|
||||
def run_bitgen(project: ProjectConfig):
|
||||
run_tool(
|
||||
project=project,
|
||||
tool_executable_name="bitgen",
|
||||
tool_option_attr="bitgen",
|
||||
mandatory_arguments=[
|
||||
"-w",
|
||||
f"{project.name}.ncd",
|
||||
f"{project.name}.bit"
|
||||
], step_number=9, total_steps=12
|
||||
)
|
||||
|
||||
|
||||
def copy_bitstream_file(project: ProjectConfig):
|
||||
copy_file(
|
||||
project=project,
|
||||
source_filename=f"{project.name}.bit",
|
||||
destination_filename=f"{project.name}.Bitstream",
|
||||
description="Bitstream File",
|
||||
step_number=10, total_steps=12
|
||||
)
|
72
src/tools/xilinx_ise/common.py
Normal file
72
src/tools/xilinx_ise/common.py
Normal file
@@ -0,0 +1,72 @@
|
||||
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 rich.console import Console
|
||||
|
||||
def run_tool(
|
||||
project: ProjectConfig,
|
||||
tool_executable_name: str,
|
||||
mandatory_arguments: List[str],
|
||||
tool_option_attr: Optional[str] = None,
|
||||
working_dir: Optional[str] = None,
|
||||
silent: bool = False,
|
||||
step_number: Optional[int] = None,
|
||||
total_steps: Optional[int] = None
|
||||
):
|
||||
if working_dir is None:
|
||||
working_dir = DIRECTORIES.build
|
||||
|
||||
xilinx_bin_dir = os.path.join(project.xilinx_path, "bin", "lin64")
|
||||
tool_executable = os.path.join(xilinx_bin_dir, tool_executable_name)
|
||||
|
||||
if not os.path.exists(tool_executable):
|
||||
raise FileNotFoundError(f"Executable nicht gefunden: {tool_executable}")
|
||||
|
||||
cmd = [tool_executable]
|
||||
|
||||
if project.tool_options and project.tool_options.common:
|
||||
cmd.extend(project.tool_options.common)
|
||||
|
||||
if tool_option_attr and project.tool_options:
|
||||
tool_opts = getattr(project.tool_options, tool_option_attr, [])
|
||||
if tool_opts:
|
||||
cmd.extend(tool_opts)
|
||||
|
||||
cmd.extend(mandatory_arguments)
|
||||
|
||||
task = ConsoleTask("hdlbuild", tool_executable_name.upper(), step_number, total_steps)
|
||||
task.run_command(cmd, cwd=working_dir, silent=silent)
|
||||
|
||||
|
||||
def copy_file(
|
||||
project: ProjectConfig,
|
||||
source_filename: str,
|
||||
destination_filename: str,
|
||||
description: str = "Report",
|
||||
step_number: Optional[int] = None,
|
||||
total_steps: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
Kopiert eine beliebige Report-Datei vom Build- in das Report-Verzeichnis.
|
||||
|
||||
Args:
|
||||
project (ProjectConfig): Geladene Projektkonfiguration
|
||||
source_filename (str): Name der Quelldatei im Build-Ordner
|
||||
destination_filename (str): Neuer Name der Zieldatei im Report-Ordner
|
||||
description (str): Optionale Beschreibung für die Ausgabe (z.B. "Synthesis Report")
|
||||
"""
|
||||
src_path = os.path.join(DIRECTORIES.build, source_filename)
|
||||
dst_path = os.path.join(DIRECTORIES.report, destination_filename)
|
||||
|
||||
if not os.path.exists(src_path):
|
||||
raise FileNotFoundError(f"{description} nicht gefunden: {src_path}")
|
||||
|
||||
os.makedirs(DIRECTORIES.report, exist_ok=True)
|
||||
|
||||
shutil.copyfile(src_path, dst_path)
|
||||
|
||||
util = ConsoleUtils("hdlbuild", step_number, total_steps)
|
||||
util.print(f"{description} kopiert nach {dst_path}")
|
34
src/tools/xilinx_ise/main.py
Normal file
34
src/tools/xilinx_ise/main.py
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
|
||||
|
||||
def xilinx_ise_synth(project: ProjectConfig):
|
||||
generate_xst_project_file(project, f"{DIRECTORIES.build}/{project.name}.prj")
|
||||
generate_xst_script_file(project, f"{DIRECTORIES.build}/{project.name}.scr")
|
||||
run_xst(project)
|
||||
|
||||
copy_synthesis_report(project)
|
||||
|
||||
def xilinx_ise_all(project: ProjectConfig):
|
||||
xilinx_ise_synth(project)
|
||||
|
||||
run_ngdbuild(project)
|
||||
|
||||
run_map(project)
|
||||
copy_map_report(project)
|
||||
|
||||
run_par(project)
|
||||
copy_par_report(project)
|
||||
copy_pinout_report(project)
|
||||
|
||||
run_bitgen(project)
|
||||
copy_bitstream_file(project)
|
||||
|
||||
run_trace(project)
|
||||
copy_trace_report(project)
|
30
src/tools/xilinx_ise/map.py
Normal file
30
src/tools/xilinx_ise/map.py
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
|
||||
def run_map(project: ProjectConfig):
|
||||
run_tool(
|
||||
project=project,
|
||||
tool_executable_name="map",
|
||||
tool_option_attr="map",
|
||||
mandatory_arguments=[
|
||||
"-p", project.target_device,
|
||||
"-w",
|
||||
f"{project.name}.ngd",
|
||||
"-o", f"{project.name}.map.ncd",
|
||||
f"{project.name}.pcf"
|
||||
], step_number=4, total_steps=12
|
||||
)
|
||||
|
||||
def copy_map_report(project: ProjectConfig):
|
||||
copy_file(
|
||||
project=project,
|
||||
source_filename=f"{project.name}.map.mrp",
|
||||
destination_filename=f"{project.name}.MapReport",
|
||||
description="Map Report",
|
||||
step_number=5, total_steps=12
|
||||
)
|
19
src/tools/xilinx_ise/ngdbuild.py
Normal file
19
src/tools/xilinx_ise/ngdbuild.py
Normal file
@@ -0,0 +1,19 @@
|
||||
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
|
||||
|
||||
def run_ngdbuild(project: ProjectConfig):
|
||||
run_tool(
|
||||
project=project,
|
||||
tool_executable_name="ngdbuild",
|
||||
tool_option_attr="ngdbuild",
|
||||
mandatory_arguments=[
|
||||
"-p", project.target_device,
|
||||
"-uc", f"{DIRECTORIES.get_relative_prefix()}{project.constraints}",
|
||||
f"{project.name}.ngc",
|
||||
f"{project.name}.ngd"
|
||||
], step_number=3, total_steps=12
|
||||
)
|
38
src/tools/xilinx_ise/par.py
Normal file
38
src/tools/xilinx_ise/par.py
Normal file
@@ -0,0 +1,38 @@
|
||||
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
|
||||
|
||||
def run_par(project: ProjectConfig):
|
||||
run_tool(
|
||||
project=project,
|
||||
tool_executable_name="par",
|
||||
tool_option_attr="par",
|
||||
mandatory_arguments=[
|
||||
"-w",
|
||||
f"{project.name}.map.ncd",
|
||||
f"{project.name}.ncd",
|
||||
f"{project.name}.pcf"
|
||||
], step_number=6, total_steps=12
|
||||
)
|
||||
|
||||
def copy_par_report(project: ProjectConfig):
|
||||
copy_file(
|
||||
project=project,
|
||||
source_filename=f"{project.name}.par",
|
||||
destination_filename=f"{project.name}.PlaceRouteReport",
|
||||
description="Place & Route Report",
|
||||
step_number=7, total_steps=12
|
||||
)
|
||||
|
||||
def copy_pinout_report(project: ProjectConfig):
|
||||
copy_file(
|
||||
project=project,
|
||||
source_filename=f"{project.name}_pad.txt",
|
||||
destination_filename=f"{project.name}.PinoutReport",
|
||||
description="Pinout Report",
|
||||
step_number=8, total_steps=12
|
||||
)
|
27
src/tools/xilinx_ise/trace.py
Normal file
27
src/tools/xilinx_ise/trace.py
Normal file
@@ -0,0 +1,27 @@
|
||||
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
|
||||
|
||||
def run_trace(project: ProjectConfig):
|
||||
run_tool(
|
||||
project=project,
|
||||
tool_executable_name="trce",
|
||||
tool_option_attr="trace",
|
||||
mandatory_arguments=[
|
||||
f"{project.name}.ncd",
|
||||
f"{project.name}.pcf",
|
||||
], step_number=11, total_steps=12
|
||||
)
|
||||
|
||||
def copy_trace_report(project: ProjectConfig):
|
||||
copy_file(
|
||||
project=project,
|
||||
source_filename=f"{project.name}.twr",
|
||||
destination_filename=f"{project.name}.TimingReport",
|
||||
description="Timing Report",
|
||||
step_number=12, total_steps=12
|
||||
)
|
63
src/tools/xilinx_ise/xst.py
Normal file
63
src/tools/xilinx_ise/xst.py
Normal file
@@ -0,0 +1,63 @@
|
||||
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
|
||||
import subprocess
|
||||
import os
|
||||
import shutil
|
||||
|
||||
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")
|
||||
|
||||
# Optionale Dependencies
|
||||
if project.dependencies:
|
||||
for dep in project.dependencies:
|
||||
# Hier könnte man noch spezielle Sources aus dep.path expandieren
|
||||
pass
|
||||
|
||||
def generate_xst_script_file(project: ProjectConfig, output_path: str):
|
||||
"""
|
||||
Generiert die XST .scr-Datei mit den Synthese-Optionen.
|
||||
"""
|
||||
with open(output_path, "w") as f:
|
||||
f.write(f"run ")
|
||||
f.write(f"-ifn {project.name}.prj ")
|
||||
f.write(f"-ofn {project.name}.ngc ")
|
||||
f.write(f"-ifmt mixed ")
|
||||
|
||||
if project.tool_options and project.tool_options.xst:
|
||||
for opt in project.tool_options.xst:
|
||||
f.write(f"{opt} ")
|
||||
|
||||
f.write(f"-top {project.topmodule} ")
|
||||
f.write(f"-ofmt NGC ")
|
||||
f.write(f"-p {project.target_device} ")
|
||||
|
||||
|
||||
|
||||
def run_xst(project: ProjectConfig):
|
||||
run_tool(
|
||||
project=project,
|
||||
tool_executable_name="xst",
|
||||
mandatory_arguments=["-ifn", f"{project.name}.scr",
|
||||
], step_number=1, total_steps=12
|
||||
)
|
||||
|
||||
def copy_synthesis_report(project: ProjectConfig):
|
||||
copy_file(
|
||||
project=project,
|
||||
source_filename=f"{project.name}.srp",
|
||||
destination_filename=f"{project.name}.SynthesisReport",
|
||||
description="Synthesebericht",
|
||||
step_number=2, total_steps=12
|
||||
)
|
139
src/utils/console_utils.py
Normal file
139
src/utils/console_utils.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import threading
|
||||
import time
|
||||
import subprocess
|
||||
from typing import List, Optional
|
||||
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
from rich.text import Text
|
||||
|
||||
class ConsoleTask:
|
||||
def __init__(self, prefix:str, title: str, step_number: Optional[int] = None, total_steps: Optional[int] = None, max_log_lines: int = 10):
|
||||
self.prefix = prefix
|
||||
self.title = title
|
||||
self.step_number = step_number
|
||||
self.total_steps = total_steps
|
||||
self.max_log_lines = max_log_lines
|
||||
self.spinner_cycle = ['|', '/', '-', '\\']
|
||||
self.stop_event = threading.Event()
|
||||
self.spinner_thread: Optional[threading.Thread] = None
|
||||
self.output_lines: List[str] = []
|
||||
self._stdout_lock = threading.Lock()
|
||||
self.console = Console()
|
||||
self.live: Optional[Live] = None
|
||||
self.spinner_idx = 0
|
||||
|
||||
def start_spinner(self):
|
||||
self.live = Live(console=self.console, refresh_per_second=30, transient=True)
|
||||
self.live.start()
|
||||
self.spinner_thread = threading.Thread(target=self._spinner_task, daemon=True)
|
||||
self.spinner_thread.start()
|
||||
|
||||
def _spinner_task(self):
|
||||
while not self.stop_event.is_set():
|
||||
with self._stdout_lock:
|
||||
self._redraw_spinner()
|
||||
self.spinner_idx += 1
|
||||
time.sleep(0.1)
|
||||
|
||||
def _render_content(self) -> Text:
|
||||
visible_lines = self.output_lines[-self.max_log_lines:]
|
||||
|
||||
prefix_text = f"[grey50]\[{self.prefix}][/grey50]" if self.prefix else ""
|
||||
step_text = f"[bold blue]Step {self.step_number}/{self.total_steps}[/bold blue]" if self.step_number and self.total_steps else ""
|
||||
title_text = f"[bold]{self.title}[/bold]" if self.title else ""
|
||||
|
||||
spinner_markup = f"{prefix_text} {step_text} {title_text} {self.spinner_cycle[self.spinner_idx % len(self.spinner_cycle)]}"
|
||||
|
||||
spinner_text = Text.from_markup(spinner_markup)
|
||||
log_text = Text("\n".join(visible_lines))
|
||||
|
||||
full_text = spinner_text + Text("\n") + log_text
|
||||
|
||||
return full_text
|
||||
|
||||
def _redraw_spinner(self):
|
||||
if self.live:
|
||||
self.live.update(self._render_content())
|
||||
|
||||
def log(self, message: str):
|
||||
with self._stdout_lock:
|
||||
self.output_lines.append(message)
|
||||
if len(self.output_lines) > self.max_log_lines:
|
||||
self.output_lines = self.output_lines[-self.max_log_lines:]
|
||||
|
||||
if self.live:
|
||||
self.live.update(self._render_content())
|
||||
|
||||
|
||||
def run_command(self, cmd: List[str], cwd: Optional[str] = None, silent: bool = False) -> int:
|
||||
success = False
|
||||
start_time = time.time()
|
||||
|
||||
self.start_spinner()
|
||||
|
||||
try:
|
||||
if silent:
|
||||
subprocess.run(cmd, cwd=cwd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
success = True
|
||||
else:
|
||||
process = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if process.stdout is None:
|
||||
raise ValueError("Failed to capture stdout")
|
||||
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if not line and process.poll() is not None:
|
||||
break
|
||||
if line:
|
||||
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:
|
||||
self.spinner_thread.join()
|
||||
|
||||
duration = time.time() - start_time
|
||||
|
||||
# Finalize output
|
||||
with self._stdout_lock:
|
||||
self._finalize_output(success, duration)
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
def _finalize_output(self, success: bool, duration: float):
|
||||
if self.live:
|
||||
self.live.stop()
|
||||
|
||||
prefix_text = f"[grey50]\[{self.prefix}][/grey50]" if self.prefix else ""
|
||||
status_symbol = "[green]✅[/green]" if success else "[red]❌[/red]"
|
||||
step_text = f"[bold blue]Step {self.step_number}/{self.total_steps}[/bold blue]" if self.step_number and self.total_steps else ""
|
||||
status_title = f"[bold green]{self.title}[/bold green]" if success else f"[bold red]{self.title}[/bold red]"
|
||||
final_line = f"{prefix_text} {step_text} {status_title} {status_symbol} [bold green]({duration:.1f}s[/bold green])"
|
||||
|
||||
# Final full output
|
||||
self.console.print(final_line)
|
||||
|
||||
class ConsoleUtils:
|
||||
def __init__(self,
|
||||
prefix: str = "",
|
||||
step_number: Optional[int] = None,
|
||||
total_steps: Optional[int] = None
|
||||
):
|
||||
self.prefix = prefix
|
||||
self.step_number = step_number
|
||||
self.total_steps = total_steps
|
||||
self.console = Console()
|
||||
|
||||
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)
|
57
src/utils/directory_manager.py
Normal file
57
src/utils/directory_manager.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
import shutil
|
||||
from models.config import DIRECTORIES
|
||||
from utils.console_utils import ConsoleUtils
|
||||
|
||||
def ensure_directories_exist(silent: bool = False):
|
||||
"""
|
||||
Erstellt alle in der Konfiguration definierten Verzeichnisse, falls sie nicht existieren.
|
||||
"""
|
||||
console_utils = None
|
||||
if not silent:
|
||||
console_utils = ConsoleUtils("hdlbuild")
|
||||
|
||||
for name, path in DIRECTORIES.dict().items():
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
if not silent and console_utils:
|
||||
console_utils.print(f"Verzeichnis erstellt: {path}")
|
||||
else:
|
||||
if not silent and console_utils:
|
||||
console_utils.print(f"[hdlbuild] Verzeichnis vorhanden: {path}")
|
||||
|
||||
def clear_directories(silent: bool = False):
|
||||
"""
|
||||
Löscht alle in der Konfiguration definierten Verzeichnisse, falls sie existieren.
|
||||
"""
|
||||
console_utils = None
|
||||
if not silent:
|
||||
console_utils = ConsoleUtils("hdlbuild")
|
||||
|
||||
for name, path in DIRECTORIES.dict().items():
|
||||
if os.path.exists(path):
|
||||
if not silent and console_utils:
|
||||
console_utils.print(f"Lösche Verzeichnis: {path}")
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
if not silent and console_utils:
|
||||
console_utils.print(f"Verzeichnis nicht vorhanden, übersprungen: {path}")
|
||||
|
||||
def clear_build_directories(silent: bool = False):
|
||||
"""
|
||||
Löscht alle in der Konfiguration definierten Verzeichnisse, falls sie existieren.
|
||||
"""
|
||||
console_utils = None
|
||||
if not silent:
|
||||
console_utils = ConsoleUtils("hdlbuild")
|
||||
|
||||
for name, path in DIRECTORIES.dict().items():
|
||||
if name == "dependency":
|
||||
continue
|
||||
if os.path.exists(path):
|
||||
if not silent and console_utils:
|
||||
console_utils.print(f"Lösche Verzeichnis: {path}")
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
if not silent and console_utils:
|
||||
console_utils.print(f"Verzeichnis nicht vorhanden, übersprungen: {path}")
|
16
src/utils/project_loader.py
Normal file
16
src/utils/project_loader.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import yaml
|
||||
from models.project import ProjectConfig
|
||||
|
||||
def load_project_config(path: str = "project.yml") -> ProjectConfig:
|
||||
"""
|
||||
Lädt die Projektkonfiguration aus einer YAML-Datei und gibt ein typisiertes ProjectConfig-Objekt zurück.
|
||||
|
||||
Args:
|
||||
path (str): Pfad zur project.yml Datei (Default: "project.yml")
|
||||
|
||||
Returns:
|
||||
ProjectConfig: Geparstes und typisiertes Projektkonfigurationsobjekt
|
||||
"""
|
||||
with open(path, "r") as file:
|
||||
raw_data = yaml.safe_load(file)
|
||||
return ProjectConfig(**raw_data)
|
19
src/utils/source_resolver.py
Normal file
19
src/utils/source_resolver.py
Normal file
@@ -0,0 +1,19 @@
|
||||
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
|
Reference in New Issue
Block a user