Coverage for apio/managers/apio_definitions.py: 91%

95 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-23 03:53 +0000

1"""The apio definitions manager class. This class manages the standard 

2and custom boards, fpgas, and programmers definitions.""" 

3 

4# -*- coding: utf-8 -*- 

5# -- This file is part of the Apio project 

6# -- (C) 2016-2019 FPGAwars 

7# -- Author Jesús Arroyo 

8# -- License GPLv2 

9 

10 

11import re 

12from pathlib import Path 

13import json5 

14from apio.common import proto_util 

15from apio.common.apio_console import cout, fatal_error 

16from apio.common.proto.apio_definitions_pb2 import ( 

17 BoardDefinition, 

18 FpgaDefinition, 

19 ProgrammerDefinition, 

20) 

21 

22# -- Boards definitions file name. 

23BOARDS_JSONC = "boards.jsonc" 

24 

25# -- FPGAs definitions file name. 

26FPGAS_JSONC = "fpgas.jsonc" 

27 

28# -- Programmers definitions file name. 

29PROGRAMMERS_JSONC = "programmers.jsonc" 

30 

31# -- A regex for validating boards, fpgas, and programmers ids. 

32DEFINITION_ID_FORMAT = re.compile(r"^[a-z][a-z0-9-]*$") 

33 

34# -- A regex for validating usb vid and pid values. 

35USB_ID_FORMAT = re.compile(r"^[0-9a-f]{4}$") 

36 

37 

38class ApioDefinitions: 

39 """Contains the apio definitions in the form of json dictionaries.""" 

40 

41 # pylint: disable=too-many-instance-attributes 

42 

43 def __init__( 

44 self, 

45 package_definitions_dir: Path, 

46 project_definitions_dir: Path | None, 

47 ): 

48 

49 assert isinstance(package_definitions_dir, Path) 

50 assert project_definitions_dir is None or isinstance( 

51 project_definitions_dir, Path 

52 ) 

53 

54 self._package_definitions_dir = package_definitions_dir 

55 self._project_definitions_dir = project_definitions_dir 

56 

57 # -- Read boards definitions as json_dicts. 

58 # -- Custom definitions overrides apio standard definitions. 

59 boards_json, self.custom_boards_ids = self._load_definitions( 

60 BOARDS_JSONC, 

61 self._package_definitions_dir, 

62 self._project_definitions_dir, 

63 ) 

64 

65 # -- Convert the board definition to BoardDefinition protos and save. 

66 self.boards: dict[str, BoardDefinition] = {} 

67 for board_id, definition_dict in boards_json.items(): 

68 board_definition = proto_util.proto_from_json_dict( 

69 definition_dict, 

70 BoardDefinition, 

71 f"Failed to parse board definition '{board_id}", 

72 ) 

73 self.boards[board_id] = board_definition 

74 

75 # -- Read fpgas definitions as json dicts. 

76 # -- Custom definitions overrides apio standard definitions. 

77 fpgas_json, self.custom_fpgas_ids = self._load_definitions( 

78 FPGAS_JSONC, 

79 self._package_definitions_dir, 

80 self._project_definitions_dir, 

81 ) 

82 

83 # -- Convert the fpgas definition dicts to FpgasDefinition protos and 

84 # -- save. 

85 self.fpgas: dict[str, FpgaDefinition] = {} 

86 for fpga_id, definition_dict in fpgas_json.items(): 

87 fpga_definition = proto_util.proto_from_json_dict( 

88 definition_dict, 

89 FpgaDefinition, 

90 f"Failed to parse fpga definition '{fpga_id}", 

91 ) 

92 self.fpgas[fpga_id] = fpga_definition 

93 

94 # -- Load programmers definitions as json dicts. 

95 # -- Custom definitions overrides apio standard definitions. 

96 programmers_json, self.custom_programmers_ids = self._load_definitions( 

97 PROGRAMMERS_JSONC, 

98 self._package_definitions_dir, 

99 self._project_definitions_dir, 

100 ) 

101 

102 # -- Convert the programmers definition dicts to FpgaDefinition protos 

103 # -- and save. 

104 self.programmers: dict[str, ProgrammerDefinition] = {} 

105 for programmer_id, definition_dict in programmers_json.items(): 

106 programmer_definition = proto_util.proto_from_json_dict( 

107 definition_dict, 

108 ProgrammerDefinition, 

109 f"Failed to parse programmer definition '{programmer_id}", 

110 ) 

111 self.programmers[programmer_id] = programmer_definition 

112 

113 # -- Validate the definitions we just loaded. 

114 self._validate_definitions() 

115 

116 def _validate_definitions(self): 

117 """Validate the boards, fpgas, and programmers definitions of this 

118 instance.""" 

119 

120 # pylint: disable=too-many-branches 

121 

122 # -- Validate boards definitions 

123 for board_id, board_definition in self.boards.items(): 

124 # -- Check that board id has a valid format. 

125 if not DEFINITION_ID_FORMAT.match(board_id): 

126 fatal_error(f"Board id `{board_id}` has an invalid format") 

127 

128 # -- Check that the definition proto is fully initialized. 

129 proto_util.check_is_initialized( 

130 board_definition, 

131 f"Failed to initialized board definition '{board_id}'", 

132 ) 

133 

134 # -- Check that the fpga definition exits. 

135 proto_util.check_is_required(board_definition, "fpga_id") 

136 fpga_id = board_definition.fpga_id 

137 if fpga_id not in self.fpgas: 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true

138 fatal_error( 

139 f"Board `{board_id}` refers to non existing " 

140 + f"fpga `{fpga_id}`" 

141 ) 

142 

143 # -- Check that the programmer definition exits. 

144 proto_util.check_is_required(board_definition, "programmer.id") 

145 programmer_id = board_definition.programmer.id 

146 if programmer_id not in self.programmers: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true

147 fatal_error( 

148 f"Board `{board_id}` refers to non existing " 

149 + f"programmer `{programmer_id}`" 

150 ) 

151 

152 # -- Validate the format of the optional usb.vid and usb.pid 

153 # -- fields. 

154 proto_util.check_not_required(board_definition, "usb") 

155 if board_definition.HasField("usb"): 

156 usb = board_definition.usb 

157 # -- Check vid 

158 proto_util.check_not_required(usb, "vid") 

159 if usb.HasField("vid"): 159 ↛ 166line 159 didn't jump to line 166 because the condition on line 159 was always true

160 if not USB_ID_FORMAT.match(usb.vid): 160 ↛ 161line 160 didn't jump to line 161 because the condition on line 160 was never true

161 fatal_error( 

162 "The usb.vid field of the board " 

163 + f"'{board_id}' has invalid value `{usb.vid}`" 

164 ) 

165 # -- check pid 

166 proto_util.check_not_required(usb, "pid") 

167 if usb.HasField("pid"): 167 ↛ 123line 167 didn't jump to line 123 because the condition on line 167 was always true

168 if not USB_ID_FORMAT.match(usb.pid): 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true

169 fatal_error( 

170 "The usb.pid field of the board " 

171 + f"'{board_id}' has invalid value `{usb.vip}`" 

172 ) 

173 

174 # -- Validate fpgas definitions. 

175 for fpga_id, fpga_definition in self.fpgas.items(): 

176 # -- Check id format. 

177 if not DEFINITION_ID_FORMAT.match(fpga_id): 

178 fatal_error(f"FPGA id has an invalid format: {fpga_id}") 

179 

180 # -- Check that the definition proto is fully initialized. 

181 proto_util.check_is_initialized( 

182 fpga_definition, 

183 f"Failed to initialize fpga definition '{fpga_id}'", 

184 ) 

185 

186 # -- Validate programmers definitions. 

187 for programmer_id, programmer_definition in self.programmers.items(): 

188 # -- Check id format. 

189 if not DEFINITION_ID_FORMAT.match(programmer_id): 

190 fatal_error( 

191 f"Programmer id has an invalid format: {programmer_id}" 

192 ) 

193 

194 # -- Check that the definition proto is fully initialized. 

195 proto_util.check_is_initialized( 

196 programmer_definition, 

197 f"Failed to initialize programmer definition '{fpga_id}'", 

198 ) 

199 

200 def is_custom_board(self, board_id: str) -> bool: 

201 """Returns true if the board's definition was loaded from a 

202 project's boards.jsonc file.""" 

203 assert board_id in self.boards, board_id 

204 return board_id in self.custom_boards_ids 

205 

206 def is_custom_fpga(self, fpga_id: str) -> bool: 

207 """Returns true if the fpga's definition was loaded from a 

208 project's fpgas.jsonc file.""" 

209 assert fpga_id in self.fpgas, fpga_id 

210 return fpga_id in self.custom_fpgas_ids 

211 

212 def is_custom_programmer(self, programmer_id: str) -> bool: 

213 """Returns true if the programmer's definition was loaded from a 

214 project's programmers.jsonc file.""" 

215 assert programmer_id in self.programmers, programmer_id 

216 return programmer_id in self.custom_programmers_ids 

217 

218 @classmethod 

219 def _load_definitions( 

220 cls, 

221 name: str, 

222 package_definitions_dir: Path, 

223 project_definitions_dir: Path | None, 

224 ) -> tuple[dict[str, dict], set[str]]: 

225 """Load a jsonc file. Try first from custom_dir, if given, and then 

226 from standard dir. This method is called for resource files in 

227 apio/resources and definitions files in the definitions packages. 

228 Returns a tuple with the merged standard and custom resource 

229 definitions (custom wins) and a set of boards ids in the custom 

230 resource file. 

231 """ 

232 

233 # -- Load the standard definition as a json dict. 

234 filepath = package_definitions_dir / name 

235 combined_dict = cls._load_definitions_file(filepath) 

236 custom_ids: set[str] = set() 

237 

238 # -- If there is a project specific override file, apply it on 

239 # -- top of the standard apio definition dict. 

240 if project_definitions_dir: 

241 filepath = project_definitions_dir / name 

242 if filepath.exists(): 

243 # -- Load the override json dict. 

244 cout(f"Loading custom '{name}'.") 

245 custom_dict = cls._load_definitions_file(filepath) 

246 # -- Apply the override. Entries in override replace same 

247 # -- key entries in result or if unique are added. 

248 combined_dict.update(custom_dict) 

249 custom_ids.update(custom_dict.keys()) 

250 

251 # -- All done. 

252 return (combined_dict, custom_ids) 

253 

254 @classmethod 

255 def _load_definitions_file(cls, filepath: Path) -> dict: 

256 """Load the resources from a given jsonc file path 

257 * OUTPUT: A dictionary with the jsonc file data 

258 In case of error it raises an exception and finish 

259 """ 

260 

261 # pylint: disable=broad-exception-caught 

262 

263 # -- Read the jsonc file 

264 try: 

265 jsonc_text = filepath.read_text(encoding="utf-8") 

266 json_dict = json5.loads(jsonc_text) 

267 except Exception as e: 

268 fatal_error( 

269 f"Failed to read and parse definition file {filepath.name}", 

270 cause=e, 

271 ) 

272 

273 # -- Return the object for the resource 

274 return json_dict