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

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 

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} 

183 

184 

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) 

192 

193 

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) 

201 

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) 

209 

210 

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) 

220 

221 

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) 

229 

230 

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) 

238 

239 

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) 

246 

247 # TODO: Add here additional check. 

248 

249 

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

256 

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) 

262 

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) 

272 

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) 

282 

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 ) 

292 

293 # -- All done 

294 return project_resources 

295 

296 

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)