Coverage for apio/utils/resource_util.py: 61%
94 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +0000
1"""Utilities related to the Apio resource files."""
3import sys
4from typing import Any, Dict
5from dataclasses import dataclass
6from jsonschema import validate
7from jsonschema.exceptions import ValidationError
8from apio.common.apio_console import cerror
11@dataclass(frozen=True)
12class ProjectResources:
13 """Contains the resources of the current project."""
15 board_id: str
16 board_info: Dict[str, Any]
17 fpga_id: str
18 fpga_info: Dict[str, Any]
19 programmer_id: str
20 programmer_info: Dict[str, Any]
23# -- JSON schema for validating board definitions in boards.jsonc.
24# -- The field 'description' is for information only.
25BOARD_SCHEMA = schema = {
26 "$schema": "http://json-schema.org/draft-07/schema#",
27 "type": "object",
28 "required": ["description", "fpga-id", "programmer"],
29 "properties": {
30 "description": {"type": "string"},
31 "legacy-name": {"type": "string"},
32 "fpga-id": {"type": "string"},
33 "programmer": {
34 "type": "object",
35 "required": ["id"],
36 "properties": {
37 "id": {"type": "string"},
38 "extra-args": {"type": "string"},
39 },
40 "additionalProperties": False,
41 },
42 "usb": {
43 "type": "object",
44 "required": ["vid", "pid"],
45 "properties": {
46 "vid": {"type": "string", "pattern": "^[0-9a-f]{4}$"},
47 "pid": {"type": "string", "pattern": "^[0-9a-f]{4}$"},
48 "product-regex": {"type": "string", "pattern": "^.*$"},
49 },
50 "additionalProperties": False,
51 },
52 "tinyprog": {
53 "type": "object",
54 "required": ["name-regex"],
55 "properties": {
56 "name-regex": {"type": "string", "pattern": "^.*$"},
57 },
58 "additionalProperties": False,
59 },
60 },
61 "additionalProperties": False,
62}
64# -- JSON schema for validating fpga definitions in fpga.jsonc.
65# -- The fields 'part-num' and 'size' are for information only.
66FPGA_SCHEMA = schema = {
67 "$schema": "http://json-schema.org/draft-07/schema#",
68 "type": "object",
69 "required": ["part-num", "arch", "size", "type"],
70 "properties": {
71 "part-num": {"type": "string"},
72 "arch": {"type": "string", "enum": ["ice40", "ecp5", "gowin"]},
73 "size": {"type": "string"},
74 "type": {"type": "string"},
75 "pack": {"type": "string"},
76 "speed": {"type": "string"},
77 },
78 "additionalProperties": False,
79}
81# -- JSON schema for validating programmer definitions in programmers.jsonc.
82PROGRAMMER_SCHEMA = {
83 "$schema": "http://json-schema.org/draft-07/schema#",
84 "type": "object",
85 "required": ["command", "args"],
86 "properties": {"command": {"type": "string"}, "args": {"type": "string"}},
87 "additionalProperties": False,
88}
90# -- JSON schema for validating config.jsonc.
91CONFIG_SCHEMA = {
92 "type": "object",
93 "required": [
94 "remote-config-ttl-days",
95 "remote-config-retry-minutes",
96 "remote-config-url",
97 ],
98 "properties": {
99 "remote-config-ttl-days": {"type": "integer", "minimum": 1},
100 "remote-config-retry-minutes": {"type": "integer", "minimum": 0},
101 "remote-config-url": {"type": "string"},
102 },
103 "additionalProperties": False,
104}
106# -- JSON schema for validating platforms.jsonc.
107PLATFORMS_SCHEMA = {
108 "type": "object",
109 "patternProperties": {
110 "^[a-z]+(-[a-z0-9]+)+$": { # matches keys like "darwin-arm64"
111 "type": "object",
112 "required": ["type", "variant"],
113 "properties": {
114 "type": {
115 "type": "string",
116 "enum": ["Mac OSX", "Linux", "Windows"],
117 },
118 "variant": {"type": "string"},
119 },
120 "additionalProperties": False,
121 }
122 },
123 "additionalProperties": False,
124}
126# -- JSON schema for validating packages.jsonc.
127PACKAGES_SCHEMA = {
128 "type": "object",
129 "patternProperties": {
130 "^[a-z0-9_-]+$": { # package names like "oss-cad-suite"
131 "type": "object",
132 "required": ["description", "env"],
133 "properties": {
134 "description": {"type": "string"},
135 "restricted-to-platforms": {
136 "type": "array",
137 "items": {"type": "string"},
138 },
139 "env": {
140 "type": "object",
141 "properties": {
142 "path": {"type": "array", "items": {"type": "string"}},
143 "vars": {
144 "type": "object",
145 "additionalProperties": {"type": "string"},
146 },
147 },
148 "additionalProperties": False,
149 },
150 },
151 "additionalProperties": False,
152 }
153 },
154 "additionalProperties": False,
155}
158def _validate_board_info(board_id: str, board_info: dict) -> None:
159 """Check the given board info and raise a fatal error on any error."""
160 try:
161 validate(instance=board_info, schema=BOARD_SCHEMA)
162 except ValidationError as e:
163 cerror(f"Invalid board definition [{board_id}]: {e.message}")
164 sys.exit(1)
167def _validate_fpga_info(fpga_id: str, fpga_info: dict) -> None:
168 """Check the given fpga info and raise a fatal error on any error."""
169 try:
170 validate(instance=fpga_info, schema=FPGA_SCHEMA)
171 except ValidationError as e:
172 cerror(f"Invalid fpga definitions [{fpga_id}]: {e.message}")
173 sys.exit(1)
175 # -- Architecture based validation. See scons.py for the architecture
176 # -- dependent use of the fields.
177 arch = fpga_info["arch"]
178 match arch:
179 # -- Special validation for ice40.
180 case "ice40":
181 if "pack" not in fpga_info: 181 ↛ 182line 181 didn't jump to line 182 because the condition on line 181 was never true
182 cerror(f"Field 'pack' is missing in ice40 fpga '{fpga_id}'")
183 sys.exit(1)
185 # -- Special validation for ecp5
186 case "ecp5":
187 if "pack" not in fpga_info: 187 ↛ 188line 187 didn't jump to line 188 because the condition on line 187 was never true
188 cerror(f"Field 'pack' is missing in ecp5 fpga '{fpga_id}'")
189 sys.exit(1)
190 if "speed" not in fpga_info: 190 ↛ 191line 190 didn't jump to line 191 because the condition on line 190 was never true
191 cerror(f"Field 'pack' is missing in ecp5 fpga '{fpga_id}'")
192 sys.exit(1)
194 # -- Special validation for gowin.
195 case "gowin": 195 ↛ 200line 195 didn't jump to line 200 because the pattern on line 195 always matched
196 pass
198 # -- Unknown arch. Should not happen since the schema validates the
199 # -- arch field.
200 case _:
201 raise ValidationError(f"Unknown arch '{arch}' in fpga '{fpga_id}'")
204def _validate_programmer_info(
205 programmer_id: str, programmer_info: dict
206) -> None:
207 """Check the given programmer info and raise a fatal error on any error."""
208 try:
209 validate(instance=programmer_info, schema=PROGRAMMER_SCHEMA)
210 except ValidationError as e:
211 cerror(f"Invalid programmer definition [{programmer_id}]: {e.message}")
212 sys.exit(1)
215def validate_config(config: dict) -> None:
216 """Check the config resource from config.jsonc."""
217 try:
218 validate(instance=config, schema=CONFIG_SCHEMA)
219 except ValidationError as e:
220 cerror(f"Invalid config: {e.message}")
221 sys.exit(1)
224def validate_platforms(platforms: dict) -> None:
225 """Check the platforms resource from platforms.jsonc."""
226 try:
227 validate(instance=platforms, schema=PLATFORMS_SCHEMA)
228 except ValidationError as e:
229 cerror(f"Invalid platforms resource: {e.message}")
230 sys.exit(1)
233def validate_packages(packages: dict) -> None:
234 """Check the packages resource from platforms.jsonc."""
235 try:
236 validate(instance=packages, schema=PACKAGES_SCHEMA)
237 except ValidationError as e:
238 cerror(f"Invalid packages resource: {e.message}")
239 sys.exit(1)
242def validate_project_resources(res: ProjectResources) -> None:
243 """Check the resources of the current project. Exit with an error
244 message on any error."""
245 _validate_board_info(res.board_id, res.board_info)
246 _validate_fpga_info(res.fpga_id, res.fpga_info)
247 _validate_programmer_info(res.programmer_id, res.programmer_info)
249 # TODO: Add here additional check.
252def collect_project_resources(
253 board_id: str, boards: dict, fpgas: dict, programmers: dict
254) -> ProjectResources:
255 """Collect and validate the resources used by a project. Since the
256 resources may be custom resources defined by the user, we need to
257 have a user friendly error handling and reporting."""
259 # -- Get the info.
260 board_info = boards.get(board_id, None)
261 if board_info is None: 261 ↛ 262line 261 didn't jump to line 262 because the condition on line 261 was never true
262 cerror(f"Unknown board id '{board_id}'.")
263 sys.exit(1)
265 # -- Get fpga id and info.
266 fpga_id = board_info.get("fpga-id", None)
267 if fpga_id is None: 267 ↛ 268line 267 didn't jump to line 268 because the condition on line 267 was never true
268 cerror(f"Board '{board_id}' has no 'fpga-id' field.")
269 sys.exit(1)
270 fpga_info = fpgas.get(fpga_id, None)
271 if fpga_info is None: 271 ↛ 272line 271 didn't jump to line 272 because the condition on line 271 was never true
272 cerror(f"Unknown fpga id '{fpga_id}'.")
273 sys.exit(1)
275 # -- Get programmer id and info.
276 programmer_id = board_info.get("programmer", {}).get("id", None)
277 if programmer_id is None: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 cerror(f"Board '{board_id}' has no 'programmer.id'.")
279 sys.exit(1)
280 programmer_info = programmers.get(programmer_id, None)
281 if programmer_info is None: 281 ↛ 282line 281 didn't jump to line 282 because the condition on line 281 was never true
282 cerror(f"Unknown programmer id '{programmer_id}'.")
283 sys.exit(1)
285 # -- Create the project resources bundle.
286 project_resources = ProjectResources(
287 board_id,
288 board_info,
289 fpga_id,
290 fpga_info,
291 programmer_id,
292 programmer_info,
293 )
295 # -- All done
296 return project_resources