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

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 

9# Standard libraries 

10import hashlib 

11import json 

12from pathlib import Path 

13from typing import TYPE_CHECKING, Optional 

14 

15# First party libraries 

16from tsfpga.system_utils import create_file, delete, read_file 

17 

18# Local folder libraries 

19from .project import VivadoIpCoreProject 

20 

21if TYPE_CHECKING: 

22 # First party libraries 

23 from tsfpga.ip_core_file import IpCoreFile 

24 from tsfpga.module_list import ModuleList 

25 

26 

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

32 

33 project_name = "vivado_ip_project" 

34 

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 

52 

53 vivado_project_class = ( 

54 VivadoIpCoreProject if vivado_project_class is None else vivado_project_class 

55 ) 

56 

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

58 

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

60 

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" 

67 

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) 

74 

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) 

82 

83 assert success, "Failed to create Vivado IP core project" 

84 

85 self._save_hash() 

86 

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 

91 

92 * List of TCL files that create IP cores, 

93 * and contents of these files, 

94 

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. 

97 

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 

104 

105 return False 

106 

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 ) 

113 

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) 

118 

119 self._hash = self._calculate_hash(ip_core_files) 

120 

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

127 

128 def sort_by_file_name(ip_core_file: "IpCoreFile") -> str: 

129 return ip_core_file.path.name 

130 

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

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

133 

134 if ip_core_file.variables: 

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

136 data += "\n" 

137 

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" 

142 

143 return data 

144 

145 def _save_hash(self) -> None: 

146 create_file(self._hash_file, self._hash) 

147 

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 

154 

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