Coverage for apio / utils / resource_util.py: 64%
89 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-26 02:38 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-26 02:38 +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": {"type": "string", "enum": ["ice40", "ecp5", "gowin"]},
73 "size": {"type": "string"},
74 "ice40-params": {
75 "type": "object",
76 "properties": {
77 "type": {"type": "string"},
78 "package": {"type": "string"},
79 },
80 "required": ["type", "package"],
81 "additionalProperties": False,
82 },
83 "ecp5-params": {
84 "type": "object",
85 "properties": {
86 "type": {"type": "string"},
87 "package": {"type": "string"},
88 "speed": {"type": "string"},
89 },
90 "required": ["type", "package", "speed"],
91 "additionalProperties": False,
92 },
93 "gowin-params": {
94 "type": "object",
95 "properties": {
96 "yosys-family": {"type": "string"},
97 "nextpnr-family": {"type": "string"},
98 "packer-device": {"type": "string"},
99 },
100 "required": ["yosys-family", "nextpnr-family", "packer-device"],
101 "additionalProperties": False,
102 },
103 },
104 "required": ["part-num", "arch", "size"],
105 "additionalProperties": False,
106}
109# -- JSON schema for validating programmer definitions in programmers.jsonc.
110PROGRAMMER_SCHEMA = {
111 "$schema": "http://json-schema.org/draft-07/schema#",
112 "type": "object",
113 "required": ["command", "args"],
114 "properties": {"command": {"type": "string"}, "args": {"type": "string"}},
115 "additionalProperties": False,
116}
118# -- JSON schema for validating config.jsonc.
119CONFIG_SCHEMA = {
120 "type": "object",
121 "required": [
122 "remote-config-ttl-days",
123 "remote-config-retry-minutes",
124 "remote-config-url",
125 ],
126 "properties": {
127 "remote-config-ttl-days": {"type": "integer", "minimum": 1},
128 "remote-config-retry-minutes": {"type": "integer", "minimum": 0},
129 "remote-config-url": {"type": "string"},
130 },
131 "additionalProperties": False,
132}
134# -- JSON schema for validating platforms.jsonc.
135PLATFORMS_SCHEMA = {
136 "type": "object",
137 "patternProperties": {
138 "^[a-z]+(-[a-z0-9]+)+$": { # matches keys like "darwin-arm64"
139 "type": "object",
140 "required": ["type", "variant"],
141 "properties": {
142 "type": {
143 "type": "string",
144 "enum": ["Mac OSX", "Linux", "Windows"],
145 },
146 "variant": {"type": "string"},
147 },
148 "additionalProperties": False,
149 }
150 },
151 "additionalProperties": False,
152}
154# -- JSON schema for validating packages.jsonc.
155PACKAGES_SCHEMA = {
156 "type": "object",
157 "patternProperties": {
158 "^[a-z0-9_-]+$": { # package names like "oss-cad-suite"
159 "type": "object",
160 "required": ["description", "env"],
161 "properties": {
162 "description": {"type": "string"},
163 "restricted-to-platforms": {
164 "type": "array",
165 "items": {"type": "string"},
166 },
167 "env": {
168 "type": "object",
169 "properties": {
170 "path": {"type": "array", "items": {"type": "string"}},
171 "unset-vars": {
172 "type": "array",
173 "items": {"type": "string"},
174 },
175 "set-vars": {
176 "type": "object",
177 "additionalProperties": {"type": "string"},
178 },
179 },
180 "additionalProperties": False,
181 },
182 },
183 "additionalProperties": False,
184 }
185 },
186 "additionalProperties": False,
187}
190def _validate_board_info(board_id: str, board_info: dict) -> None:
191 """Check the given board info and raise a fatal error on any error."""
192 try:
193 validate(instance=board_info, schema=BOARD_SCHEMA)
194 except ValidationError as e:
195 cerror(f"Invalid board definition [{board_id}]: {e.message}")
196 sys.exit(1)
199def validate_fpga_info(fpga_id: str, fpga_info: dict) -> None:
200 """Check the given fpga info and raise a fatal error on any error."""
201 try:
202 validate(instance=fpga_info, schema=FPGA_SCHEMA)
203 except ValidationError as e:
204 cerror(f"Invalid fpga definition [{fpga_id}]: {e.message}")
205 sys.exit(1)
207 # -- Expecting a params field for the specified architecture.
208 params_pattern = re.compile(r".*-params$")
209 actual_params = [key for key in fpga_info if params_pattern.match(key)]
210 expected_params = [fpga_info["arch"] + "-params"]
211 if actual_params != expected_params: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true
212 cerror(f"Unexpected params {actual_params} in fpga {fpga_id}")
213 sys.exit(1)
216def _validate_programmer_info(
217 programmer_id: str, programmer_info: dict
218) -> None:
219 """Check the given programmer info and raise a fatal error on any error."""
220 try:
221 validate(instance=programmer_info, schema=PROGRAMMER_SCHEMA)
222 except ValidationError as e:
223 cerror(f"Invalid programmer definition [{programmer_id}]: {e.message}")
224 sys.exit(1)
227def validate_config(config: dict) -> None:
228 """Check the config resource from config.jsonc."""
229 try:
230 validate(instance=config, schema=CONFIG_SCHEMA)
231 except ValidationError as e:
232 cerror(f"Invalid config: {e.message}")
233 sys.exit(1)
236def validate_platforms(platforms: dict) -> None:
237 """Check the platforms resource from platforms.jsonc."""
238 try:
239 validate(instance=platforms, schema=PLATFORMS_SCHEMA)
240 except ValidationError as e:
241 cerror(f"Invalid platforms resource: {e.message}")
242 sys.exit(1)
245def validate_packages(packages: dict) -> None:
246 """Check the packages resource from platforms.jsonc."""
247 try:
248 validate(instance=packages, schema=PACKAGES_SCHEMA)
249 except ValidationError as e:
250 cerror(f"Invalid packages resource: {e.message}")
251 sys.exit(1)
254def validate_project_resources(res: ProjectResources) -> None:
255 """Check the resources of the current project. Exit with an error
256 message on any error."""
257 _validate_board_info(res.board_id, res.board_info)
258 validate_fpga_info(res.fpga_id, res.fpga_info)
259 _validate_programmer_info(res.programmer_id, res.programmer_info)
261 # TODO: Add here additional check.
264def collect_project_resources(
265 board_id: str, boards: dict, fpgas: dict, programmers: dict
266) -> ProjectResources:
267 """Collect and validate the resources used by a project. Since the
268 resources may be custom resources defined by the user, we need to
269 have a user friendly error handling and reporting."""
271 # -- Get the info.
272 board_info = boards.get(board_id, None)
273 if board_info is None: 273 ↛ 274line 273 didn't jump to line 274 because the condition on line 273 was never true
274 cerror(f"Unknown board id '{board_id}'.")
275 sys.exit(1)
277 # -- Get fpga id and info.
278 fpga_id = board_info.get("fpga-id", None)
279 if fpga_id is None: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true
280 cerror(f"Board '{board_id}' has no 'fpga-id' field.")
281 sys.exit(1)
282 fpga_info = fpgas.get(fpga_id, None)
283 if fpga_info is None: 283 ↛ 284line 283 didn't jump to line 284 because the condition on line 283 was never true
284 cerror(f"Unknown fpga id '{fpga_id}'.")
285 sys.exit(1)
287 # -- Get programmer id and info.
288 programmer_id = board_info.get("programmer", {}).get("id", None)
289 if programmer_id is None: 289 ↛ 290line 289 didn't jump to line 290 because the condition on line 289 was never true
290 cerror(f"Board '{board_id}' has no 'programmer.id'.")
291 sys.exit(1)
292 programmer_info = programmers.get(programmer_id, None)
293 if programmer_info is None: 293 ↛ 294line 293 didn't jump to line 294 because the condition on line 293 was never true
294 cerror(f"Unknown programmer id '{programmer_id}'.")
295 sys.exit(1)
297 # -- Create the project resources bundle.
298 project_resources = ProjectResources(
299 board_id,
300 board_info,
301 fpga_id,
302 fpga_info,
303 programmer_id,
304 programmer_info,
305 )
307 # -- All done
308 return project_resources
311def get_fpga_arch_params(fpga_info: Dict) -> Tuple[str, Dict]:
312 """Extracts the arch specific params of an fpga, Returns a tuple
313 with the field name and the field value."""
314 arch = fpga_info["arch"]
315 field_name = arch + "-params"
316 field_value = fpga_info[field_name]
317 return (field_name, field_value)