Coverage for tsfpga/vivado/ip_cores.py: 94%
62 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-01 20:51 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-01 20:51 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the tsfpga project, a project platform for modern FPGA development.
5# https://tsfpga.com
6# https://github.com/tsfpga/tsfpga
7# --------------------------------------------------------------------------------------------------
9from __future__ import annotations
11import hashlib
12import json
13from typing import TYPE_CHECKING
15from tsfpga.system_utils import create_file, delete, read_file
17from .project import VivadoIpCoreProject
19if TYPE_CHECKING:
20 from pathlib import Path
22 from tsfpga.ip_core_file import IpCoreFile
23 from tsfpga.module_list import ModuleList
26class VivadoIpCores:
27 """
28 Handle a list of IP core sources. Has a mechanism to detect whether a regenerate of IP files
29 is needed.
30 """
32 project_name = "vivado_ip_project"
34 def __init__(
35 self,
36 modules: ModuleList,
37 output_path: Path,
38 part_name: str,
39 vivado_project_class: type[VivadoIpCoreProject] | None = None,
40 ) -> None:
41 """
42 Arguments:
43 modules: IP cores from these modules will be included.
44 output_path: The Vivado project will be placed here.
45 part_name: Vivado part name to be used for the project.
46 vivado_project_class: The Vivado project class that will be used for the IP core
47 project. Is safe to leave at default in most cases.
48 """
49 self.project_directory = output_path.resolve() / self.project_name
50 self._part_name = part_name
52 vivado_project_class = (
53 VivadoIpCoreProject if vivado_project_class is None else vivado_project_class
54 )
56 self._hash_file = self.project_directory / "ip_files_hash.txt"
58 self._setup(modules=modules, vivado_project_class=vivado_project_class)
60 @property
61 def compile_order_file(self) -> Path:
62 """
63 pathlib.Path: Path to the generated compile order file.
64 """
65 return self.project_directory / "compile_order.txt"
67 @property
68 def vivado_project_file(self) -> Path:
69 """
70 pathlib.Path: Path to the Vivado project file.
71 """
72 return self._vivado_project.project_file(self.project_directory)
74 def create_vivado_project(self) -> None:
75 """
76 Create IP core Vivado project.
77 """
78 print(f"Creating IP core project in {self.project_directory}")
79 delete(self.project_directory)
81 if not self._vivado_project.create(self.project_directory):
82 raise RuntimeError("Failed to create Vivado IP core project")
84 self._save_hash()
86 def create_vivado_project_if_needed(self) -> bool:
87 """
88 Create IP core Vivado project if anything has changed since last time this was run.
89 If
91 * List of TCL files that create IP cores,
92 * and contents of these files,
94 is the same then it will not create. But if anything is added or removed from the list,
95 or the contents of a TCL file is changed, there will be a recreation.
97 Return:
98 True if Vivado project was created. False otherwise.
99 """
100 if self._should_create():
101 self.create_vivado_project()
102 return True
104 return False
106 def _setup(self, modules: ModuleList, vivado_project_class: type[VivadoIpCoreProject]) -> None:
107 self._vivado_project = vivado_project_class(
108 name=self.project_name, modules=modules, part=self._part_name
109 )
111 ip_core_files = []
112 for module in modules:
113 # Send the same two arguments that are sent in the VivadoProject create flow
114 ip_core_files += module.get_ip_core_files(generics={}, part=self._part_name)
116 self._hash = self._calculate_hash(ip_core_files)
118 @staticmethod
119 def _calculate_hash(ip_core_files: list[IpCoreFile]) -> str:
120 """
121 A string with hashes of the different IP core files.
122 """
123 data = ""
125 def sort_by_file_name(ip_core_file: IpCoreFile) -> str:
126 return ip_core_file.path.name
128 for ip_core_file in sorted(ip_core_files, key=sort_by_file_name):
129 data += f"{ip_core_file.path}\n"
131 if ip_core_file.variables:
132 data += json.dumps(ip_core_file.variables, sort_keys=True)
133 data += "\n"
135 with ip_core_file.path.open("rb") as file_handle:
136 # Is considered insecure, but we don't need a cryptographically secure hash here.
137 # Just something that is fast and unique.
138 ip_hash = hashlib.md5() # noqa: S324
139 ip_hash.update(file_handle.read())
140 data += f"{ip_hash.hexdigest()}\n"
142 return data
144 def _save_hash(self) -> None:
145 create_file(self._hash_file, self._hash)
147 def _should_create(self) -> bool:
148 """
149 Return True if a Vivado project create is needed, i.e. if anything has changed.
150 """
151 if not (self._hash_file.exists() and self.compile_order_file.exists()):
152 return True
154 return read_file(self._hash_file) != self._hash