Coverage for apio / scons / scons_handler.py: 92%

156 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-24 01:53 +0000

1# -*- coding: utf-8 -*- 

2# -- This file is part of the Apio project 

3# -- (C) 2016-2018 FPGAwars 

4# -- Author Jesús Arroyo 

5# -- License GPLv2 

6# -- Derived from: 

7# ---- Platformio project 

8# ---- (C) 2014-2016 Ivan Kravets <me@ikravets.com> 

9# ---- License Apache v2 

10 

11"""Apio scons related utilities..""" 

12 

13import sys 

14from pathlib import Path 

15from SCons.Script import ARGUMENTS, COMMAND_LINE_TARGETS 

16from google.protobuf import text_format 

17from apio.common.common_util import get_project_source_files 

18from apio.scons.plugin_ice40 import PluginIce40 

19from apio.scons.plugin_ecp5 import PluginEcp5 

20from apio.scons.plugin_gowin import PluginGowin 

21from apio.common.proto.apio_pb2 import ( 

22 SimParams, 

23 SconsParams, 

24 ICE40, 

25 ECP5, 

26 GOWIN, 

27) 

28from apio.common import apio_console 

29from apio.scons.apio_env import ApioEnv 

30from apio.scons.plugin_base import PluginBase 

31from apio.common import rich_lib_windows 

32from apio.scons.plugin_util import ( 

33 get_sim_config, 

34 get_tests_configs, 

35 gtkwave_target, 

36 report_action, 

37 get_programmer_cmd, 

38) 

39from apio.common.apio_console import cerror, cout 

40 

41# -- Scons builders ids. 

42SYNTH_BUILDER = "SYNTH_BUILDER" 

43PNR_BUILDER = "PNR_BUILDER" 

44BITSTREAM_BUILDER = "BITSTREAM_BUILDER" 

45TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER" 

46TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER" 

47YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER" 

48GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER" 

49LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER" 

50LINT_BUILDER = "LINT_BUILDER" 

51 

52 

53class SconsHandler: 

54 """Base apio scons handler""" 

55 

56 def __init__(self, apio_env: ApioEnv, arch_plugin: PluginBase): 

57 """Do not call directly, use SconsHandler.start().""" 

58 self.apio_env = apio_env 

59 self.arch_plugin = arch_plugin 

60 

61 @staticmethod 

62 def start() -> None: 

63 """This static method is called from SConstruct to create and 

64 execute an SconsHandler.""" 

65 

66 # -- Read the text of the scons params file. 

67 params_path = Path(ARGUMENTS["params"]) 

68 with open(params_path, "r", encoding="utf8") as f: 

69 proto_text = f.read() 

70 

71 # -- Parse the text into SconsParams object. 

72 params: SconsParams = text_format.Parse(proto_text, SconsParams()) 

73 

74 # -- Compare the params timestamp to the timestamp in the command. 

75 timestamp = ARGUMENTS["timestamp"] 

76 assert params.timestamp == timestamp 

77 

78 # -- If running on windows, apply the lib library workaround 

79 if params.environment.is_windows: 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true

80 rich_lib_windows.apply_workaround() 

81 

82 # -- Set terminal mode and theme to match the apio process. 

83 apio_console.configure( 

84 terminal_mode=params.environment.terminal_mode, 

85 theme_name=params.environment.theme_name, 

86 ) 

87 

88 # -- Create the apio environment. 

89 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params) 

90 

91 # -- Select the plugin. 

92 if params.arch == ICE40: 

93 plugin = PluginIce40(apio_env) 

94 elif params.arch == ECP5: 

95 plugin = PluginEcp5(apio_env) 

96 elif params.arch == GOWIN: 96 ↛ 99line 96 didn't jump to line 99 because the condition on line 96 was always true

97 plugin = PluginGowin(apio_env) 

98 else: 

99 cout( 

100 f"Apio SConstruct dispatch error: unknown arch [{params.arch}]" 

101 ) 

102 sys.exit(1) 

103 

104 # -- Create the handler. 

105 scons_handler = SconsHandler(apio_env, plugin) 

106 

107 # -- Invoke the handler. This services the scons request. 

108 scons_handler.execute() 

109 

110 def _register_common_targets(self, synth_srcs): 

111 """Register the common synth, pnr, and bitstream operations which 

112 are used by a few top level targets. 

113 """ 

114 

115 apio_env = self.apio_env 

116 params = apio_env.params 

117 plugin = self.arch_plugin 

118 

119 # -- Sanity check 

120 assert apio_env.targeting("build", "upload", "report") 

121 

122 # -- Synth builder and target. 

123 apio_env.builder(SYNTH_BUILDER, plugin.synth_builder()) 

124 

125 synth_target = apio_env.builder_target( 

126 builder_id=SYNTH_BUILDER, 

127 target=apio_env.target, 

128 sources=[synth_srcs], 

129 always_build=(params.verbosity.all or params.verbosity.synth), 

130 ) 

131 

132 # -- Place-and-route builder and target 

133 apio_env.builder(PNR_BUILDER, plugin.pnr_builder()) 

134 

135 pnr_target = apio_env.builder_target( 

136 builder_id=PNR_BUILDER, 

137 target=apio_env.target, 

138 sources=[synth_target, self.arch_plugin.constrain_file()], 

139 always_build=(params.verbosity.all or params.verbosity.pnr), 

140 ) 

141 

142 # -- Bitstream builder builder and target 

143 apio_env.builder(BITSTREAM_BUILDER, plugin.bitstream_builder()) 

144 

145 apio_env.builder_target( 

146 builder_id=BITSTREAM_BUILDER, 

147 target=apio_env.target, 

148 sources=pnr_target, 

149 ) 

150 

151 def _register_build_target(self, synth_srcs): 

152 """Register the 'build' target which creates the binary bitstream.""" 

153 apio_env = self.apio_env 

154 params = apio_env.params 

155 plugin = self.arch_plugin 

156 

157 # -- Sanity check 

158 assert apio_env.targeting("build") 

159 

160 # -- Register the common targets for synth, pnr, and bitstream. 

161 self._register_common_targets(synth_srcs) 

162 

163 # -- Top level "build" target. 

164 apio_env.alias( 

165 "build", 

166 source=apio_env.target + plugin.plugin_info().bin_file_suffix, 

167 always_build=( 

168 params.verbosity.all 

169 or params.verbosity.synth 

170 or params.verbosity.pnr 

171 ), 

172 ) 

173 

174 def _register_upload_target(self, synth_srcs): 

175 """Register the 'upload' target which upload the binary file 

176 generated by the bitstream generator.""" 

177 

178 apio_env = self.apio_env 

179 plugin_info = self.arch_plugin.plugin_info() 

180 

181 # -- Sanity check 

182 assert apio_env.targeting("upload") 

183 

184 # -- Register the common targets for synth, pnr, and bitstream. 

185 self._register_common_targets(synth_srcs) 

186 

187 # -- Create the top level 'upload' target. 

188 apio_env.alias( 

189 "upload", 

190 source=apio_env.target + plugin_info.bin_file_suffix, 

191 action=get_programmer_cmd(apio_env), 

192 always_build=True, 

193 ) 

194 

195 def _register_report_target(self, synth_srcs): 

196 """Registers the 'report' target which a report file from the 

197 PNR generated .pnr file.""" 

198 apio_env = self.apio_env 

199 params = apio_env.params 

200 plugin_info = self.arch_plugin.plugin_info() 

201 

202 # -- Sanity check 

203 assert apio_env.targeting("report") 

204 

205 # -- Register the common targets for synth, pnr, and bitstream. 

206 self._register_common_targets(synth_srcs) 

207 

208 # -- Register the top level 'report' target. 

209 apio_env.alias( 

210 "report", 

211 source=apio_env.target + ".pnr", 

212 action=report_action( 

213 plugin_info.clk_name_index, params.verbosity.pnr 

214 ), 

215 always_build=True, 

216 ) 

217 

218 def _register_graph_target( 

219 self, 

220 synth_srcs, 

221 ): 

222 """Registers the 'graph' target which generates a .dot file using 

223 yosys and renders it using graphviz.""" 

224 apio_env = self.apio_env 

225 params = apio_env.params 

226 plugin = self.arch_plugin 

227 

228 # -- Sanity check 

229 assert apio_env.targeting("graph") 

230 assert params.target.HasField("graph") 

231 

232 # -- Create the .dot generation builder and target. 

233 apio_env.builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder()) 

234 

235 dot_target = apio_env.builder_target( 

236 builder_id=YOSYS_DOT_BUILDER, 

237 target=apio_env.graph_target, 

238 sources=synth_srcs, 

239 always_build=True, 

240 ) 

241 

242 # -- Create the rendering builder and target. 

243 apio_env.builder( 

244 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder() 

245 ) 

246 graphviz_target = apio_env.builder_target( 

247 builder_id=GRAPHVIZ_RENDERER_BUILDER, 

248 target=apio_env.graph_target, 

249 sources=dot_target, 

250 always_build=True, 

251 ) 

252 

253 # -- Create the top level "graph" target. 

254 apio_env.alias( 

255 "graph", 

256 source=graphviz_target, 

257 always_build=True, 

258 ) 

259 

260 def _register_lint_target(self, synth_srcs, test_srcs): 

261 """Registers the 'lint' target which creates a lint configuration file 

262 and runs the linter.""" 

263 

264 apio_env = self.apio_env 

265 params = apio_env.params 

266 plugin = self.arch_plugin 

267 

268 # -- Sanity check 

269 assert apio_env.targeting("lint") 

270 assert params.target.HasField("lint") 

271 

272 # -- Create the builder and target of the config file creation. 

273 apio_env.builder(LINT_CONFIG_BUILDER, plugin.lint_config_builder()) 

274 

275 lint_config_target = apio_env.builder_target( 

276 builder_id=LINT_CONFIG_BUILDER, 

277 target=apio_env.target, 

278 sources=[], 

279 ) 

280 

281 # -- Create the builder and target the lint operation. 

282 apio_env.builder(LINT_BUILDER, plugin.lint_builder()) 

283 

284 lint_out_target = apio_env.builder_target( 

285 builder_id=LINT_BUILDER, 

286 target=apio_env.target, 

287 sources=synth_srcs + test_srcs, 

288 extra_dependencies=[lint_config_target], 

289 ) 

290 

291 # -- Create the top level "lint" target. 

292 apio_env.alias( 

293 "lint", 

294 source=lint_out_target, 

295 always_build=True, 

296 ) 

297 

298 def _register_sim_target(self, synth_srcs, test_srcs): 

299 """Registers the 'sim' targets which compiles and runs the 

300 simulation of a testbench.""" 

301 

302 apio_env = self.apio_env 

303 params = apio_env.params 

304 plugin = self.arch_plugin 

305 

306 # -- Sanity check 

307 assert apio_env.targeting("sim") 

308 assert params.target.HasField("sim") 

309 

310 # -- Get values. 

311 sim_params: SimParams = params.target.sim 

312 testbench = sim_params.testbench # Optional. 

313 

314 # -- Collect information for sim. 

315 sim_config = get_sim_config(apio_env, testbench, synth_srcs, test_srcs) 

316 

317 # -- Compilation builder and target 

318 

319 apio_env.builder( 

320 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

321 ) 

322 

323 sim_out_target = apio_env.builder_target( 

324 builder_id=TESTBENCH_COMPILE_BUILDER, 

325 target=sim_config.build_testbench_name, 

326 sources=sim_config.srcs, 

327 always_build=sim_params.force_sim, 

328 ) 

329 

330 # -- Simulation builder and target.. 

331 

332 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder()) 

333 

334 sim_vcd_target = apio_env.builder_target( 

335 builder_id=TESTBENCH_RUN_BUILDER, 

336 target=sim_config.build_testbench_name, 

337 sources=[sim_out_target], 

338 always_build=sim_params, 

339 ) 

340 

341 # -- The top level "sim" target. 

342 gtkwave_target( 

343 apio_env, 

344 "sim", 

345 sim_vcd_target, 

346 sim_config, 

347 sim_params, 

348 ) 

349 

350 def _register_test_target(self, synth_srcs, test_srcs): 

351 """Registers 'test' target and its dependencies. Each testbench 

352 is tested independently with its own set of sub-targets.""" 

353 

354 apio_env = self.apio_env 

355 params = apio_env.params 

356 plugin = self.arch_plugin 

357 

358 # -- Sanity check 

359 assert apio_env.targeting("test") 

360 assert params.target.HasField("test") 

361 

362 # -- Collect the test related values. 

363 test_params = params.target.test 

364 tests_configs = get_tests_configs( 

365 apio_env, test_params.testbench, synth_srcs, test_srcs 

366 ) 

367 

368 # -- Create compilation and simulation targets. 

369 apio_env.builder( 

370 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

371 ) 

372 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder()) 

373 

374 # -- Create targets for each testbench we are testing. 

375 tests_targets = [] 

376 for test_config in tests_configs: 

377 

378 # -- Create the compilation target. 

379 test_out_target = apio_env.builder_target( 

380 builder_id=TESTBENCH_COMPILE_BUILDER, 

381 target=test_config.build_testbench_name, 

382 sources=test_config.srcs, 

383 always_build=True, 

384 ) 

385 

386 # -- Create the simulation target. 

387 test_vcd_target = apio_env.builder_target( 

388 builder_id=TESTBENCH_RUN_BUILDER, 

389 target=test_config.build_testbench_name, 

390 sources=[test_out_target], 

391 always_build=True, 

392 ) 

393 

394 # -- Append to the list of targets we need to execute. 

395 tests_targets.append(test_vcd_target) 

396 

397 # -- The top level 'test' target. 

398 apio_env.alias("test", source=tests_targets, always_build=True) 

399 

400 def execute(self): 

401 """The entry point of the scons handler. It registers the builders 

402 and targets for the selected command and scons executes in upon 

403 return.""" 

404 

405 apio_env = self.apio_env 

406 

407 # -- Collect the lists of the synthesizable files (e.g. "main.v") and a 

408 # -- testbench files (e.g. "main_tb.v") 

409 synth_srcs, test_srcs = get_project_source_files() 

410 

411 # -- Sanity check that we don't call the scons to do cleanup. This is 

412 # -- handled directly by the 'apio clean' command. 

413 assert not apio_env.scons_env.GetOption("clean") 

414 

415 # -- Get the target, we expect exactly one. 

416 targets = apio_env.command_line_targets 

417 assert len(targets) == 1, targets 

418 target = targets[0] 

419 

420 # -- Dispatch by target. 

421 # -- Not using python 'match' statement for compatibility with 

422 # -- python 3.9. 

423 if target == "build": 

424 self._register_build_target(synth_srcs) 

425 

426 elif target == "upload": 426 ↛ 427line 426 didn't jump to line 427 because the condition on line 426 was never true

427 self._register_upload_target(synth_srcs) 

428 

429 elif target == "report": 

430 self._register_report_target(synth_srcs) 

431 

432 elif target == "graph": 

433 self._register_graph_target(synth_srcs) 

434 

435 elif target == "sim": 

436 self._register_sim_target(synth_srcs, test_srcs) 

437 

438 elif target == "test": 

439 self._register_test_target(synth_srcs, test_srcs) 

440 

441 elif target == "lint": 441 ↛ 445line 441 didn't jump to line 445 because the condition on line 441 was always true

442 self._register_lint_target(synth_srcs, test_srcs) 

443 

444 else: 

445 cerror(f"Unexpected scons target: {target}") 

446 sys.exit(1) 

447 

448 # -- Note that so far we just registered builders and target. 

449 # -- The actual execution is done by scons once this method returns.