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

156 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +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 SconsParams, ICE40, ECP5, GOWIN 

22from apio.common import apio_console 

23from apio.scons.apio_env import ApioEnv 

24from apio.scons.plugin_base import PluginBase 

25from apio.common import rich_lib_windows 

26from apio.scons.plugin_util import ( 

27 get_sim_config, 

28 get_tests_configs, 

29 waves_target, 

30 report_action, 

31 get_programmer_cmd, 

32) 

33from apio.common.apio_console import cerror, cout 

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 

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

69 timestamp = ARGUMENTS["timestamp"] 

70 assert params.timestamp == timestamp 

71 

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

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

74 rich_lib_windows.apply_workaround() 

75 

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

77 apio_console.configure( 

78 terminal_mode=params.environment.terminal_mode, 

79 theme_name=params.environment.theme_name, 

80 ) 

81 

82 # -- Create the apio environment. 

83 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params) 

84 

85 # -- Select the plugin. 

86 if params.arch == ICE40: 

87 plugin = PluginIce40(apio_env) 

88 elif params.arch == ECP5: 

89 plugin = PluginEcp5(apio_env) 

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

91 plugin = PluginGowin(apio_env) 

92 else: 

93 cout( 

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

95 ) 

96 sys.exit(1) 

97 

98 # -- Create the handler. 

99 scons_handler = SconsHandler(apio_env, plugin) 

100 

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

102 scons_handler.execute() 

103 

104 def _register_common_targets(self, synth_srcs): 

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

106 are used by a few top level targets. 

107 """ 

108 

109 apio_env = self.apio_env 

110 params = apio_env.params 

111 plugin = self.arch_plugin 

112 

113 # -- Sanity check 

114 assert apio_env.targeting("build", "upload", "report") 

115 

116 # -- Synth builder and target. 

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

118 

119 synth_target = apio_env.builder_target( 

120 builder_id=SYNTH_BUILDER, 

121 target=apio_env.target, 

122 sources=[synth_srcs], 

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

124 ) 

125 

126 # -- Place-and-route builder and target 

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

128 

129 pnr_target = apio_env.builder_target( 

130 builder_id=PNR_BUILDER, 

131 target=apio_env.target, 

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

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

134 ) 

135 

136 # -- Bitstream builder builder and target 

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

138 

139 apio_env.builder_target( 

140 builder_id=BITSTREAM_BUILDER, 

141 target=apio_env.target, 

142 sources=pnr_target, 

143 ) 

144 

145 def _register_build_target(self, synth_srcs): 

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

147 apio_env = self.apio_env 

148 params = apio_env.params 

149 plugin = self.arch_plugin 

150 

151 # -- Sanity check 

152 assert apio_env.targeting("build") 

153 

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

155 self._register_common_targets(synth_srcs) 

156 

157 # -- Top level "build" target. 

158 apio_env.alias( 

159 "build", 

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

161 always_build=( 

162 params.verbosity.all 

163 or params.verbosity.synth 

164 or params.verbosity.pnr 

165 ), 

166 ) 

167 

168 def _register_upload_target(self, synth_srcs): 

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

170 generated by the bitstream generator.""" 

171 

172 apio_env = self.apio_env 

173 plugin_info = self.arch_plugin.plugin_info() 

174 

175 # -- Sanity check 

176 assert apio_env.targeting("upload") 

177 

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

179 self._register_common_targets(synth_srcs) 

180 

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

182 apio_env.alias( 

183 "upload", 

184 source=apio_env.target + plugin_info.bin_file_suffix, 

185 action=get_programmer_cmd(apio_env), 

186 always_build=True, 

187 ) 

188 

189 def _register_report_target(self, synth_srcs): 

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

191 PNR generated .pnr file.""" 

192 apio_env = self.apio_env 

193 params = apio_env.params 

194 plugin_info = self.arch_plugin.plugin_info() 

195 

196 # -- Sanity check 

197 assert apio_env.targeting("report") 

198 

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

200 self._register_common_targets(synth_srcs) 

201 

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

203 apio_env.alias( 

204 "report", 

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

206 action=report_action( 

207 plugin_info.clk_name_index, params.verbosity.pnr 

208 ), 

209 always_build=True, 

210 ) 

211 

212 def _register_graph_target( 

213 self, 

214 synth_srcs, 

215 ): 

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

217 yosys and renders it using graphviz.""" 

218 apio_env = self.apio_env 

219 params = apio_env.params 

220 plugin = self.arch_plugin 

221 

222 # -- Sanity check 

223 assert apio_env.targeting("graph") 

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

225 

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

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

228 

229 dot_target = apio_env.builder_target( 

230 builder_id=YOSYS_DOT_BUILDER, 

231 target=apio_env.graph_target, 

232 sources=synth_srcs, 

233 always_build=True, 

234 ) 

235 

236 # -- Create the rendering builder and target. 

237 apio_env.builder( 

238 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder() 

239 ) 

240 graphviz_target = apio_env.builder_target( 

241 builder_id=GRAPHVIZ_RENDERER_BUILDER, 

242 target=apio_env.graph_target, 

243 sources=dot_target, 

244 always_build=True, 

245 ) 

246 

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

248 apio_env.alias( 

249 "graph", 

250 source=graphviz_target, 

251 always_build=True, 

252 ) 

253 

254 def _register_lint_target(self, synth_srcs, test_srcs): 

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

256 and runs the linter.""" 

257 

258 apio_env = self.apio_env 

259 params = apio_env.params 

260 plugin = self.arch_plugin 

261 

262 # -- Sanity check 

263 assert apio_env.targeting("lint") 

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

265 

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

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

268 

269 lint_config_target = apio_env.builder_target( 

270 builder_id=LINT_CONFIG_BUILDER, 

271 target=apio_env.target, 

272 sources=[], 

273 ) 

274 

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

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

277 

278 lint_out_target = apio_env.builder_target( 

279 builder_id=LINT_BUILDER, 

280 target=apio_env.target, 

281 sources=synth_srcs + test_srcs, 

282 extra_dependencies=[lint_config_target], 

283 ) 

284 

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

286 apio_env.alias( 

287 "lint", 

288 source=lint_out_target, 

289 always_build=True, 

290 ) 

291 

292 def _register_sim_target(self, synth_srcs, test_srcs): 

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

294 simulation of a testbench.""" 

295 

296 apio_env = self.apio_env 

297 params = apio_env.params 

298 plugin = self.arch_plugin 

299 

300 # -- Sanity check 

301 assert apio_env.targeting("sim") 

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

303 

304 # -- Get values. 

305 sim_params = params.target.sim 

306 testbench = sim_params.testbench # Optional. 

307 

308 # -- Collect information for sim. 

309 sim_config = get_sim_config(apio_env, testbench, synth_srcs, test_srcs) 

310 

311 # -- Compilation builder and target 

312 

313 apio_env.builder( 

314 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

315 ) 

316 

317 sim_out_target = apio_env.builder_target( 

318 builder_id=TESTBENCH_COMPILE_BUILDER, 

319 target=sim_config.build_testbench_name, 

320 sources=sim_config.srcs, 

321 always_build=sim_params.force_sim, 

322 ) 

323 

324 # -- Simulation builder and target.. 

325 

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

327 

328 sim_vcd_target = apio_env.builder_target( 

329 builder_id=TESTBENCH_RUN_BUILDER, 

330 target=sim_config.build_testbench_name, 

331 sources=[sim_out_target], 

332 always_build=sim_params, 

333 ) 

334 

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

336 waves_target( 

337 apio_env, 

338 "sim", 

339 sim_vcd_target, 

340 sim_config, 

341 no_gtkwave=sim_params.no_gtkwave, 

342 ) 

343 

344 def _register_test_target(self, synth_srcs, test_srcs): 

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

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

347 

348 apio_env = self.apio_env 

349 params = apio_env.params 

350 plugin = self.arch_plugin 

351 

352 # -- Sanity check 

353 assert apio_env.targeting("test") 

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

355 

356 # -- Collect the test related values. 

357 test_params = params.target.test 

358 tests_configs = get_tests_configs( 

359 apio_env, test_params.testbench, synth_srcs, test_srcs 

360 ) 

361 

362 # -- Create compilation and simulation targets. 

363 apio_env.builder( 

364 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder() 

365 ) 

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

367 

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

369 tests_targets = [] 

370 for test_config in tests_configs: 

371 

372 # -- Create the compilation target. 

373 test_out_target = apio_env.builder_target( 

374 builder_id=TESTBENCH_COMPILE_BUILDER, 

375 target=test_config.build_testbench_name, 

376 sources=test_config.srcs, 

377 always_build=True, 

378 ) 

379 

380 # -- Create the simulation target. 

381 test_vcd_target = apio_env.builder_target( 

382 builder_id=TESTBENCH_RUN_BUILDER, 

383 target=test_config.build_testbench_name, 

384 sources=[test_out_target], 

385 always_build=True, 

386 ) 

387 

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

389 tests_targets.append(test_vcd_target) 

390 

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

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

393 

394 def execute(self): 

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

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

397 return.""" 

398 

399 apio_env = self.apio_env 

400 

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

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

403 synth_srcs, test_srcs = get_project_source_files() 

404 

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

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

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

408 

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

410 targets = apio_env.command_line_targets 

411 assert len(targets) == 1, targets 

412 target = targets[0] 

413 

414 # -- Dispatch by target. 

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

416 # -- python 3.9. 

417 if target == "build": 

418 self._register_build_target(synth_srcs) 

419 

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

421 self._register_upload_target(synth_srcs) 

422 

423 elif target == "report": 

424 self._register_report_target(synth_srcs) 

425 

426 elif target == "graph": 

427 self._register_graph_target(synth_srcs) 

428 

429 elif target == "sim": 

430 self._register_sim_target(synth_srcs, test_srcs) 

431 

432 elif target == "test": 

433 self._register_test_target(synth_srcs, test_srcs) 

434 

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

436 self._register_lint_target(synth_srcs, test_srcs) 

437 

438 else: 

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

440 sys.exit(1) 

441 

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

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