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

175 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-23 03: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 

13from pathlib import Path 

14from SCons.Script import ARGUMENTS, COMMAND_LINE_TARGETS 

15from google.protobuf import text_format 

16from apio.common.common_util import get_project_source_files 

17from apio.scons.plugin_ice40 import PluginIce40 

18from apio.scons.plugin_ecp5 import PluginEcp5 

19from apio.scons.plugin_gowin import PluginGowin 

20from apio.scons.plugin_xilinx import PluginXilinx 

21from apio.common.proto.apio_common_pb2 import ApioArch 

22from apio.common.proto.apio_scons_pb2 import SimParams, SconsParams 

23from apio.common import apio_console, proto_util 

24from apio.scons.apio_env import ApioEnv 

25from apio.scons.plugin_base import PluginBase 

26from apio.common import rich_lib_windows 

27from apio.scons.plugin_util import ( 

28 get_apio_sim_testbench_info, 

29 get_apio_test_testbenches_infos, 

30 gtkwave_target, 

31 report_action, 

32 get_programmer_cmd, 

33 TestbenchInfo, 

34) 

35from apio.common.apio_console import fatal_error 

36 

37# -- Scons builders ids. 

38SYNTH_BUILDER = "SYNTH_BUILDER" 

39PNR_BUILDER = "PNR_BUILDER" 

40BITSTREAM_BUILDER = "BITSTREAM_BUILDER" 

41TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER" 

42TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER" 

43YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER" 

44GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER" 

45LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER" 

46LINT_BUILDER = "LINT_BUILDER" 

47 

48 

49class SconsHandler: 

50 """Base apio scons handler""" 

51 

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

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

54 self.apio_env = apio_env 

55 self.arch_plugin = arch_plugin 

56 

57 @staticmethod 

58 def start() -> None: 

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

60 execute an SconsHandler.""" 

61 

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

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

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

65 proto_text = f.read() 

66 

67 # -- Parse the text into SconsParams object. 

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

69 proto_util.check_is_initialized(params, "Failed to parse scons params") 

70 

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

72 # -- This verified that we got the intended file instance. 

73 proto_util.check_is_required(params, "timestamp") 

74 timestamp = ARGUMENTS["timestamp"] 

75 assert params.timestamp == timestamp 

76 

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

78 proto_util.check_is_required(params, "environment.is_windows") 

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 proto_util.check_is_required( 

84 params, "environment.terminal_mode", "environment.theme_name" 

85 ) 

86 apio_console.configure( 

87 terminal_mode=params.environment.terminal_mode, 

88 theme_name=params.environment.theme_name, 

89 ) 

90 

91 # -- Create the apio environment. 

92 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params) 

93 

94 # -- Select the plugin. 

95 proto_util.check_is_required(params, "arch") 

96 plugin: PluginBase 

97 match params.arch: 

98 case ApioArch.ice40: 

99 plugin = PluginIce40(apio_env) 

100 case ApioArch.ecp5: 

101 plugin = PluginEcp5(apio_env) 

102 case ApioArch.gowin: 

103 plugin = PluginGowin(apio_env) 

104 case ApioArch.xilinx: 104 ↛ 106line 104 didn't jump to line 106 because the pattern on line 104 always matched

105 plugin = PluginXilinx(apio_env) 

106 case _: 

107 fatal_error( 

108 "Apio SConstruct dispatch error: unknown " 

109 + f"arch [{ApioArch.Name(params.arch)}]" 

110 ) 

111 

112 # -- Create the handler. 

113 scons_handler = SconsHandler(apio_env, plugin) 

114 

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

116 scons_handler.execute() 

117 

118 def _register_common_targets(self, synth_srcs): 

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

120 are used by a few top level targets. 

121 """ 

122 

123 apio_env = self.apio_env 

124 params = apio_env.params 

125 plugin = self.arch_plugin 

126 

127 # -- Sanity check 

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

129 

130 # -- Synth builder and target. 

131 apio_env.add_builder(SYNTH_BUILDER, plugin.make_synth_builder()) 

132 

133 # -- Yosys synthesis builder and target. 

134 # -- Verbosity is an optional field so we rely on the protocol buffer 

135 # -- implicit default value false for non populated boolean fields. 

136 proto_util.check_not_required(params, "verbosity") 

137 synth_target = apio_env.add_builder_target( 

138 builder_id=SYNTH_BUILDER, 

139 target=apio_env.target, 

140 sources=[synth_srcs], 

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

142 ) 

143 

144 # -- Place-and-route builder and target 

145 apio_env.add_builder(PNR_BUILDER, plugin.make_pnr_builder()) 

146 

147 proto_util.check_not_required(params, "verbosity") 

148 pnr_target = apio_env.add_builder_target( 

149 builder_id=PNR_BUILDER, 

150 target=apio_env.target, 

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

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

153 ) 

154 

155 # -- Bitstream builder builder and target 

156 apio_env.add_builder( 

157 BITSTREAM_BUILDER, plugin.make_bitstream_builder() 

158 ) 

159 

160 apio_env.add_builder_target( 

161 builder_id=BITSTREAM_BUILDER, 

162 target=apio_env.target, 

163 sources=pnr_target, 

164 ) 

165 

166 def _register_apio_build_target(self, synth_srcs): 

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

168 apio_env = self.apio_env 

169 params = apio_env.params 

170 plugin = self.arch_plugin 

171 

172 # -- Sanity check 

173 assert apio_env.targeting_one_of("build") 

174 

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

176 self._register_common_targets(synth_srcs) 

177 

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

179 target_file = ( 

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

181 ) 

182 

183 # -- Top level "build" target. 

184 apio_env.add_alias( 

185 "build", 

186 source=target_file, 

187 always_build=( 

188 params.verbosity.all 

189 or params.verbosity.synth 

190 or params.verbosity.pnr 

191 ), 

192 ) 

193 

194 def _register_apio_upload_target(self, synth_srcs): 

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

196 generated by the bitstream generator.""" 

197 

198 apio_env = self.apio_env 

199 plugin_info = self.arch_plugin.plugin_info() 

200 

201 # -- Sanity check 

202 assert apio_env.targeting_one_of("upload") 

203 

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

205 self._register_common_targets(synth_srcs) 

206 

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

208 apio_env.add_alias( 

209 "upload", 

210 source=apio_env.target + plugin_info.bitstream_file_suffix, 

211 action=get_programmer_cmd(apio_env), 

212 always_build=True, 

213 ) 

214 

215 def _register_apio_report_target(self, synth_srcs): 

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

217 PNR generated .pnr file.""" 

218 apio_env = self.apio_env 

219 params = apio_env.params 

220 

221 # -- Sanity check 

222 assert apio_env.targeting_one_of("report") 

223 

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

225 self._register_common_targets(synth_srcs) 

226 

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

228 apio_env.add_alias( 

229 "report", 

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

231 action=report_action(params.verbosity.pnr), 

232 always_build=True, 

233 ) 

234 

235 def _register_apio_graph_target( 

236 self, 

237 synth_srcs, 

238 ): 

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

240 yosys and renders it using graphviz.""" 

241 apio_env = self.apio_env 

242 params = apio_env.params 

243 plugin = self.arch_plugin 

244 

245 # -- Sanity check 

246 assert apio_env.targeting_one_of("graph") 

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

248 

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

250 apio_env.add_builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder()) 

251 

252 dot_target = apio_env.add_builder_target( 

253 builder_id=YOSYS_DOT_BUILDER, 

254 target=apio_env.graph_target, 

255 sources=synth_srcs, 

256 always_build=True, 

257 ) 

258 

259 # -- Create the rendering builder and target. 

260 apio_env.add_builder( 

261 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder() 

262 ) 

263 graphviz_target = apio_env.add_builder_target( 

264 builder_id=GRAPHVIZ_RENDERER_BUILDER, 

265 target=apio_env.graph_target, 

266 sources=dot_target, 

267 always_build=True, 

268 ) 

269 

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

271 apio_env.add_alias( 

272 "graph", 

273 source=graphviz_target, 

274 always_build=True, 

275 ) 

276 

277 def _register_apio_lint_target(self, synth_srcs, test_srcs): 

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

279 and runs the linter.""" 

280 

281 apio_env = self.apio_env 

282 params = apio_env.params 

283 plugin = self.arch_plugin 

284 

285 # -- Sanity check 

286 assert apio_env.targeting_one_of("lint") 

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

288 

289 # -- Get lint params proto. 

290 lint_params = params.target.lint 

291 

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

293 extra_dependencies = [] 

294 

295 lint_whole_project = not lint_params.file_names 

296 using_vlt = lint_whole_project and (not lint_params.novlt) 

297 if using_vlt: 

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

299 # -- of some librariy warnings is enable. 

300 apio_env.add_builder( 

301 LINT_CONFIG_BUILDER, plugin.make_lint_config_builder() 

302 ) 

303 

304 lint_config_target = apio_env.add_builder_target( 

305 builder_id=LINT_CONFIG_BUILDER, 

306 target=apio_env.target, 

307 sources=[], 

308 ) 

309 extra_dependencies.append(lint_config_target) 

310 

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

312 apio_env.add_builder(LINT_BUILDER, plugin.make_lint_builder()) 

313 

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

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

316 # -- testbench files in the project. 

317 if lint_params.file_names: 

318 files_to_lint = [ 

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

320 ] 

321 else: 

322 files_to_lint = synth_srcs + test_srcs 

323 

324 lint_out_target = apio_env.add_builder_target( 

325 builder_id=LINT_BUILDER, 

326 target=apio_env.target, 

327 sources=files_to_lint, 

328 extra_dependencies=extra_dependencies, 

329 ) 

330 

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

332 apio_env.add_alias( 

333 "lint", 

334 source=lint_out_target, 

335 always_build=True, 

336 ) 

337 

338 def _register_apio_sim_target(self, synth_srcs, test_srcs) -> None: 

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

340 simulation of a testbench.""" 

341 

342 apio_env = self.apio_env 

343 params = apio_env.params 

344 plugin = self.arch_plugin 

345 

346 # -- Sanity check 

347 assert apio_env.targeting_one_of("sim") 

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

349 

350 # -- Get values. 

351 sim_params: SimParams = params.target.sim 

352 

353 # -- Collect information for sim. 

354 testbench_info: TestbenchInfo = get_apio_sim_testbench_info( 

355 apio_env, 

356 sim_params, 

357 synth_srcs, 

358 test_srcs, 

359 ) 

360 

361 # -- Compilation builder and target 

362 

363 apio_env.add_builder( 

364 TESTBENCH_COMPILE_BUILDER, plugin.make_testbench_compile_builder() 

365 ) 

366 

367 sim_out_target = apio_env.add_builder_target( 

368 builder_id=TESTBENCH_COMPILE_BUILDER, 

369 target=testbench_info.build_testbench_name, 

370 sources=testbench_info.srcs, 

371 always_build=sim_params.force_sim, 

372 ) 

373 

374 # -- Simulation builder and target.. 

375 

376 apio_env.add_builder( 

377 TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder() 

378 ) 

379 

380 sim_vcd_target = apio_env.add_builder_target( 

381 builder_id=TESTBENCH_RUN_BUILDER, 

382 target=testbench_info.build_testbench_name, 

383 sources=[sim_out_target], 

384 always_build=sim_params.force_sim, 

385 ) 

386 

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

388 # -- for avoiding pylance warnings 

389 gtkwave_extra_options: list[str] = [ 

390 str(x) for x in params.apio_env_params.gtkwave_extra_options 

391 ] 

392 

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

394 gtkwave_target( 

395 apio_env, 

396 "sim", 

397 sim_vcd_target, 

398 testbench_info, 

399 sim_params, 

400 gtkwave_extra_options, 

401 ) 

402 

403 def _register_apio_test_target(self, synth_srcs, test_srcs): 

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

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

406 

407 apio_env = self.apio_env 

408 params = apio_env.params 

409 plugin = self.arch_plugin 

410 

411 # -- Sanity check 

412 assert apio_env.targeting_one_of("test") 

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

414 

415 # -- Collect the test related values. 

416 test_params = params.target.test 

417 testbenches_infos = get_apio_test_testbenches_infos( 

418 apio_env, 

419 test_params, 

420 synth_srcs, 

421 test_srcs, 

422 ) 

423 

424 # -- Create compilation and simulation targets. 

425 apio_env.add_builder( 

426 TESTBENCH_COMPILE_BUILDER, plugin.make_testbench_compile_builder() 

427 ) 

428 apio_env.add_builder( 

429 TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder() 

430 ) 

431 

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

433 tests_targets = [] 

434 for testbench_info in testbenches_infos: 

435 

436 # -- Create the compilation target. 

437 test_out_target = apio_env.add_builder_target( 

438 builder_id=TESTBENCH_COMPILE_BUILDER, 

439 target=testbench_info.build_testbench_name, 

440 sources=testbench_info.srcs, 

441 always_build=True, 

442 ) 

443 

444 # -- Create the simulation target. 

445 test_vcd_target = apio_env.add_builder_target( 

446 builder_id=TESTBENCH_RUN_BUILDER, 

447 target=testbench_info.build_testbench_name, 

448 sources=[test_out_target], 

449 always_build=True, 

450 ) 

451 

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

453 tests_targets.append(test_vcd_target) 

454 

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

456 apio_env.add_alias("test", source=tests_targets, always_build=True) 

457 

458 def execute(self): 

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

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

461 return.""" 

462 

463 apio_env = self.apio_env 

464 

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

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

467 synth_srcs, test_srcs = get_project_source_files() 

468 

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

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

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

472 

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

474 targets = apio_env.command_line_targets 

475 assert len(targets) == 1, targets 

476 target = targets[0] 

477 

478 # -- Dispatch by target. 

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

480 # -- python 3.9. 

481 if target == "build": 

482 self._register_apio_build_target(synth_srcs) 

483 

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

485 self._register_apio_upload_target(synth_srcs) 

486 

487 elif target == "report": 

488 self._register_apio_report_target(synth_srcs) 

489 

490 elif target == "graph": 

491 self._register_apio_graph_target(synth_srcs) 

492 

493 elif target == "sim": 

494 self._register_apio_sim_target(synth_srcs, test_srcs) 

495 

496 elif target == "test": 

497 self._register_apio_test_target(synth_srcs, test_srcs) 

498 

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

500 self._register_apio_lint_target(synth_srcs, test_srcs) 

501 

502 else: 

503 fatal_error(f"Unexpected scons target: {target}") 

504 

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

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