Coverage for tsfpga/vivado/ip_cores.py: 95%
61 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-20 20:52 +0000
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-20 20:52 +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# --------------------------------------------------------------------------------------------------
9# Standard libraries
10import hashlib
11import json
12from pathlib import Path
13from typing import TYPE_CHECKING, Optional
15# First party libraries
16from tsfpga.system_utils import create_file, delete, read_file
18# Local folder libraries
19from .project import VivadoIpCoreProject
21if TYPE_CHECKING:
22 # First party libraries
23 from tsfpga.ip_core_file import IpCoreFile
24 from tsfpga.module_list import ModuleList
27class VivadoIpCores:
28 """
29 Handle a list of IP core sources. Has a mechanism to detect whether a regenerate of IP files
30 is needed.
31 """
33 project_name = "vivado_ip_project"
35 def __init__(
36 self,
37 modules: "ModuleList",
38 output_path: Path,
39 part_name: str,
40 vivado_project_class: Optional[type["VivadoIpCoreProject"]] = None,
41 ) -> None:
42 """
43 Arguments:
44 modules: IP cores from these modules will be included.
45 output_path: The Vivado project will be placed here.
46 part_name: Vivado part name to be used for the project.
47 vivado_project_class: The Vivado project class that will be used for the IP core
48 project. Is safe to leave at default in most cases.
49 """
50 self.project_directory = output_path.resolve() / self.project_name
51 self._part_name = part_name
53 vivado_project_class = (
54 VivadoIpCoreProject if vivado_project_class is None else vivado_project_class
55 )
57 self._hash_file = self.project_directory / "ip_files_hash.txt"
59 self._setup(modules=modules, vivado_project_class=vivado_project_class)
61 @property
62 def compile_order_file(self) -> Path:
63 """
64 pathlib.Path: Path to the generated compile order file.
65 """
66 return self.project_directory / "compile_order.txt"
68 @property
69 def vivado_project_file(self) -> Path:
70 """
71 pathlib.Path: Path to the Vivado project file.
72 """
73 return self._vivado_project.project_file(self.project_directory)
75 def create_vivado_project(self) -> None:
76 """
77 Create IP core Vivado project.
78 """
79 print(f"Creating IP core project in {self.project_directory}")
80 delete(self.project_directory)
81 success = self._vivado_project.create(self.project_directory)
83 assert success, "Failed to create Vivado IP core project"
85 self._save_hash()
87 def create_vivado_project_if_needed(self) -> bool:
88 """
89 Create IP core Vivado project if anything has changed since last time this was run.
90 If
92 * List of TCL files that create IP cores,
93 * and contents of these files,
95 is the same then it will not create. But if anything is added or removed from the list,
96 or the contents of a TCL file is changed, there will be a recreation.
98 Return:
99 True if Vivado project was created. False otherwise.
100 """
101 if self._should_create():
102 self.create_vivado_project()
103 return True
105 return False
107 def _setup(
108 self, modules: "ModuleList", vivado_project_class: type["VivadoIpCoreProject"]
109 ) -> None:
110 self._vivado_project = vivado_project_class(
111 name=self.project_name, modules=modules, part=self._part_name
112 )
114 ip_core_files = []
115 for module in modules:
116 # Send the same two arguments that are sent in the VivadoProject create flow
117 ip_core_files += module.get_ip_core_files(generics={}, part=self._part_name)
119 self._hash = self._calculate_hash(ip_core_files)
121 @staticmethod
122 def _calculate_hash(ip_core_files: list["IpCoreFile"]) -> str:
123 """
124 A string with hashes of the different IP core files.
125 """
126 data = ""
128 def sort_by_file_name(ip_core_file: "IpCoreFile") -> str:
129 return ip_core_file.path.name
131 for ip_core_file in sorted(ip_core_files, key=sort_by_file_name):
132 data += f"{ip_core_file.path}\n"
134 if ip_core_file.variables:
135 data += json.dumps(ip_core_file.variables, sort_keys=True)
136 data += "\n"
138 with open(ip_core_file.path, "rb") as file_handle:
139 ip_hash = hashlib.md5()
140 ip_hash.update(file_handle.read())
141 data += f"{ip_hash.hexdigest()}\n"
143 return data
145 def _save_hash(self) -> None:
146 create_file(self._hash_file, self._hash)
148 def _should_create(self) -> bool:
149 """
150 Return True if a Vivado project create is needed, i.e. if anything has changed.
151 """
152 if not (self._hash_file.exists() and self.compile_order_file.exists()):
153 return True
155 return read_file(self._hash_file) != self._hash