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
« 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."""
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
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)
22# -- Boards definitions file name.
23BOARDS_JSONC = "boards.jsonc"
25# -- FPGAs definitions file name.
26FPGAS_JSONC = "fpgas.jsonc"
28# -- Programmers definitions file name.
29PROGRAMMERS_JSONC = "programmers.jsonc"
31# -- A regex for validating boards, fpgas, and programmers ids.
32DEFINITION_ID_FORMAT = re.compile(r"^[a-z][a-z0-9-]*$")
34# -- A regex for validating usb vid and pid values.
35USB_ID_FORMAT = re.compile(r"^[0-9a-f]{4}$")
38class ApioDefinitions:
39 """Contains the apio definitions in the form of json dictionaries."""
41 # pylint: disable=too-many-instance-attributes
43 def __init__(
44 self,
45 package_definitions_dir: Path,
46 project_definitions_dir: Path | None,
47 ):
49 assert isinstance(package_definitions_dir, Path)
50 assert project_definitions_dir is None or isinstance(
51 project_definitions_dir, Path
52 )
54 self._package_definitions_dir = package_definitions_dir
55 self._project_definitions_dir = project_definitions_dir
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 )
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
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 )
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
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 )
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
113 # -- Validate the definitions we just loaded.
114 self._validate_definitions()
116 def _validate_definitions(self):
117 """Validate the boards, fpgas, and programmers definitions of this
118 instance."""
120 # pylint: disable=too-many-branches
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")
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 )
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 )
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 )
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 )
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}")
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 )
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 )
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 )
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
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
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
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 """
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()
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())
251 # -- All done.
252 return (combined_dict, custom_ids)
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 """
261 # pylint: disable=broad-exception-caught
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 )
273 # -- Return the object for the resource
274 return json_dict