Coverage for apio/utils/resource_util.py: 65%
82 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 01:55 +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}
149# -- JSON schema for validating packages.jsonc.
150PACKAGES_SCHEMA = {
151 "type": "object",
152 "patternProperties": {
153 "^[a-z0-9_-]+$": { # package names like "oss-cad-suite"
154 "type": "object",
155 "required": ["description", "env"],
156 "properties": {
157 "description": {"type": "string"},
158 "restricted-to-platforms": {
159 "type": "array",
160 "items": {"type": "string"},
161 },
162 "env": {
163 "type": "object",
164 "properties": {
165 "path": {"type": "array", "items": {"type": "string"}},
166 "unset-vars": {
167 "type": "array",
168 "items": {"type": "string"},
169 },
170 "set-vars": {
171 "type": "object",
172 "additionalProperties": {"type": "string"},
173 },
174 },
175 "additionalProperties": False,
176 },
177 },
178 "additionalProperties": False,
179 }
180 },
181 "additionalProperties": False,
182}
185def _validate_board_info(board_id: str, board_info: dict) -> None:
186 """Check the given board info and raise a fatal error on any error."""
187 try:
188 validate(instance=board_info, schema=BOARD_SCHEMA)
189 except ValidationError as e:
190 cerror(f"Invalid board definition [{board_id}]: {e.message}")
191 sys.exit(1)
194def validate_fpga_info(fpga_id: str, fpga_info: dict) -> None:
195 """Check the given fpga info and raise a fatal error on any error."""
196 try:
197 validate(instance=fpga_info, schema=FPGA_SCHEMA)
198 except ValidationError as e:
199 cerror(f"Invalid fpga definition [{fpga_id}]: {e.message}")
200 sys.exit(1)
202 # -- Expecting a params field for the specified architecture.
203 params_pattern = re.compile(r".*-params$")
204 actual_params = [key for key in fpga_info if params_pattern.match(key)]
205 expected_params = [fpga_info["arch"] + "-params"]
206 if actual_params != expected_params: 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true
207 cerror(f"Unexpected params {actual_params} in fpga {fpga_id}")
208 sys.exit(1)
211def _validate_programmer_info(
212 programmer_id: str, programmer_info: dict
213) -> None:
214 """Check the given programmer info and raise a fatal error on any error."""
215 try:
216 validate(instance=programmer_info, schema=PROGRAMMER_SCHEMA)
217 except ValidationError as e:
218 cerror(f"Invalid programmer definition [{programmer_id}]: {e.message}")
219 sys.exit(1)
222def validate_config(config: dict) -> None:
223 """Check the config resource from config.jsonc."""
224 try:
225 validate(instance=config, schema=CONFIG_SCHEMA)
226 except ValidationError as e:
227 cerror(f"Invalid config: {e.message}")
228 sys.exit(1)
231def validate_packages(packages: dict) -> None:
232 """Check the packages resource from packages.jsonc."""
233 try:
234 validate(instance=packages, schema=PACKAGES_SCHEMA)
235 except ValidationError as e:
236 cerror(f"Invalid packages resource: {e.message}")
237 sys.exit(1)
240def validate_project_resources(res: ProjectResources) -> None:
241 """Check the resources of the current project. Exit with an error
242 message on any error."""
243 _validate_board_info(res.board_id, res.board_info)
244 validate_fpga_info(res.fpga_id, res.fpga_info)
245 _validate_programmer_info(res.programmer_id, res.programmer_info)
247 # TODO: Add here additional check.
250def collect_project_resources(
251 board_id: str, boards: dict, fpgas: dict, programmers: dict
252) -> ProjectResources:
253 """Collect and validate the resources used by a project. Since the
254 resources may be custom resources defined by the user, we need to
255 have a user friendly error handling and reporting."""
257 # -- Get the info.
258 board_info = boards.get(board_id, None)
259 if board_info is None: 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true
260 cerror(f"Unknown board id '{board_id}'.")
261 sys.exit(1)
263 # -- Get fpga id and info.
264 fpga_id = board_info.get("fpga-id", None)
265 if fpga_id is None: 265 ↛ 266line 265 didn't jump to line 266 because the condition on line 265 was never true
266 cerror(f"Board '{board_id}' has no 'fpga-id' field.")
267 sys.exit(1)
268 fpga_info = fpgas.get(fpga_id, None)
269 if fpga_info is None: 269 ↛ 270line 269 didn't jump to line 270 because the condition on line 269 was never true
270 cerror(f"Unknown fpga id '{fpga_id}'.")
271 sys.exit(1)
273 # -- Get programmer id and info.
274 programmer_id = board_info.get("programmer", {}).get("id", None)
275 if programmer_id is None: 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true
276 cerror(f"Board '{board_id}' has no 'programmer.id'.")
277 sys.exit(1)
278 programmer_info = programmers.get(programmer_id, None)
279 if programmer_info is None: 279 ↛ 280line 279 didn't jump to line 280 because the condition on line 279 was never true
280 cerror(f"Unknown programmer id '{programmer_id}'.")
281 sys.exit(1)
283 # -- Create the project resources bundle.
284 project_resources = ProjectResources(
285 board_id,
286 board_info,
287 fpga_id,
288 fpga_info,
289 programmer_id,
290 programmer_info,
291 )
293 # -- All done
294 return project_resources
297def get_fpga_arch_params(fpga_info: Dict) -> Tuple[str, Dict]:
298 """Extracts the arch specific params of an fpga, Returns a tuple
299 with the field name and the field value."""
300 arch = fpga_info["arch"]
301 field_name = arch + "-params"
302 field_value = fpga_info[field_name]
303 return (field_name, field_value)