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

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

2 

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 

9 

10 

11@dataclass(frozen=True) 

12class ProjectResources: 

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

14 

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] 

21 

22 

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} 

63 

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} 

80 

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} 

89 

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} 

105 

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} 

125 

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} 

156 

157 

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) 

165 

166 

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) 

174 

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) 

184 

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) 

193 

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 

197 

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}'") 

202 

203 

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) 

213 

214 

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) 

222 

223 

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) 

231 

232 

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) 

240 

241 

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) 

248 

249 # TODO: Add here additional check. 

250 

251 

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

258 

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) 

264 

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) 

274 

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) 

284 

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 ) 

294 

295 # -- All done 

296 return project_resources