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

159 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 02:47 +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 # -- Top level "build" target. 

165 apio_env.alias( 

166 "build", 

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

168 always_build=( 

169 params.verbosity.all 

170 or params.verbosity.synth 

171 or params.verbosity.pnr 

172 ), 

173 ) 

174 

175 def _register_apio_upload_target(self, synth_srcs): 

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

177 generated by the bitstream generator.""" 

178 

179 apio_env = self.apio_env 

180 plugin_info = self.arch_plugin.plugin_info() 

181 

182 # -- Sanity check 

183 assert apio_env.targeting_one_of("upload") 

184 

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

186 self._register_common_targets(synth_srcs) 

187 

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

189 apio_env.alias( 

190 "upload", 

191 source=apio_env.target + plugin_info.bin_file_suffix, 

192 action=get_programmer_cmd(apio_env), 

193 always_build=True, 

194 ) 

195 

196 def _register_apio_report_target(self, synth_srcs): 

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

198 PNR generated .pnr file.""" 

199 apio_env = self.apio_env 

200 params = apio_env.params 

201 plugin_info = self.arch_plugin.plugin_info() 

202 

203 # -- Sanity check 

204 assert apio_env.targeting_one_of("report") 

205 

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

207 self._register_common_targets(synth_srcs) 

208 

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

210 apio_env.alias( 

211 "report", 

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

213 action=report_action( 

214 plugin_info.clk_name_index, params.verbosity.pnr 

215 ), 

216 always_build=True, 

217 ) 

218 

219 def _register_apio_graph_target( 

220 self, 

221 synth_srcs, 

222 ): 

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

224 yosys and renders it using graphviz.""" 

225 apio_env = self.apio_env 

226 params = apio_env.params 

227 plugin = self.arch_plugin 

228 

229 # -- Sanity check 

230 assert apio_env.targeting_one_of("graph") 

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

232 

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

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

235 

236 dot_target = apio_env.builder_target( 

237 builder_id=YOSYS_DOT_BUILDER, 

238 target=apio_env.graph_target, 

239 sources=synth_srcs, 

240 always_build=True, 

241 ) 

242 

243 # -- Create the rendering builder and target. 

244 apio_env.builder( 

245 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder() 

246 ) 

247 graphviz_target = apio_env.builder_target( 

248 builder_id=GRAPHVIZ_RENDERER_BUILDER, 

249 target=apio_env.graph_target, 

250 sources=dot_target, 

251 always_build=True, 

252 ) 

253 

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

255 apio_env.alias( 

256 "graph", 

257 source=graphviz_target, 

258 always_build=True, 

259 ) 

260 

261 def _register_apio_lint_target(self, synth_srcs, test_srcs): 

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

263 and runs the linter.""" 

264 

265 apio_env = self.apio_env 

266 params = apio_env.params 

267 plugin = self.arch_plugin 

268 

269 # -- Sanity check 

270 assert apio_env.targeting_one_of("lint") 

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

272 

273 # -- Get lint params proto. 

274 lint_params = params.target.lint 

275 

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

277 extra_dependencies = [] 

278 

279 if not lint_params.novlt: 279 ↛ 292line 279 didn't jump to line 292 because the condition on line 279 was always true

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

281 # -- of some librariy warnings is enable. 

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

283 

284 lint_config_target = apio_env.builder_target( 

285 builder_id=LINT_CONFIG_BUILDER, 

286 target=apio_env.target, 

287 sources=[], 

288 ) 

289 extra_dependencies.append(lint_config_target) 

290 

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

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

293 

294 lint_out_target = apio_env.builder_target( 

295 builder_id=LINT_BUILDER, 

296 target=apio_env.target, 

297 sources=synth_srcs + test_srcs, 

298 extra_dependencies=extra_dependencies, 

299 ) 

300 

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

302 apio_env.alias( 

303 "lint", 

304 source=lint_out_target, 

305 always_build=True, 

306 ) 

307 

308 def _register_apio_sim_target(self, synth_srcs, test_srcs): 

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

310 simulation of a testbench.""" 

311 

312 apio_env = self.apio_env 

313 params = apio_env.params 

314 plugin = self.arch_plugin 

315 

316 # -- Sanity check 

317 assert apio_env.targeting_one_of("sim") 

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

319 

320 # -- Get values. 

321 sim_params: SimParams = params.target.sim 

322 

323 # -- Collect information for sim. 

324 testbench_info: TestbenchInfo = get_apio_sim_testbench_info( 

325 apio_env, 

326 sim_params, 

327 synth_srcs, 

328 test_srcs, 

329 ) 

330 

331 # -- Compilation builder and target 

332 

333 apio_env.builder( 

334 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

335 ) 

336 

337 sim_out_target = apio_env.builder_target( 

338 builder_id=TESTBENCH_COMPILE_BUILDER, 

339 target=testbench_info.build_testbench_name, 

340 sources=testbench_info.srcs, 

341 always_build=sim_params.force_sim, 

342 ) 

343 

344 # -- Simulation builder and target.. 

345 

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

347 

348 sim_vcd_target = apio_env.builder_target( 

349 builder_id=TESTBENCH_RUN_BUILDER, 

350 target=testbench_info.build_testbench_name, 

351 sources=[sim_out_target], 

352 always_build=sim_params.force_sim, 

353 ) 

354 

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

356 gtkwave_target( 

357 apio_env, 

358 "sim", 

359 sim_vcd_target, 

360 testbench_info, 

361 sim_params, 

362 params.apio_env_params.gtkwave_extra_options, 

363 ) 

364 

365 def _register_apio_test_target(self, synth_srcs, test_srcs): 

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

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

368 

369 apio_env = self.apio_env 

370 params = apio_env.params 

371 plugin = self.arch_plugin 

372 

373 # -- Sanity check 

374 assert apio_env.targeting_one_of("test") 

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

376 

377 # -- Collect the test related values. 

378 test_params = params.target.test 

379 testbenches_infos = get_apio_test_testbenches_infos( 

380 apio_env, 

381 test_params, 

382 synth_srcs, 

383 test_srcs, 

384 ) 

385 

386 # -- Create compilation and simulation targets. 

387 apio_env.builder( 

388 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

389 ) 

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

391 

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

393 tests_targets = [] 

394 for testbench_info in testbenches_infos: 

395 

396 # -- Create the compilation target. 

397 test_out_target = apio_env.builder_target( 

398 builder_id=TESTBENCH_COMPILE_BUILDER, 

399 target=testbench_info.build_testbench_name, 

400 sources=testbench_info.srcs, 

401 always_build=True, 

402 ) 

403 

404 # -- Create the simulation target. 

405 test_vcd_target = apio_env.builder_target( 

406 builder_id=TESTBENCH_RUN_BUILDER, 

407 target=testbench_info.build_testbench_name, 

408 sources=[test_out_target], 

409 always_build=True, 

410 ) 

411 

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

413 tests_targets.append(test_vcd_target) 

414 

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

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

417 

418 def execute(self): 

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

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

421 return.""" 

422 

423 apio_env = self.apio_env 

424 

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

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

427 synth_srcs, test_srcs = get_project_source_files() 

428 

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

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

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

432 

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

434 targets = apio_env.command_line_targets 

435 assert len(targets) == 1, targets 

436 target = targets[0] 

437 

438 # -- Dispatch by target. 

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

440 # -- python 3.9. 

441 if target == "build": 

442 self._register_apio_build_target(synth_srcs) 

443 

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

445 self._register_apio_upload_target(synth_srcs) 

446 

447 elif target == "report": 

448 self._register_apio_report_target(synth_srcs) 

449 

450 elif target == "graph": 

451 self._register_apio_graph_target(synth_srcs) 

452 

453 elif target == "sim": 

454 self._register_apio_sim_target(synth_srcs, test_srcs) 

455 

456 elif target == "test": 

457 self._register_apio_test_target(synth_srcs, test_srcs) 

458 

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

460 self._register_apio_lint_target(synth_srcs, test_srcs) 

461 

462 else: 

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

464 sys.exit(1) 

465 

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

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