Coverage for apio / utils / resource_util.py: 64%

89 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-25 02:31 +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": {"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} 

107 

108 

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} 

117 

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} 

133 

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} 

153 

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} 

188 

189 

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) 

197 

198 

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) 

206 

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) 

214 

215 

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) 

225 

226 

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) 

234 

235 

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) 

243 

244 

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) 

252 

253 

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) 

260 

261 # TODO: Add here additional check. 

262 

263 

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

270 

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) 

276 

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) 

286 

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) 

296 

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 ) 

306 

307 # -- All done 

308 return project_resources 

309 

310 

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)