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

139 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-07 05:08 +0000

1# This file is part of the Apio project 

2# (C) 2016-present FPGAwars 

3# Project authors: 

4# * Jesús Arroyo (Jesus89) (2016-2019) 

5# * Juan Gonzalez (obijuan) (2019-2024) 

6# * The Apio Team (2024-present) 

7# License: GPLv2 

8 

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

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

11 

12 

13import re 

14from dataclasses import dataclass 

15from pathlib import Path 

16import json 

17from apio.utils import jsonc, env_options 

18from apio.common import proto_util 

19from apio.common.apio_console import cout, fatal_error 

20from apio.common.proto.stubs.apio_definitions_pb2 import ( 

21 BoardDefinition, 

22 FpgaDefinition, 

23 ProgrammerDefinition, 

24) 

25 

26# -- Boards definitions file name. 

27BOARDS_JSONC = "boards.jsonc" 

28 

29# -- FPGAs definitions file name. 

30FPGAS_JSONC = "fpgas.jsonc" 

31 

32# -- Programmers definitions file name. 

33PROGRAMMERS_JSONC = "programmers.jsonc" 

34 

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

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

37 

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

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

40 

41 

42class ApioDefinitions: 

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

44 

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

46 

47 def __init__( 

48 self, 

49 apio_packages_dir: Path, 

50 project_definitions_dir: Path | None, 

51 ): 

52 # pylint: disable=too-many-locals 

53 

54 assert isinstance(apio_packages_dir, Path) 

55 assert project_definitions_dir is None or isinstance( 

56 project_definitions_dir, Path 

57 ) 

58 

59 self._apio_packages_dir = apio_packages_dir 

60 self._project_definitions_dir = project_definitions_dir 

61 

62 # -- The dir with the standard boards, fpgas, and programmers 

63 # -- definition files. Can be overridden with APIO_DEFINITIONS. 

64 self._package_definitions_dir = self._resolve_package_definitions_dir( 

65 apio_packages_dir 

66 ) 

67 

68 # -- Read boards definitions as a json_dict. 

69 # -- Custom definitions overrides apio standard definitions. 

70 boards_json, self.custom_boards_ids = self._load_definitions( 

71 {}, 

72 BOARDS_JSONC, 

73 self._package_definitions_dir, 

74 self._project_definitions_dir, 

75 ) 

76 

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

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

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

80 board_definition = proto_util.proto_from_json_dict( 

81 definition_dict, 

82 BoardDefinition, 

83 ) 

84 if board_definition is None: 

85 fatal_error(f"Failed to parse board definition '{board_id}") 

86 self.boards[board_id] = board_definition 

87 

88 # -- Read the base fpga definitions exported by the underlying tools. 

89 base_fpgas_dict = self._load_definitions_file( 

90 self._apio_packages_dir / "oss-cad-suite" / "FPGAS.jsonc" 

91 ) 

92 openxc7_fpgas_dict = self._load_definitions_file( 

93 self._apio_packages_dir / "openxc7" / "FPGAS.jsonc" 

94 ) 

95 for fpga_id, fpga_def in openxc7_fpgas_dict.items(): 

96 assert ( 

97 fpga_id not in base_fpgas_dict 

98 ), f"Duplicate fpga id: {fpga_id}" 

99 base_fpgas_dict[fpga_id] = fpga_def 

100 

101 # -- Read fpgas definitions as json dicts. 

102 # -- Custom definitions overrides apio standard definitions. 

103 fpgas_json, self.custom_fpgas_ids = self._load_definitions( 

104 base_fpgas_dict, 

105 FPGAS_JSONC, 

106 self._package_definitions_dir, 

107 self._project_definitions_dir, 

108 ) 

109 

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

111 # -- save. 

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

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

114 fpga_definition = proto_util.proto_from_json_dict( 

115 definition_dict, 

116 FpgaDefinition, 

117 ) 

118 if fpga_definition is None: 

119 fatal_error(f"Failed to parse fpga definition '{fpga_id}'") 

120 self.fpgas[fpga_id] = fpga_definition 

121 

122 # -- Load programmers definitions as json dicts. 

123 # -- Custom definitions overrides apio standard definitions. 

124 programmers_json, self.custom_programmers_ids = self._load_definitions( 

125 {}, 

126 PROGRAMMERS_JSONC, 

127 self._package_definitions_dir, 

128 self._project_definitions_dir, 

129 ) 

130 

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

132 # -- and save. 

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

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

135 programmer_definition = proto_util.proto_from_json_dict( 

136 definition_dict, 

137 ProgrammerDefinition, 

138 ) 

139 if programmer_definition is None: 

140 fatal_error( 

141 f"Failed to parse programmer definition '{programmer_id}" 

142 ) 

143 self.programmers[programmer_id] = programmer_definition 

144 

145 # -- Validate the definitions we just loaded. 

146 self._validate_definitions() 

147 

148 @property 

149 def package_definitions_dir(self) -> Path: 

150 """Returns the dir from which the standard boards, fpgas, and 

151 programmers definition files were loaded.""" 

152 return self._package_definitions_dir 

153 

154 @staticmethod 

155 def _resolve_package_definitions_dir(apio_packages_dir: Path) -> Path: 

156 """Returns the dir of the standard definition files. This is 

157 <packages dir>/definitions, unless overridden by the APIO_DEFINITIONS 

158 env var.""" 

159 override = env_options.get(env_options.APIO_DEFINITIONS) 

160 if override is None: 

161 return apio_packages_dir / "definitions" 

162 

163 override_dir = Path(override) 

164 if not override_dir.is_dir(): 

165 fatal_error( 

166 f"{env_options.APIO_DEFINITIONS} is not a directory: " 

167 f"'{override}'" 

168 ) 

169 return override_dir 

170 

171 def _validate_definitions(self): 

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

173 instance.""" 

174 

175 # pylint: disable=too-many-branches 

176 

177 # -- Validate boards definitions 

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

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

180 if not DEFINITION_ID_FORMAT.match(board_id): 

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

182 

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

184 proto_util.check_is_initialized( 

185 board_definition, 

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

187 ) 

188 

189 # -- Check that the fpga definition exits. 

190 proto_util.check_is_required(board_definition, "fpga_id") 

191 fpga_id = board_definition.fpga_id 

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

193 fatal_error( 

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

195 + f"fpga `{fpga_id}`" 

196 ) 

197 

198 # -- Check that the programmer definition exits. 

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

200 programmer_id = board_definition.programmer.id 

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

202 fatal_error( 

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

204 + f"programmer `{programmer_id}`" 

205 ) 

206 

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

208 # -- fields. 

209 proto_util.check_not_required(board_definition, "usb") 

210 if board_definition.HasField("usb"): 

211 usb = board_definition.usb 

212 # -- Check vid 

213 proto_util.check_not_required(usb, "vid") 

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

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

216 fatal_error( 

217 "The usb.vid field of the board " 

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

219 ) 

220 # -- check pid 

221 proto_util.check_not_required(usb, "pid") 

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

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

224 fatal_error( 

225 "The usb.pid field of the board " 

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

227 ) 

228 

229 # -- Validate fpgas definitions. 

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

231 # -- Check id format. 

232 if not DEFINITION_ID_FORMAT.match(fpga_id): 

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

234 

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

236 proto_util.check_is_initialized( 

237 fpga_definition, 

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

239 ) 

240 

241 # -- Validate programmers definitions. 

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

243 # -- Check id format. 

244 if not DEFINITION_ID_FORMAT.match(programmer_id): 

245 fatal_error( 

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

247 ) 

248 

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

250 proto_util.check_is_initialized( 

251 programmer_definition, 

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

253 ) 

254 

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

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

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

258 assert board_id in self.boards, board_id 

259 return board_id in self.custom_boards_ids 

260 

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

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

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

264 assert fpga_id in self.fpgas, fpga_id 

265 return fpga_id in self.custom_fpgas_ids 

266 

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

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

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

270 assert programmer_id in self.programmers, programmer_id 

271 return programmer_id in self.custom_programmers_ids 

272 

273 @classmethod 

274 def _load_definitions( 

275 cls, 

276 base_definitions: dict[str, dict], 

277 name: str, 

278 package_definitions_dir: Path, 

279 project_definitions_dir: Path | None, 

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

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

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

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

284 Returns a tuple with the merged standard and custom resource 

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

286 resource file. 

287 """ 

288 # -- Level 1 - Start with the base definition. 

289 combined_dict: dict[str, dict] = {} 

290 combined_dict.update(base_definitions) 

291 

292 # -- Level 2 - Load the standard definition from the package. 

293 package_file = package_definitions_dir / name 

294 package_dict = cls._load_definitions_file(package_file) 

295 combined_dict.update(package_dict) 

296 

297 # -- Level 3 - If there is a project specific override file, apply it 

298 # -- on top of the standard apio definition dict. These are the custom 

299 # -- definitions provided by the user. 

300 custom_ids: set[str] = set() 

301 if project_definitions_dir: 

302 project_file = project_definitions_dir / name 

303 if project_file.exists(): 

304 # -- Load the override json dict. 

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

306 project_dict = cls._load_definitions_file(project_file) 

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

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

309 combined_dict.update(project_dict) 

310 custom_ids.update(project_dict.keys()) 

311 

312 # -- Sort combined dict by key. 

313 combined_dict = dict(sorted(combined_dict.items())) 

314 

315 # -- All done. 

316 return (combined_dict, custom_ids) 

317 

318 @classmethod 

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

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

321 * OUTPUT: A dictionary with the jsonc file data 

322 In case of error it raises an exception and finish 

323 """ 

324 

325 # pylint: disable=broad-exception-caught 

326 

327 # -- Read the jsonc file 

328 try: 

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

330 json_text = jsonc.to_json(jsonc_text) 

331 json_dict = json.loads(json_text) 

332 except Exception as e: 

333 fatal_error( 

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

335 cause=e, 

336 ) 

337 

338 # -- Return the object for the resource 

339 return json_dict 

340 

341 

342@dataclass(frozen=True) 

343class ProjectDefinitions: 

344 """Contains the definitions of the current project.""" 

345 

346 board_id: str 

347 board_definition: BoardDefinition 

348 fpga_id: str 

349 fpga_definition: FpgaDefinition 

350 programmer_id: str 

351 programmer_definition: ProgrammerDefinition 

352 

353 

354def collect_project_definitions( 

355 board_id: str, 

356 definitions: ApioDefinitions, 

357) -> ProjectDefinitions: 

358 """Collect the board, fpga, and programmer definitions used by a project. 

359 Since the definitions may be custom definition defined by the user, we 

360 need to have a user friendly error handling and reporting.""" 

361 

362 # -- Get the board definition. 

363 board_definition = definitions.boards.get(board_id, None) 

364 if board_definition is None: 364 ↛ 365line 364 didn't jump to line 365 because the condition on line 364 was never true

365 fatal_error(f"Unknown board id '{board_id}'.") 

366 

367 # -- We assume below that these fields are required. 

368 proto_util.check_is_required(board_definition, "fpga_id", "programmer.id") 

369 

370 # -- Get fpga id and definition. 

371 fpga_id = board_definition.fpga_id 

372 fpga_definition = definitions.fpgas[fpga_id] 

373 

374 # -- Get programmer id and info. 

375 programmer_id = board_definition.programmer.id 

376 programmer_definition = definitions.programmers[programmer_id] 

377 

378 # -- Create the project definitions bundle. 

379 project_definitions = ProjectDefinitions( 

380 board_id, 

381 board_definition, 

382 fpga_id, 

383 fpga_definition, 

384 programmer_id, 

385 programmer_definition, 

386 ) 

387 

388 # -- All done 

389 return project_definitions