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

1"""Utilities related to the Apio resource files.""" 

2 

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 

10 

11 

12@dataclass(frozen=True) 

13class ProjectResources: 

14 """Contains the resources of the current project.""" 

15 

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] 

22 

23 

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} 

64 

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} 

121 

122 

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} 

131 

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} 

147 

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} 

167 

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} 

202 

203 

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) 

211 

212 

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) 

220 

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) 

228 

229 

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) 

239 

240 

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) 

248 

249 

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) 

257 

258 

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) 

266 

267 

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) 

274 

275 # TODO: Add here additional check. 

276 

277 

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.""" 

284 

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) 

290 

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) 

300 

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) 

310 

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 ) 

320 

321 # -- All done 

322 return project_resources 

323 

324 

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)