Coverage for apio/utils/resource_util.py: 64%
89 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
1"""Utilities related to the Apio resource files."""
3import sys
4import re
5from typing import Any, Dict, Tuple
6from dataclasses import dataclass
7from jsonschema import validate
8from jsonschema.exceptions import ValidationError
9from apio.common.apio_console import cerror
12@dataclass(frozen=True)
13class ProjectResources:
14 """Contains the resources of the current project."""
16 board_id: str
17 board_info: Dict[str, Any]
18 fpga_id: str
19 fpga_info: Dict[str, Any]
20 programmer_id: str
21 programmer_info: Dict[str, Any]
24# -- JSON schema for validating board definitions in boards.jsonc.
25# -- The field 'description' is for information only.
26BOARD_SCHEMA = schema = {
27 "$schema": "http://json-schema.org/draft-07/schema#",
28 "type": "object",
29 "required": ["description", "fpga-id", "programmer"],
30 "properties": {
31 "description": {"type": "string"},
32 "legacy-name": {"type": "string"},
33 "fpga-id": {"type": "string"},
34 "programmer": {
35 "type": "object",
36 "required": ["id"],
37 "properties": {
38 "id": {"type": "string"},
39 "extra-args": {"type": "string"},
40 },
41 "additionalProperties": False,
42 },
43 "usb": {
44 "type": "object",
45 "required": ["vid", "pid"],
46 "properties": {
47 "vid": {"type": "string", "pattern": "^[0-9a-f]{4}$"},
48 "pid": {"type": "string", "pattern": "^[0-9a-f]{4}$"},
49 "product-regex": {"type": "string", "pattern": "^.*$"},
50 },
51 "additionalProperties": False,
52 },
53 "tinyprog": {
54 "type": "object",
55 "required": ["name-regex"],
56 "properties": {
57 "name-regex": {"type": "string", "pattern": "^.*$"},
58 },
59 "additionalProperties": False,
60 },
61 },
62 "additionalProperties": False,
63}
65# -- JSON schema for validating fpga definitions in fpga.jsonc.
66# -- The fields 'part-num' and 'size' are for information only.
67FPGA_SCHEMA = schema = {
68 "$schema": "http://json-schema.org/draft-07/schema#",
69 "type": "object",
70 "properties": {
71 "part-num": {"type": "string"},
72 "arch": {
73 "type": "string",
74 "enum": ["ice40", "ecp5", "gowin", "xilinx"],
75 },
76 "size": {"type": "string"},
77 "ice40-params": {
78 "type": "object",
79 "properties": {
80 "type": {"type": "string"},
81 "package": {"type": "string"},
82 },
83 "required": ["type", "package"],
84 "additionalProperties": False,
85 },
86 "ecp5-params": {
87 "type": "object",
88 "properties": {
89 "type": {"type": "string"},
90 "package": {"type": "string"},
91 "speed": {"type": "string"},
92 },
93 "required": ["type", "package", "speed"],
94 "additionalProperties": False,
95 },
96 "gowin-params": {
97 "type": "object",
98 "properties": {
99 "yosys-family": {"type": "string"},
100 "nextpnr-family": {"type": "string"},
101 "packer-device": {"type": "string"},
102 },
103 "required": ["yosys-family", "nextpnr-family", "packer-device"],
104 "additionalProperties": False,
105 },
106 "xilinx-params": {
107 "type": "object",
108 "properties": {
109 "family": {"type": "string"},
110 "yosys-arch": {"type": "string"},
111 "package": {"type": "string"},
112 "speed": {"type": "string"},
113 },
114 "required": ["family", "yosys-arch", "package", "speed"],
115 "additionalProperties": False,
116 },
117 },
118 "required": ["part-num", "arch", "size"],
119 "additionalProperties": False,
120}
123# -- JSON schema for validating programmer definitions in programmers.jsonc.
124PROGRAMMER_SCHEMA = {
125 "$schema": "http://json-schema.org/draft-07/schema#",
126 "type": "object",
127 "required": ["command", "args"],
128 "properties": {"command": {"type": "string"}, "args": {"type": "string"}},
129 "additionalProperties": False,
130}
132# -- JSON schema for validating config.jsonc.
133CONFIG_SCHEMA = {
134 "type": "object",
135 "required": [
136 "remote-config-ttl-days",
137 "remote-config-retry-minutes",
138 "remote-config-url",
139 ],
140 "properties": {
141 "remote-config-ttl-days": {"type": "integer", "minimum": 1},
142 "remote-config-retry-minutes": {"type": "integer", "minimum": 0},
143 "remote-config-url": {"type": "string"},
144 },
145 "additionalProperties": False,
146}
148# -- JSON schema for validating platforms.jsonc.
149PLATFORMS_SCHEMA = {
150 "type": "object",
151 "patternProperties": {
152 "^[a-z]+(-[a-z0-9]+)+$": { # matches keys like "darwin-arm64"
153 "type": "object",
154 "required": ["type", "variant"],
155 "properties": {
156 "type": {
157 "type": "string",
158 "enum": ["Mac OSX", "Linux", "Windows"],
159 },
160 "variant": {"type": "string"},
161 },
162 "additionalProperties": False,
163 }
164 },
165 "additionalProperties": False,
166}
168# -- JSON schema for validating packages.jsonc.
169PACKAGES_SCHEMA = {
170 "type": "object",
171 "patternProperties": {
172 "^[a-z0-9_-]+$": { # package names like "oss-cad-suite"
173 "type": "object",
174 "required": ["description", "env"],
175 "properties": {
176 "description": {"type": "string"},
177 "restricted-to-platforms": {
178 "type": "array",
179 "items": {"type": "string"},
180 },
181 "env": {
182 "type": "object",
183 "properties": {
184 "path": {"type": "array", "items": {"type": "string"}},
185 "unset-vars": {
186 "type": "array",
187 "items": {"type": "string"},
188 },
189 "set-vars": {
190 "type": "object",
191 "additionalProperties": {"type": "string"},
192 },
193 },
194 "additionalProperties": False,
195 },
196 },
197 "additionalProperties": False,
198 }
199 },
200 "additionalProperties": False,
201}
204def _validate_board_info(board_id: str, board_info: dict) -> None:
205 """Check the given board info and raise a fatal error on any error."""
206 try:
207 validate(instance=board_info, schema=BOARD_SCHEMA)
208 except ValidationError as e:
209 cerror(f"Invalid board definition [{board_id}]: {e.message}")
210 sys.exit(1)
213def validate_fpga_info(fpga_id: str, fpga_info: dict) -> None:
214 """Check the given fpga info and raise a fatal error on any error."""
215 try:
216 validate(instance=fpga_info, schema=FPGA_SCHEMA)
217 except ValidationError as e:
218 cerror(f"Invalid fpga definition [{fpga_id}]: {e.message}")
219 sys.exit(1)
221 # -- Expecting a params field for the specified architecture.
222 params_pattern = re.compile(r".*-params$")
223 actual_params = [key for key in fpga_info if params_pattern.match(key)]
224 expected_params = [fpga_info["arch"] + "-params"]
225 if actual_params != expected_params: 225 ↛ 226line 225 didn't jump to line 226 because the condition on line 225 was never true
226 cerror(f"Unexpected params {actual_params} in fpga {fpga_id}")
227 sys.exit(1)
230def _validate_programmer_info(
231 programmer_id: str, programmer_info: dict
232) -> None:
233 """Check the given programmer info and raise a fatal error on any error."""
234 try:
235 validate(instance=programmer_info, schema=PROGRAMMER_SCHEMA)
236 except ValidationError as e:
237 cerror(f"Invalid programmer definition [{programmer_id}]: {e.message}")
238 sys.exit(1)
241def validate_config(config: dict) -> None:
242 """Check the config resource from config.jsonc."""
243 try:
244 validate(instance=config, schema=CONFIG_SCHEMA)
245 except ValidationError as e:
246 cerror(f"Invalid config: {e.message}")
247 sys.exit(1)
250def validate_platforms(platforms: dict) -> None:
251 """Check the platforms resource from platforms.jsonc."""
252 try:
253 validate(instance=platforms, schema=PLATFORMS_SCHEMA)
254 except ValidationError as e:
255 cerror(f"Invalid platforms resource: {e.message}")
256 sys.exit(1)
259def validate_packages(packages: dict) -> None:
260 """Check the packages resource from platforms.jsonc."""
261 try:
262 validate(instance=packages, schema=PACKAGES_SCHEMA)
263 except ValidationError as e:
264 cerror(f"Invalid packages resource: {e.message}")
265 sys.exit(1)
268def validate_project_resources(res: ProjectResources) -> None:
269 """Check the resources of the current project. Exit with an error
270 message on any error."""
271 _validate_board_info(res.board_id, res.board_info)
272 validate_fpga_info(res.fpga_id, res.fpga_info)
273 _validate_programmer_info(res.programmer_id, res.programmer_info)
275 # TODO: Add here additional check.
278def collect_project_resources(
279 board_id: str, boards: dict, fpgas: dict, programmers: dict
280) -> ProjectResources:
281 """Collect and validate the resources used by a project. Since the
282 resources may be custom resources defined by the user, we need to
283 have a user friendly error handling and reporting."""
285 # -- Get the info.
286 board_info = boards.get(board_id, None)
287 if board_info is None: 287 ↛ 288line 287 didn't jump to line 288 because the condition on line 287 was never true
288 cerror(f"Unknown board id '{board_id}'.")
289 sys.exit(1)
291 # -- Get fpga id and info.
292 fpga_id = board_info.get("fpga-id", None)
293 if fpga_id is None: 293 ↛ 294line 293 didn't jump to line 294 because the condition on line 293 was never true
294 cerror(f"Board '{board_id}' has no 'fpga-id' field.")
295 sys.exit(1)
296 fpga_info = fpgas.get(fpga_id, None)
297 if fpga_info is None: 297 ↛ 298line 297 didn't jump to line 298 because the condition on line 297 was never true
298 cerror(f"Unknown fpga id '{fpga_id}'.")
299 sys.exit(1)
301 # -- Get programmer id and info.
302 programmer_id = board_info.get("programmer", {}).get("id", None)
303 if programmer_id is None: 303 ↛ 304line 303 didn't jump to line 304 because the condition on line 303 was never true
304 cerror(f"Board '{board_id}' has no 'programmer.id'.")
305 sys.exit(1)
306 programmer_info = programmers.get(programmer_id, None)
307 if programmer_info is None: 307 ↛ 308line 307 didn't jump to line 308 because the condition on line 307 was never true
308 cerror(f"Unknown programmer id '{programmer_id}'.")
309 sys.exit(1)
311 # -- Create the project resources bundle.
312 project_resources = ProjectResources(
313 board_id,
314 board_info,
315 fpga_id,
316 fpga_info,
317 programmer_id,
318 programmer_info,
319 )
321 # -- All done
322 return project_resources
325def get_fpga_arch_params(fpga_info: Dict) -> Tuple[str, Dict]:
326 """Extracts the arch specific params of an fpga, Returns a tuple
327 with the field name and the field value."""
328 arch = fpga_info["arch"]
329 field_name = arch + "-params"
330 field_value = fpga_info[field_name]
331 return (field_name, field_value)