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

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# -------------------------------------------------------------------------------------------------- 

8 

9from __future__ import annotations 

10 

11import hashlib 

12import json 

13from typing import TYPE_CHECKING 

14 

15from tsfpga.system_utils import create_file, delete, read_file 

16 

17from .project import VivadoIpCoreProject 

18 

19if TYPE_CHECKING: 

20 from pathlib import Path 

21 

22 from tsfpga.ip_core_file import IpCoreFile 

23 from tsfpga.module_list import ModuleList 

24 

25 

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 """ 

31 

32 project_name = "vivado_ip_project" 

33 

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 

51 

52 vivado_project_class = ( 

53 VivadoIpCoreProject if vivado_project_class is None else vivado_project_class 

54 ) 

55 

56 self._hash_file = self.project_directory / "ip_files_hash.txt" 

57 

58 self._setup(modules=modules, vivado_project_class=vivado_project_class) 

59 

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" 

66 

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) 

73 

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) 

80 

81 if not self._vivado_project.create(self.project_directory): 

82 raise RuntimeError("Failed to create Vivado IP core project") 

83 

84 self._save_hash() 

85 

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 

90 

91 * List of TCL files that create IP cores, 

92 * and contents of these files, 

93 

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. 

96 

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 

103 

104 return False 

105 

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 ) 

110 

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) 

115 

116 self._hash = self._calculate_hash(ip_core_files) 

117 

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 = "" 

124 

125 def sort_by_file_name(ip_core_file: IpCoreFile) -> str: 

126 return ip_core_file.path.name 

127 

128 for ip_core_file in sorted(ip_core_files, key=sort_by_file_name): 

129 data += f"{ip_core_file.path}\n" 

130 

131 if ip_core_file.variables: 

132 data += json.dumps(ip_core_file.variables, sort_keys=True) 

133 data += "\n" 

134 

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" 

141 

142 return data 

143 

144 def _save_hash(self) -> None: 

145 create_file(self._hash_file, self._hash) 

146 

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 

153 

154 return read_file(self._hash_file) != self._hash