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

167 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-26 02:38 +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_apio_sim_testbench_info, 

34 get_apio_test_testbenches_infos, 

35 gtkwave_target, 

36 report_action, 

37 get_programmer_cmd, 

38 TestbenchInfo, 

39) 

40from apio.common.apio_console import cerror, cout 

41 

42# -- Scons builders ids. 

43SYNTH_BUILDER = "SYNTH_BUILDER" 

44PNR_BUILDER = "PNR_BUILDER" 

45BITSTREAM_BUILDER = "BITSTREAM_BUILDER" 

46TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER" 

47TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER" 

48YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER" 

49GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER" 

50LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER" 

51LINT_BUILDER = "LINT_BUILDER" 

52 

53 

54class SconsHandler: 

55 """Base apio scons handler""" 

56 

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

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

59 self.apio_env = apio_env 

60 self.arch_plugin = arch_plugin 

61 

62 @staticmethod 

63 def start() -> None: 

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

65 execute an SconsHandler.""" 

66 

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

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

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

70 proto_text = f.read() 

71 

72 # -- Parse the text into SconsParams object. 

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

74 

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

76 timestamp = ARGUMENTS["timestamp"] 

77 assert params.timestamp == timestamp 

78 

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

80 if params.environment.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 apio_console.configure( 

85 terminal_mode=params.environment.terminal_mode, 

86 theme_name=params.environment.theme_name, 

87 ) 

88 

89 # -- Create the apio environment. 

90 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params) 

91 

92 # -- Select the plugin. 

93 if params.arch == ICE40: 

94 plugin = PluginIce40(apio_env) 

95 elif params.arch == ECP5: 

96 plugin = PluginEcp5(apio_env) 

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

98 plugin = PluginGowin(apio_env) 

99 else: 

100 cout( 

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

102 ) 

103 sys.exit(1) 

104 

105 # -- Create the handler. 

106 scons_handler = SconsHandler(apio_env, plugin) 

107 

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

109 scons_handler.execute() 

110 

111 def _register_common_targets(self, synth_srcs): 

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

113 are used by a few top level targets. 

114 """ 

115 

116 apio_env = self.apio_env 

117 params = apio_env.params 

118 plugin = self.arch_plugin 

119 

120 # -- Sanity check 

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

122 

123 # -- Synth builder and target. 

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

125 

126 synth_target = apio_env.builder_target( 

127 builder_id=SYNTH_BUILDER, 

128 target=apio_env.target, 

129 sources=[synth_srcs], 

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

131 ) 

132 

133 # -- Place-and-route builder and target 

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

135 

136 pnr_target = apio_env.builder_target( 

137 builder_id=PNR_BUILDER, 

138 target=apio_env.target, 

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

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

141 ) 

142 

143 # -- Bitstream builder builder and target 

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

145 

146 apio_env.builder_target( 

147 builder_id=BITSTREAM_BUILDER, 

148 target=apio_env.target, 

149 sources=pnr_target, 

150 ) 

151 

152 def _register_apio_build_target(self, synth_srcs): 

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

154 apio_env = self.apio_env 

155 params = apio_env.params 

156 plugin = self.arch_plugin 

157 

158 # -- Sanity check 

159 assert apio_env.targeting_one_of("build") 

160 

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

162 self._register_common_targets(synth_srcs) 

163 

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

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

166 # -- and stop after the nextpnr step. 

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

168 # -- Target is the pnr output file. 

169 target_file = ( 

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

171 ) 

172 else: 

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

174 target_file = ( 

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

176 ) 

177 

178 # -- Top level "build" target. 

179 apio_env.alias( 

180 "build", 

181 source=target_file, 

182 always_build=( 

183 params.verbosity.all 

184 or params.verbosity.synth 

185 or params.verbosity.pnr 

186 ), 

187 ) 

188 

189 def _register_apio_upload_target(self, synth_srcs): 

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

191 generated by the bitstream generator.""" 

192 

193 apio_env = self.apio_env 

194 plugin_info = self.arch_plugin.plugin_info() 

195 

196 # -- Sanity check 

197 assert apio_env.targeting_one_of("upload") 

198 

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

200 self._register_common_targets(synth_srcs) 

201 

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

203 apio_env.alias( 

204 "upload", 

205 source=apio_env.target + plugin_info.bitstream_file_suffix, 

206 action=get_programmer_cmd(apio_env), 

207 always_build=True, 

208 ) 

209 

210 def _register_apio_report_target(self, synth_srcs): 

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

212 PNR generated .pnr file.""" 

213 apio_env = self.apio_env 

214 params = apio_env.params 

215 plugin_info = self.arch_plugin.plugin_info() 

216 

217 # -- Sanity check 

218 assert apio_env.targeting_one_of("report") 

219 

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

221 self._register_common_targets(synth_srcs) 

222 

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

224 apio_env.alias( 

225 "report", 

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

227 action=report_action( 

228 plugin_info.clk_name_index, params.verbosity.pnr 

229 ), 

230 always_build=True, 

231 ) 

232 

233 def _register_apio_graph_target( 

234 self, 

235 synth_srcs, 

236 ): 

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

238 yosys and renders it using graphviz.""" 

239 apio_env = self.apio_env 

240 params = apio_env.params 

241 plugin = self.arch_plugin 

242 

243 # -- Sanity check 

244 assert apio_env.targeting_one_of("graph") 

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

246 

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

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

249 

250 dot_target = apio_env.builder_target( 

251 builder_id=YOSYS_DOT_BUILDER, 

252 target=apio_env.graph_target, 

253 sources=synth_srcs, 

254 always_build=True, 

255 ) 

256 

257 # -- Create the rendering builder and target. 

258 apio_env.builder( 

259 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder() 

260 ) 

261 graphviz_target = apio_env.builder_target( 

262 builder_id=GRAPHVIZ_RENDERER_BUILDER, 

263 target=apio_env.graph_target, 

264 sources=dot_target, 

265 always_build=True, 

266 ) 

267 

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

269 apio_env.alias( 

270 "graph", 

271 source=graphviz_target, 

272 always_build=True, 

273 ) 

274 

275 def _register_apio_lint_target(self, synth_srcs, test_srcs): 

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

277 and runs the linter.""" 

278 

279 apio_env = self.apio_env 

280 params = apio_env.params 

281 plugin = self.arch_plugin 

282 

283 # -- Sanity check 

284 assert apio_env.targeting_one_of("lint") 

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

286 

287 # -- Get lint params proto. 

288 lint_params = params.target.lint 

289 

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

291 extra_dependencies = [] 

292 

293 lint_whole_project = not lint_params.file_names 

294 using_vlt = lint_whole_project and (not lint_params.novlt) 

295 if using_vlt: 

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

297 # -- of some librariy warnings is enable. 

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

299 

300 lint_config_target = apio_env.builder_target( 

301 builder_id=LINT_CONFIG_BUILDER, 

302 target=apio_env.target, 

303 sources=[], 

304 ) 

305 extra_dependencies.append(lint_config_target) 

306 

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

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

309 

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

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

312 # -- testbench files in the project. 

313 if lint_params.file_names: 

314 files_to_lint = [ 

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

316 ] 

317 else: 

318 files_to_lint = synth_srcs + test_srcs 

319 

320 lint_out_target = apio_env.builder_target( 

321 builder_id=LINT_BUILDER, 

322 target=apio_env.target, 

323 sources=files_to_lint, 

324 extra_dependencies=extra_dependencies, 

325 ) 

326 

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

328 apio_env.alias( 

329 "lint", 

330 source=lint_out_target, 

331 always_build=True, 

332 ) 

333 

334 def _register_apio_sim_target(self, synth_srcs, test_srcs): 

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

336 simulation of a testbench.""" 

337 

338 apio_env = self.apio_env 

339 params = apio_env.params 

340 plugin = self.arch_plugin 

341 

342 # -- Sanity check 

343 assert apio_env.targeting_one_of("sim") 

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

345 

346 # -- Get values. 

347 sim_params: SimParams = params.target.sim 

348 

349 # -- Collect information for sim. 

350 testbench_info: TestbenchInfo = get_apio_sim_testbench_info( 

351 apio_env, 

352 sim_params, 

353 synth_srcs, 

354 test_srcs, 

355 ) 

356 

357 # -- Compilation builder and target 

358 

359 apio_env.builder( 

360 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

361 ) 

362 

363 sim_out_target = apio_env.builder_target( 

364 builder_id=TESTBENCH_COMPILE_BUILDER, 

365 target=testbench_info.build_testbench_name, 

366 sources=testbench_info.srcs, 

367 always_build=sim_params.force_sim, 

368 ) 

369 

370 # -- Simulation builder and target.. 

371 

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

373 

374 sim_vcd_target = apio_env.builder_target( 

375 builder_id=TESTBENCH_RUN_BUILDER, 

376 target=testbench_info.build_testbench_name, 

377 sources=[sim_out_target], 

378 always_build=sim_params.force_sim, 

379 ) 

380 

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

382 gtkwave_target( 

383 apio_env, 

384 "sim", 

385 sim_vcd_target, 

386 testbench_info, 

387 sim_params, 

388 params.apio_env_params.gtkwave_extra_options, 

389 ) 

390 

391 def _register_apio_test_target(self, synth_srcs, test_srcs): 

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

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

394 

395 apio_env = self.apio_env 

396 params = apio_env.params 

397 plugin = self.arch_plugin 

398 

399 # -- Sanity check 

400 assert apio_env.targeting_one_of("test") 

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

402 

403 # -- Collect the test related values. 

404 test_params = params.target.test 

405 testbenches_infos = get_apio_test_testbenches_infos( 

406 apio_env, 

407 test_params, 

408 synth_srcs, 

409 test_srcs, 

410 ) 

411 

412 # -- Create compilation and simulation targets. 

413 apio_env.builder( 

414 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

415 ) 

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

417 

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

419 tests_targets = [] 

420 for testbench_info in testbenches_infos: 

421 

422 # -- Create the compilation target. 

423 test_out_target = apio_env.builder_target( 

424 builder_id=TESTBENCH_COMPILE_BUILDER, 

425 target=testbench_info.build_testbench_name, 

426 sources=testbench_info.srcs, 

427 always_build=True, 

428 ) 

429 

430 # -- Create the simulation target. 

431 test_vcd_target = apio_env.builder_target( 

432 builder_id=TESTBENCH_RUN_BUILDER, 

433 target=testbench_info.build_testbench_name, 

434 sources=[test_out_target], 

435 always_build=True, 

436 ) 

437 

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

439 tests_targets.append(test_vcd_target) 

440 

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

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

443 

444 def execute(self): 

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

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

447 return.""" 

448 

449 apio_env = self.apio_env 

450 

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

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

453 synth_srcs, test_srcs = get_project_source_files() 

454 

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

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

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

458 

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

460 targets = apio_env.command_line_targets 

461 assert len(targets) == 1, targets 

462 target = targets[0] 

463 

464 # -- Dispatch by target. 

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

466 # -- python 3.9. 

467 if target == "build": 

468 self._register_apio_build_target(synth_srcs) 

469 

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

471 self._register_apio_upload_target(synth_srcs) 

472 

473 elif target == "report": 

474 self._register_apio_report_target(synth_srcs) 

475 

476 elif target == "graph": 

477 self._register_apio_graph_target(synth_srcs) 

478 

479 elif target == "sim": 

480 self._register_apio_sim_target(synth_srcs, test_srcs) 

481 

482 elif target == "test": 

483 self._register_apio_test_target(synth_srcs, test_srcs) 

484 

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

486 self._register_apio_lint_target(synth_srcs, test_srcs) 

487 

488 else: 

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

490 sys.exit(1) 

491 

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

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