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

177 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-24 03:51 +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.scons.plugin_xilinx import PluginXilinx 

22from apio.common.proto.apio_pb2 import ( 

23 SimParams, 

24 SconsParams, 

25 ICE40, 

26 ECP5, 

27 GOWIN, 

28 XILINX, 

29) 

30from apio.common import apio_console 

31from apio.scons.apio_env import ApioEnv 

32from apio.scons.plugin_base import PluginBase 

33from apio.common import rich_lib_windows 

34from apio.scons.plugin_util import ( 

35 get_apio_sim_testbench_info, 

36 get_apio_test_testbenches_infos, 

37 gtkwave_target, 

38 report_action, 

39 get_programmer_cmd, 

40 TestbenchInfo, 

41) 

42from apio.common.apio_console import cerror, cout 

43 

44# -- Scons builders ids. 

45SYNTH_BUILDER = "SYNTH_BUILDER" 

46PNR_BUILDER = "PNR_BUILDER" 

47BITSTREAM_PRE_BUILDER = "BITSTREAM_PRE_BUILDER" 

48BITSTREAM_BUILDER = "BITSTREAM_BUILDER" 

49TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER" 

50TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER" 

51YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER" 

52GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER" 

53LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER" 

54LINT_BUILDER = "LINT_BUILDER" 

55 

56 

57class SconsHandler: 

58 """Base apio scons handler""" 

59 

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

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

62 self.apio_env = apio_env 

63 self.arch_plugin = arch_plugin 

64 

65 @staticmethod 

66 def start() -> None: 

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

68 execute an SconsHandler.""" 

69 

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

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

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

73 proto_text = f.read() 

74 

75 # -- Parse the text into SconsParams object. 

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

77 

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

79 # timestamp = ARGUMENTS["timestamp"] 

80 # assert params.timestamp == timestamp 

81 

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

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

84 rich_lib_windows.apply_workaround() 

85 

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

87 apio_console.configure( 

88 terminal_mode=params.environment.terminal_mode, 

89 theme_name=params.environment.theme_name, 

90 ) 

91 

92 # -- Create the apio environment. 

93 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params) 

94 

95 # -- Select the plugin. 

96 if params.arch == ICE40: 

97 plugin = PluginIce40(apio_env) 

98 elif params.arch == ECP5: 

99 plugin = PluginEcp5(apio_env) 

100 elif params.arch == GOWIN: 

101 plugin = PluginGowin(apio_env) 

102 elif params.arch == XILINX: 102 ↛ 105line 102 didn't jump to line 105 because the condition on line 102 was always true

103 plugin = PluginXilinx(apio_env) 

104 else: 

105 cout( 

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

107 ) 

108 sys.exit(1) 

109 

110 # -- Create the handler. 

111 scons_handler = SconsHandler(apio_env, plugin) 

112 

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

114 scons_handler.execute() 

115 

116 def _register_common_targets(self, synth_srcs): 

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

118 are used by a few top level targets. 

119 """ 

120 

121 apio_env = self.apio_env 

122 params = apio_env.params 

123 plugin = self.arch_plugin 

124 

125 # -- Sanity check 

126 assert apio_env.targeting_one_of("build", "upload", "report") 

127 

128 # -- Synth builder and target. 

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

130 

131 synth_target = apio_env.builder_target( 

132 builder_id=SYNTH_BUILDER, 

133 target=apio_env.target, 

134 sources=[synth_srcs], 

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

136 ) 

137 

138 # -- Place-and-route builder and target 

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

140 

141 pnr_target = apio_env.builder_target( 

142 builder_id=PNR_BUILDER, 

143 target=apio_env.target, 

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

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

146 ) 

147 

148 # -- DEBUG 

149 # -- Special case for xilinx 

150 if apio_env.params.arch == XILINX: 

151 

152 # -- Access to plugin.pre_builder() 

153 # -- But using getattr pylance does not complain 

154 # -- Cannot access attribute "bitstream_pre_builder" 

155 # -- for class "PluginBase" (reportAttributeAccessIssue) 

156 pre_builder = getattr(plugin, "bitstream_pre_builder") 

157 

158 # -- The bitstream builder consist of two stages 

159 # -- First stage: pre_builder 

160 apio_env.builder(BITSTREAM_PRE_BUILDER, pre_builder()) 

161 

162 pre_builder_target = apio_env.builder_target( 

163 builder_id=BITSTREAM_PRE_BUILDER, 

164 target=apio_env.target, 

165 sources=pnr_target, 

166 ) 

167 

168 # -- Second stage: builder 

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

170 

171 apio_env.builder_target( 

172 builder_id=BITSTREAM_BUILDER, 

173 target=apio_env.target, 

174 sources=pre_builder_target, 

175 ) 

176 

177 else: 

178 

179 # -- Bitstream builder builder and target 

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

181 

182 apio_env.builder_target( 

183 builder_id=BITSTREAM_BUILDER, 

184 target=apio_env.target, 

185 sources=pnr_target, 

186 ) 

187 

188 def _register_apio_build_target(self, synth_srcs): 

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

190 apio_env = self.apio_env 

191 params = apio_env.params 

192 plugin = self.arch_plugin 

193 

194 # -- Sanity check 

195 assert apio_env.targeting_one_of("build") 

196 

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

198 self._register_common_targets(synth_srcs) 

199 

200 # -- Determine target file. Normally it's the bitstream file but 

201 # -- if building with nextpnr --gui flag we skip the packing step 

202 # -- and stop after the nextpnr step. 

203 if params.nextpnr_gui: 203 ↛ 205line 203 didn't jump to line 205 because the condition on line 203 was never true

204 # -- Target is the pnr output file. 

205 target_file = ( 

206 apio_env.target + plugin.plugin_info().pnr_file_suffix 

207 ) 

208 else: 

209 # -- Target is the packager's output bitstream file. 

210 target_file = ( 

211 apio_env.target + plugin.plugin_info().bitstream_file_suffix 

212 ) 

213 

214 # -- Top level "build" target. 

215 apio_env.alias( 

216 "build", 

217 source=target_file, 

218 always_build=( 

219 params.verbosity.all 

220 or params.verbosity.synth 

221 or params.verbosity.pnr 

222 ), 

223 ) 

224 

225 def _register_apio_upload_target(self, synth_srcs): 

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

227 generated by the bitstream generator.""" 

228 

229 apio_env = self.apio_env 

230 plugin_info = self.arch_plugin.plugin_info() 

231 

232 # -- Sanity check 

233 assert apio_env.targeting_one_of("upload") 

234 

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

236 self._register_common_targets(synth_srcs) 

237 

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

239 apio_env.alias( 

240 "upload", 

241 source=apio_env.target + plugin_info.bitstream_file_suffix, 

242 action=get_programmer_cmd(apio_env), 

243 always_build=True, 

244 ) 

245 

246 def _register_apio_report_target(self, synth_srcs): 

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

248 PNR generated .pnr file.""" 

249 apio_env = self.apio_env 

250 params = apio_env.params 

251 plugin_info = self.arch_plugin.plugin_info() 

252 

253 # -- Sanity check 

254 assert apio_env.targeting_one_of("report") 

255 

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

257 self._register_common_targets(synth_srcs) 

258 

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

260 apio_env.alias( 

261 "report", 

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

263 action=report_action( 

264 plugin_info.clk_name_index, params.verbosity.pnr 

265 ), 

266 always_build=True, 

267 ) 

268 

269 def _register_apio_graph_target( 

270 self, 

271 synth_srcs, 

272 ): 

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

274 yosys and renders it using graphviz.""" 

275 apio_env = self.apio_env 

276 params = apio_env.params 

277 plugin = self.arch_plugin 

278 

279 # -- Sanity check 

280 assert apio_env.targeting_one_of("graph") 

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

282 

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

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

285 

286 dot_target = apio_env.builder_target( 

287 builder_id=YOSYS_DOT_BUILDER, 

288 target=apio_env.graph_target, 

289 sources=synth_srcs, 

290 always_build=True, 

291 ) 

292 

293 # -- Create the rendering builder and target. 

294 apio_env.builder( 

295 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder() 

296 ) 

297 graphviz_target = apio_env.builder_target( 

298 builder_id=GRAPHVIZ_RENDERER_BUILDER, 

299 target=apio_env.graph_target, 

300 sources=dot_target, 

301 always_build=True, 

302 ) 

303 

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

305 apio_env.alias( 

306 "graph", 

307 source=graphviz_target, 

308 always_build=True, 

309 ) 

310 

311 def _register_apio_lint_target(self, synth_srcs, test_srcs): 

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

313 and runs the linter.""" 

314 

315 apio_env = self.apio_env 

316 params = apio_env.params 

317 plugin = self.arch_plugin 

318 

319 # -- Sanity check 

320 assert apio_env.targeting_one_of("lint") 

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

322 

323 # -- Get lint params proto. 

324 lint_params = params.target.lint 

325 

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

327 extra_dependencies = [] 

328 

329 lint_whole_project = not lint_params.file_names 

330 using_vlt = lint_whole_project and (not lint_params.novlt) 

331 if using_vlt: 

332 # -- The auto generated verilator config file with supression 

333 # -- of some librariy warnings is enable. 

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

335 

336 lint_config_target = apio_env.builder_target( 

337 builder_id=LINT_CONFIG_BUILDER, 

338 target=apio_env.target, 

339 sources=[], 

340 ) 

341 extra_dependencies.append(lint_config_target) 

342 

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

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

345 

346 # -- Determine the files that will be linted. If specific files were 

347 # -- not specified on the command line, we take all the source and 

348 # -- testbench files in the project. 

349 if lint_params.file_names: 

350 files_to_lint = [ 

351 apio_env.scons_env.File(f) for f in lint_params.file_names 

352 ] 

353 else: 

354 files_to_lint = synth_srcs + test_srcs 

355 

356 lint_out_target = apio_env.builder_target( 

357 builder_id=LINT_BUILDER, 

358 target=apio_env.target, 

359 sources=files_to_lint, 

360 extra_dependencies=extra_dependencies, 

361 ) 

362 

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

364 apio_env.alias( 

365 "lint", 

366 source=lint_out_target, 

367 always_build=True, 

368 ) 

369 

370 def _register_apio_sim_target(self, synth_srcs, test_srcs): 

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

372 simulation of a testbench.""" 

373 

374 apio_env = self.apio_env 

375 params = apio_env.params 

376 plugin = self.arch_plugin 

377 

378 # -- Sanity check 

379 assert apio_env.targeting_one_of("sim") 

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

381 

382 # -- Get values. 

383 sim_params: SimParams = params.target.sim 

384 

385 # -- Collect information for sim. 

386 testbench_info: TestbenchInfo = get_apio_sim_testbench_info( 

387 apio_env, 

388 sim_params, 

389 synth_srcs, 

390 test_srcs, 

391 ) 

392 

393 # -- Compilation builder and target 

394 

395 apio_env.builder( 

396 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

397 ) 

398 

399 sim_out_target = apio_env.builder_target( 

400 builder_id=TESTBENCH_COMPILE_BUILDER, 

401 target=testbench_info.build_testbench_name, 

402 sources=testbench_info.srcs, 

403 always_build=sim_params.force_sim, 

404 ) 

405 

406 # -- Simulation builder and target.. 

407 

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

409 

410 sim_vcd_target = apio_env.builder_target( 

411 builder_id=TESTBENCH_RUN_BUILDER, 

412 target=testbench_info.build_testbench_name, 

413 sources=[sim_out_target], 

414 always_build=sim_params.force_sim, 

415 ) 

416 

417 # -- Get the gtkwave extra options (with the correct type) 

418 # -- for avoiding pylance warnings 

419 gtkwave_extra_options = params.apio_env_params.gtkwave_extra_options 

420 gtkwave_extra_options = [str(x) for x in gtkwave_extra_options] 

421 

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

423 gtkwave_target( 

424 apio_env, 

425 "sim", 

426 sim_vcd_target, 

427 testbench_info, 

428 sim_params, 

429 gtkwave_extra_options, 

430 ) 

431 

432 def _register_apio_test_target(self, synth_srcs, test_srcs): 

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

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

435 

436 apio_env = self.apio_env 

437 params = apio_env.params 

438 plugin = self.arch_plugin 

439 

440 # -- Sanity check 

441 assert apio_env.targeting_one_of("test") 

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

443 

444 # -- Collect the test related values. 

445 test_params = params.target.test 

446 testbenches_infos = get_apio_test_testbenches_infos( 

447 apio_env, 

448 test_params, 

449 synth_srcs, 

450 test_srcs, 

451 ) 

452 

453 # -- Create compilation and simulation targets. 

454 apio_env.builder( 

455 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

456 ) 

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

458 

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

460 tests_targets = [] 

461 for testbench_info in testbenches_infos: 

462 

463 # -- Create the compilation target. 

464 test_out_target = apio_env.builder_target( 

465 builder_id=TESTBENCH_COMPILE_BUILDER, 

466 target=testbench_info.build_testbench_name, 

467 sources=testbench_info.srcs, 

468 always_build=True, 

469 ) 

470 

471 # -- Create the simulation target. 

472 test_vcd_target = apio_env.builder_target( 

473 builder_id=TESTBENCH_RUN_BUILDER, 

474 target=testbench_info.build_testbench_name, 

475 sources=[test_out_target], 

476 always_build=True, 

477 ) 

478 

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

480 tests_targets.append(test_vcd_target) 

481 

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

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

484 

485 def execute(self): 

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

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

488 return.""" 

489 

490 apio_env = self.apio_env 

491 

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

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

494 synth_srcs, test_srcs = get_project_source_files() 

495 

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

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

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

499 

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

501 targets = apio_env.command_line_targets 

502 assert len(targets) == 1, targets 

503 target = targets[0] 

504 

505 # -- Dispatch by target. 

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

507 # -- python 3.9. 

508 if target == "build": 

509 self._register_apio_build_target(synth_srcs) 

510 

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

512 self._register_apio_upload_target(synth_srcs) 

513 

514 elif target == "report": 

515 self._register_apio_report_target(synth_srcs) 

516 

517 elif target == "graph": 

518 self._register_apio_graph_target(synth_srcs) 

519 

520 elif target == "sim": 

521 self._register_apio_sim_target(synth_srcs, test_srcs) 

522 

523 elif target == "test": 

524 self._register_apio_test_target(synth_srcs, test_srcs) 

525 

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

527 self._register_apio_lint_target(synth_srcs, test_srcs) 

528 

529 else: 

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

531 sys.exit(1) 

532 

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

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