Coverage for tsfpga/vivado/ip_cores.py: 94%
62 statements
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-20 20:51 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2024-11-20 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# --------------------------------------------------------------------------------------------------
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
26 # Local folder libraries
27 from .project import VivadoProject
30class VivadoIpCores:
31 """
32 Handle a list of IP core sources. Has a mechanism to detect whether a regenerate of IP files
33 is needed.
34 """
36 project_name = "vivado_ip_project"
38 def __init__(
39 self,
40 modules: "ModuleList",
41 output_path: Path,
42 part_name: str,
43 vivado_project_class: Optional[type["VivadoProject"]] = None,
44 ) -> None:
45 """
46 Arguments:
47 modules: IP cores from these modules will be included.
48 output_path: The Vivado project will be placed here.
49 part_name: Vivado part name to be used for the project.
50 vivado_project_class: The Vivado project class that will be used for the IP core
51 project. Is safe to leave at default in most cases.
52 """
53 self.project_directory = output_path.resolve() / self.project_name
54 self._part_name = part_name
56 vivado_project_class = (
57 VivadoIpCoreProject if vivado_project_class is None else vivado_project_class
58 )
60 self._hash_file = self.project_directory / "ip_files_hash.txt"
62 self._setup(modules, vivado_project_class)
64 @property
65 def compile_order_file(self) -> Path:
66 """
67 pathlib.Path: Path to the generated compile order file.
68 """
69 return self.project_directory / "compile_order.txt"
71 @property
72 def vivado_project_file(self) -> Path:
73 """
74 pathlib.Path: Path to the Vivado project file.
75 """
76 return self._vivado_project.project_file(self.project_directory)
78 def create_vivado_project(self) -> None:
79 """
80 Create IP core Vivado project.
81 """
82 print(f"Creating IP core project in {self.project_directory}")
83 delete(self.project_directory)
84 success = self._vivado_project.create(self.project_directory)
86 assert success, "Failed to create Vivado IP core project"
88 self._save_hash()
90 def create_vivado_project_if_needed(self) -> bool:
91 """
92 Create IP core Vivado project if anything has changed since last time this was run.
93 If
95 * List of TCL files that create IP cores,
96 * and contents of these files,
98 is the same then it will not create. But if anything is added or removed from the list,
99 or the contents of a TCL file is changed, there will be a recreation.
101 Return:
102 True if Vivado project was created. False otherwise.
103 """
104 if self._should_create():
105 self.create_vivado_project()
106 return True
108 return False
110 def _setup(self, modules: "ModuleList", vivado_project_class: type["VivadoProject"]) -> None:
111 self._vivado_project = vivado_project_class(
112 name=self.project_name, modules=modules, part=self._part_name
113 )
115 ip_core_files = []
116 for module in modules:
117 # Send the same two arguments that are sent in the VivadoProject create flow
118 ip_core_files += module.get_ip_core_files(generics={}, part=self._part_name)
120 self._hash = self._calculate_hash(ip_core_files)
122 @staticmethod
123 def _calculate_hash(ip_core_files: list["IpCoreFile"]) -> str:
124 """
125 A string with hashes of the different IP core files.
126 """
127 data = ""
129 def sort_by_file_name(ip_core_file: "IpCoreFile") -> str:
130 return ip_core_file.path.name
132 for ip_core_file in sorted(ip_core_files, key=sort_by_file_name):
133 data += f"{ip_core_file.path}\n"
135 if ip_core_file.variables:
136 data += json.dumps(ip_core_file.variables, sort_keys=True)
137 data += "\n"
139 with open(ip_core_file.path, "rb") as file_handle:
140 ip_hash = hashlib.md5()
141 ip_hash.update(file_handle.read())
142 data += f"{ip_hash.hexdigest()}\n"
144 return data
146 def _save_hash(self) -> None:
147 create_file(self._hash_file, self._hash)
149 def _should_create(self) -> bool:
150 """
151 Return True if a Vivado project create is needed, i.e. if anything has changed.
152 """
153 if not (self._hash_file.exists() and self.compile_order_file.exists()):
154 return True
156 return read_file(self._hash_file) != self._hash