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

178 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-07 05:08 +0000

1# This file is part of the Apio project 

2# (C) 2016-present FPGAwars 

3# Project authors: 

4# * Jesús Arroyo (Jesus89) (2016-2019) 

5# * Juan Gonzalez (obijuan) (2019-2024) 

6# * The Apio Team (2024-present) 

7# License: GPLv2 

8 

9"""Apio scons related utilities..""" 

10 

11from pathlib import Path 

12from SCons.Script import ARGUMENTS, COMMAND_LINE_TARGETS 

13from google.protobuf import text_format 

14from apio.common.common_util import get_project_source_files 

15from apio.scons.plugin_ice40 import PluginIce40 

16from apio.scons.plugin_ecp5 import PluginEcp5 

17from apio.scons.plugin_gowin import PluginGowin 

18from apio.scons.plugin_xilinx import PluginXilinx 

19from apio.common.proto.stubs.apio_common_pb2 import ApioArch 

20from apio.common.proto.stubs.apio_scons_pb2 import SimParams, SconsParams 

21from apio.common import apio_console, proto_util, apio_platforms 

22from apio.scons.apio_env import ApioEnv 

23from apio.scons.plugin_base import PluginBase 

24from apio.common import rich_lib_windows 

25from apio.scons.plugin_util import ( 

26 get_apio_sim_testbench_info, 

27 get_apio_test_testbenches_infos, 

28 gtkwave_target, 

29 report_action, 

30 get_programmer_cmd, 

31 TestbenchInfo, 

32) 

33from apio.common.apio_console import fatal_error 

34 

35# -- Scons builders ids. 

36SYNTH_BUILDER = "SYNTH_BUILDER" 

37PNR_BUILDER = "PNR_BUILDER" 

38BITSTREAM_BUILDER = "BITSTREAM_BUILDER" 

39TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER" 

40TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER" 

41YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER" 

42GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER" 

43LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER" 

44LINT_BUILDER = "LINT_BUILDER" 

45 

46 

47class SconsHandler: 

48 """Base apio scons handler""" 

49 

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

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

52 self.apio_env = apio_env 

53 self.arch_plugin = arch_plugin 

54 

55 @staticmethod 

56 def start() -> None: 

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

58 execute an SconsHandler.""" 

59 

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

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

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

63 proto_text = f.read() 

64 

65 # -- Parse the text into SconsParams object. 

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

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

68 

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

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

71 proto_util.check_is_required(params, "timestamp") 

72 timestamp = ARGUMENTS["timestamp"] 

73 assert params.timestamp == timestamp 

74 

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

76 proto_util.check_is_required(params, "environment.platform_id") 

77 apio_platform = apio_platforms.get_apio_platforms()[ 

78 params.environment.platform_id 

79 ] 

80 if apio_platform.is_windows: 80 ↛ 81line 80 didn't jump to line 81 because the condition on line 80 was never true

81 rich_lib_windows.apply_workaround() 

82 

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

84 proto_util.check_is_required( 

85 params, "environment.terminal_mode", "environment.theme_name" 

86 ) 

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 # -- Sanity check that we are consistent on the ApioPlatform selection. 

96 assert apio_env.apio_platform is apio_platform 

97 

98 # -- Select the plugin. 

99 proto_util.check_is_required(params, "fpga_definition.arch") 

100 arch = params.fpga_definition.arch 

101 plugin: PluginBase 

102 match arch: 

103 case ApioArch.ice40: 

104 plugin = PluginIce40(apio_env) 

105 case ApioArch.ecp5: 

106 plugin = PluginEcp5(apio_env) 

107 case ApioArch.gowin: 

108 plugin = PluginGowin(apio_env) 

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

110 plugin = PluginXilinx(apio_env) 

111 case _: 

112 fatal_error( 

113 "Apio SConstruct dispatch error: unknown " 

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

115 ) 

116 

117 # -- Create the handler. 

118 scons_handler = SconsHandler(apio_env, plugin) 

119 

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

121 scons_handler.execute() 

122 

123 def _register_common_targets(self, synth_srcs): 

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

125 are used by a few top level targets. 

126 """ 

127 

128 apio_env = self.apio_env 

129 params = apio_env.params 

130 plugin = self.arch_plugin 

131 

132 # -- Sanity check 

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

134 

135 # -- Synth builder and target. 

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

137 

138 # -- Yosys synthesis builder and target. 

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

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

141 proto_util.check_not_required(params, "verbosity") 

142 synth_target = apio_env.add_builder_target( 

143 builder_id=SYNTH_BUILDER, 

144 target=apio_env.target, 

145 sources=[synth_srcs], 

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

147 ) 

148 

149 # -- Place-and-route builder and target 

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

151 

152 proto_util.check_not_required(params, "verbosity") 

153 pnr_target = apio_env.add_builder_target( 

154 builder_id=PNR_BUILDER, 

155 target=apio_env.target, 

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

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

158 ) 

159 

160 # -- Bitstream builder builder and target 

161 apio_env.add_builder( 

162 BITSTREAM_BUILDER, plugin.make_bitstream_builder() 

163 ) 

164 

165 apio_env.add_builder_target( 

166 builder_id=BITSTREAM_BUILDER, 

167 target=apio_env.target, 

168 sources=pnr_target, 

169 ) 

170 

171 def _register_apio_build_target(self, synth_srcs): 

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

173 apio_env = self.apio_env 

174 params = apio_env.params 

175 plugin = self.arch_plugin 

176 

177 # -- Sanity check 

178 assert apio_env.targeting_one_of("build") 

179 

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

181 self._register_common_targets(synth_srcs) 

182 

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

184 target_file = ( 

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

186 ) 

187 

188 # -- Top level "build" target. 

189 apio_env.add_alias( 

190 "build", 

191 source=target_file, 

192 always_build=( 

193 params.verbosity.all 

194 or params.verbosity.synth 

195 or params.verbosity.pnr 

196 ), 

197 ) 

198 

199 def _register_apio_upload_target(self, synth_srcs): 

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

201 generated by the bitstream generator.""" 

202 

203 apio_env = self.apio_env 

204 plugin_info = self.arch_plugin.plugin_info() 

205 

206 # -- Sanity check 

207 assert apio_env.targeting_one_of("upload") 

208 

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

210 self._register_common_targets(synth_srcs) 

211 

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

213 apio_env.add_alias( 

214 "upload", 

215 source=apio_env.target + plugin_info.bitstream_file_suffix, 

216 action=get_programmer_cmd(apio_env), 

217 always_build=True, 

218 ) 

219 

220 def _register_apio_report_target(self, synth_srcs): 

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

222 PNR generated .pnr file.""" 

223 apio_env = self.apio_env 

224 params = apio_env.params 

225 

226 # -- Sanity check 

227 assert apio_env.targeting_one_of("report") 

228 

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

230 self._register_common_targets(synth_srcs) 

231 

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

233 apio_env.add_alias( 

234 "report", 

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

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

237 always_build=True, 

238 ) 

239 

240 def _register_apio_graph_target( 

241 self, 

242 synth_srcs, 

243 ): 

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

245 yosys and renders it using graphviz.""" 

246 apio_env = self.apio_env 

247 params = apio_env.params 

248 plugin = self.arch_plugin 

249 

250 # -- Sanity check 

251 assert apio_env.targeting_one_of("graph") 

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

253 

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

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

256 

257 dot_target = apio_env.add_builder_target( 

258 builder_id=YOSYS_DOT_BUILDER, 

259 target=apio_env.graph_target, 

260 sources=synth_srcs, 

261 always_build=True, 

262 ) 

263 

264 # -- Create the rendering builder and target. 

265 apio_env.add_builder( 

266 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder() 

267 ) 

268 graphviz_target = apio_env.add_builder_target( 

269 builder_id=GRAPHVIZ_RENDERER_BUILDER, 

270 target=apio_env.graph_target, 

271 sources=dot_target, 

272 always_build=True, 

273 ) 

274 

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

276 apio_env.add_alias( 

277 "graph", 

278 source=graphviz_target, 

279 always_build=True, 

280 ) 

281 

282 def _register_apio_lint_target(self, synth_srcs, test_srcs): 

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

284 and runs the linter.""" 

285 

286 apio_env = self.apio_env 

287 params = apio_env.params 

288 plugin = self.arch_plugin 

289 

290 # -- Sanity check 

291 assert apio_env.targeting_one_of("lint") 

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

293 

294 # -- Get lint params proto. 

295 lint_params = params.target.lint 

296 

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

298 extra_dependencies = [] 

299 

300 lint_whole_project = not lint_params.file_names 

301 using_vlt = lint_whole_project and (not lint_params.novlt) 

302 if using_vlt: 

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

304 # -- of some librariy warnings is enable. 

305 apio_env.add_builder( 

306 LINT_CONFIG_BUILDER, plugin.make_lint_config_builder() 

307 ) 

308 

309 lint_config_target = apio_env.add_builder_target( 

310 builder_id=LINT_CONFIG_BUILDER, 

311 target=apio_env.target, 

312 sources=[], 

313 ) 

314 extra_dependencies.append(lint_config_target) 

315 

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

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

318 

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

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

321 # -- testbench files in the project. 

322 if lint_params.file_names: 

323 files_to_lint = [ 

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

325 ] 

326 else: 

327 files_to_lint = synth_srcs + test_srcs 

328 

329 lint_out_target = apio_env.add_builder_target( 

330 builder_id=LINT_BUILDER, 

331 target=apio_env.target, 

332 sources=files_to_lint, 

333 extra_dependencies=extra_dependencies, 

334 ) 

335 

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

337 apio_env.add_alias( 

338 "lint", 

339 source=lint_out_target, 

340 always_build=True, 

341 ) 

342 

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

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

345 simulation of a testbench.""" 

346 

347 apio_env = self.apio_env 

348 params = apio_env.params 

349 plugin = self.arch_plugin 

350 

351 # -- Sanity check 

352 assert apio_env.targeting_one_of("sim") 

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

354 

355 # -- Get values. 

356 sim_params: SimParams = params.target.sim 

357 

358 # -- Collect information for sim. 

359 testbench_info: TestbenchInfo = get_apio_sim_testbench_info( 

360 apio_env, 

361 sim_params, 

362 synth_srcs, 

363 test_srcs, 

364 ) 

365 

366 # -- Compilation builder and target 

367 

368 apio_env.add_builder( 

369 TESTBENCH_COMPILE_BUILDER, plugin.make_testbench_compile_builder() 

370 ) 

371 

372 sim_out_target = apio_env.add_builder_target( 

373 builder_id=TESTBENCH_COMPILE_BUILDER, 

374 target=testbench_info.build_testbench_name, 

375 sources=testbench_info.srcs, 

376 always_build=sim_params.force_sim, 

377 ) 

378 

379 # -- Simulation builder and target.. 

380 

381 apio_env.add_builder( 

382 TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder() 

383 ) 

384 

385 sim_vcd_target = apio_env.add_builder_target( 

386 builder_id=TESTBENCH_RUN_BUILDER, 

387 target=testbench_info.build_testbench_name, 

388 sources=[sim_out_target], 

389 always_build=sim_params.force_sim, 

390 ) 

391 

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

393 # -- for avoiding pylance warnings 

394 gtkwave_extra_options: list[str] = [ 

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

396 ] 

397 

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

399 gtkwave_target( 

400 apio_env, 

401 "sim", 

402 sim_vcd_target, 

403 testbench_info, 

404 sim_params, 

405 gtkwave_extra_options, 

406 ) 

407 

408 def _register_apio_test_target(self, synth_srcs, test_srcs): 

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

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

411 

412 apio_env = self.apio_env 

413 params = apio_env.params 

414 plugin = self.arch_plugin 

415 

416 # -- Sanity check 

417 assert apio_env.targeting_one_of("test") 

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

419 

420 # -- Collect the test related values. 

421 test_params = params.target.test 

422 testbenches_infos = get_apio_test_testbenches_infos( 

423 apio_env, 

424 test_params, 

425 synth_srcs, 

426 test_srcs, 

427 ) 

428 

429 # -- Create compilation and simulation targets. 

430 apio_env.add_builder( 

431 TESTBENCH_COMPILE_BUILDER, plugin.make_testbench_compile_builder() 

432 ) 

433 apio_env.add_builder( 

434 TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder() 

435 ) 

436 

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

438 tests_targets = [] 

439 for testbench_info in testbenches_infos: 

440 

441 # -- Create the compilation target. 

442 test_out_target = apio_env.add_builder_target( 

443 builder_id=TESTBENCH_COMPILE_BUILDER, 

444 target=testbench_info.build_testbench_name, 

445 sources=testbench_info.srcs, 

446 always_build=True, 

447 ) 

448 

449 # -- Create the simulation target. 

450 test_vcd_target = apio_env.add_builder_target( 

451 builder_id=TESTBENCH_RUN_BUILDER, 

452 target=testbench_info.build_testbench_name, 

453 sources=[test_out_target], 

454 always_build=True, 

455 ) 

456 

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

458 tests_targets.append(test_vcd_target) 

459 

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

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

462 

463 def execute(self): 

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

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

466 return.""" 

467 

468 apio_env = self.apio_env 

469 

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

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

472 synth_srcs, test_srcs = get_project_source_files() 

473 

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

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

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

477 

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

479 targets = apio_env.command_line_targets 

480 assert len(targets) == 1, targets 

481 target = targets[0] 

482 

483 # -- Dispatch by target. 

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

485 # -- python 3.9. 

486 if target == "build": 

487 self._register_apio_build_target(synth_srcs) 

488 

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

490 self._register_apio_upload_target(synth_srcs) 

491 

492 elif target == "report": 

493 self._register_apio_report_target(synth_srcs) 

494 

495 elif target == "graph": 

496 self._register_apio_graph_target(synth_srcs) 

497 

498 elif target == "sim": 

499 self._register_apio_sim_target(synth_srcs, test_srcs) 

500 

501 elif target == "test": 

502 self._register_apio_test_target(synth_srcs, test_srcs) 

503 

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

505 self._register_apio_lint_target(synth_srcs, test_srcs) 

506 

507 else: 

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

509 

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

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