Coverage for apio/scons/scons_handler.py: 92%
177 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +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
11"""Apio scons related utilities.."""
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.scons.plugin_xilinx import PluginXilinx
22from apio.common.proto.apio_pb2 import (
23 SimParams,
24 SconsParams,
25 ICE40,
26 ECP5,
27 GOWIN,
28 XILINX,
29)
30from apio.common import apio_console
31from apio.scons.apio_env import ApioEnv
32from apio.scons.plugin_base import PluginBase
33from apio.common import rich_lib_windows
34from apio.scons.plugin_util import (
35 get_apio_sim_testbench_info,
36 get_apio_test_testbenches_infos,
37 gtkwave_target,
38 report_action,
39 get_programmer_cmd,
40 TestbenchInfo,
41)
42from apio.common.apio_console import cerror, cout
44# -- Scons builders ids.
45SYNTH_BUILDER = "SYNTH_BUILDER"
46PNR_BUILDER = "PNR_BUILDER"
47BITSTREAM_PRE_BUILDER = "BITSTREAM_PRE_BUILDER"
48BITSTREAM_BUILDER = "BITSTREAM_BUILDER"
49TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER"
50TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER"
51YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER"
52GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER"
53LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER"
54LINT_BUILDER = "LINT_BUILDER"
57class SconsHandler:
58 """Base apio scons handler"""
60 def __init__(self, apio_env: ApioEnv, arch_plugin: PluginBase):
61 """Do not call directly, use SconsHandler.start()."""
62 self.apio_env = apio_env
63 self.arch_plugin = arch_plugin
65 @staticmethod
66 def start() -> None:
67 """This static method is called from SConstruct to create and
68 execute an SconsHandler."""
70 # -- Read the text of the scons params file.
71 params_path = Path(ARGUMENTS["params"])
72 with open(params_path, "r", encoding="utf8") as f:
73 proto_text = f.read()
75 # -- Parse the text into SconsParams object.
76 params: SconsParams = text_format.Parse(proto_text, SconsParams())
78 # -- Compare the params timestamp to the timestamp in the command.
79 # timestamp = ARGUMENTS["timestamp"]
80 # assert params.timestamp == timestamp
82 # -- If running on windows, apply the lib library workaround
83 if params.environment.is_windows: 83 ↛ 84line 83 didn't jump to line 84 because the condition on line 83 was never true
84 rich_lib_windows.apply_workaround()
86 # -- Set terminal mode and theme to match the apio process.
87 apio_console.configure(
88 terminal_mode=params.environment.terminal_mode,
89 theme_name=params.environment.theme_name,
90 )
92 # -- Create the apio environment.
93 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params)
95 # -- Select the plugin.
96 if params.arch == ICE40:
97 plugin = PluginIce40(apio_env)
98 elif params.arch == ECP5:
99 plugin = PluginEcp5(apio_env)
100 elif params.arch == GOWIN:
101 plugin = PluginGowin(apio_env)
102 elif params.arch == XILINX: 102 ↛ 105line 102 didn't jump to line 105 because the condition on line 102 was always true
103 plugin = PluginXilinx(apio_env)
104 else:
105 cout(
106 f"Apio SConstruct dispatch error: unknown arch [{params.arch}]"
107 )
108 sys.exit(1)
110 # -- Create the handler.
111 scons_handler = SconsHandler(apio_env, plugin)
113 # -- Invoke the handler. This services the scons request.
114 scons_handler.execute()
116 def _register_common_targets(self, synth_srcs):
117 """Register the common synth, pnr, and bitstream operations which
118 are used by a few top level targets.
119 """
121 apio_env = self.apio_env
122 params = apio_env.params
123 plugin = self.arch_plugin
125 # -- Sanity check
126 assert apio_env.targeting_one_of("build", "upload", "report")
128 # -- Synth builder and target.
129 apio_env.builder(SYNTH_BUILDER, plugin.synth_builder())
131 synth_target = apio_env.builder_target(
132 builder_id=SYNTH_BUILDER,
133 target=apio_env.target,
134 sources=[synth_srcs],
135 always_build=(params.verbosity.all or params.verbosity.synth),
136 )
138 # -- Place-and-route builder and target
139 apio_env.builder(PNR_BUILDER, plugin.pnr_builder())
141 pnr_target = apio_env.builder_target(
142 builder_id=PNR_BUILDER,
143 target=apio_env.target,
144 sources=[synth_target, self.arch_plugin.constrain_file()],
145 always_build=(params.verbosity.all or params.verbosity.pnr),
146 )
148 # -- DEBUG
149 # -- Special case for xilinx
150 if apio_env.params.arch == XILINX:
152 # -- Access to plugin.pre_builder()
153 # -- But using getattr pylance does not complain
154 # -- Cannot access attribute "bitstream_pre_builder"
155 # -- for class "PluginBase" (reportAttributeAccessIssue)
156 pre_builder = getattr(plugin, "bitstream_pre_builder")
158 # -- The bitstream builder consist of two stages
159 # -- First stage: pre_builder
160 apio_env.builder(BITSTREAM_PRE_BUILDER, pre_builder())
162 pre_builder_target = apio_env.builder_target(
163 builder_id=BITSTREAM_PRE_BUILDER,
164 target=apio_env.target,
165 sources=pnr_target,
166 )
168 # -- Second stage: builder
169 apio_env.builder(BITSTREAM_BUILDER, plugin.bitstream_builder())
171 apio_env.builder_target(
172 builder_id=BITSTREAM_BUILDER,
173 target=apio_env.target,
174 sources=pre_builder_target,
175 )
177 else:
179 # -- Bitstream builder builder and target
180 apio_env.builder(BITSTREAM_BUILDER, plugin.bitstream_builder())
182 apio_env.builder_target(
183 builder_id=BITSTREAM_BUILDER,
184 target=apio_env.target,
185 sources=pnr_target,
186 )
188 def _register_apio_build_target(self, synth_srcs):
189 """Register the 'build' target which creates the binary bitstream."""
190 apio_env = self.apio_env
191 params = apio_env.params
192 plugin = self.arch_plugin
194 # -- Sanity check
195 assert apio_env.targeting_one_of("build")
197 # -- Register the common targets for synth, pnr, and bitstream.
198 self._register_common_targets(synth_srcs)
200 # -- Determine target file. Normally it's the bitstream file but
201 # -- if building with nextpnr --gui flag we skip the packing step
202 # -- and stop after the nextpnr step.
203 if params.nextpnr_gui: 203 ↛ 205line 203 didn't jump to line 205 because the condition on line 203 was never true
204 # -- Target is the pnr output file.
205 target_file = (
206 apio_env.target + plugin.plugin_info().pnr_file_suffix
207 )
208 else:
209 # -- Target is the packager's output bitstream file.
210 target_file = (
211 apio_env.target + plugin.plugin_info().bitstream_file_suffix
212 )
214 # -- Top level "build" target.
215 apio_env.alias(
216 "build",
217 source=target_file,
218 always_build=(
219 params.verbosity.all
220 or params.verbosity.synth
221 or params.verbosity.pnr
222 ),
223 )
225 def _register_apio_upload_target(self, synth_srcs):
226 """Register the 'upload' target which upload the binary file
227 generated by the bitstream generator."""
229 apio_env = self.apio_env
230 plugin_info = self.arch_plugin.plugin_info()
232 # -- Sanity check
233 assert apio_env.targeting_one_of("upload")
235 # -- Register the common targets for synth, pnr, and bitstream.
236 self._register_common_targets(synth_srcs)
238 # -- Create the top level 'upload' target.
239 apio_env.alias(
240 "upload",
241 source=apio_env.target + plugin_info.bitstream_file_suffix,
242 action=get_programmer_cmd(apio_env),
243 always_build=True,
244 )
246 def _register_apio_report_target(self, synth_srcs):
247 """Registers the 'report' target which a report file from the
248 PNR generated .pnr file."""
249 apio_env = self.apio_env
250 params = apio_env.params
251 plugin_info = self.arch_plugin.plugin_info()
253 # -- Sanity check
254 assert apio_env.targeting_one_of("report")
256 # -- Register the common targets for synth, pnr, and bitstream.
257 self._register_common_targets(synth_srcs)
259 # -- Register the top level 'report' target.
260 apio_env.alias(
261 "report",
262 source=apio_env.target + ".pnr",
263 action=report_action(
264 plugin_info.clk_name_index, params.verbosity.pnr
265 ),
266 always_build=True,
267 )
269 def _register_apio_graph_target(
270 self,
271 synth_srcs,
272 ):
273 """Registers the 'graph' target which generates a .dot file using
274 yosys and renders it using graphviz."""
275 apio_env = self.apio_env
276 params = apio_env.params
277 plugin = self.arch_plugin
279 # -- Sanity check
280 assert apio_env.targeting_one_of("graph")
281 assert params.target.HasField("graph")
283 # -- Create the .dot generation builder and target.
284 apio_env.builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder())
286 dot_target = apio_env.builder_target(
287 builder_id=YOSYS_DOT_BUILDER,
288 target=apio_env.graph_target,
289 sources=synth_srcs,
290 always_build=True,
291 )
293 # -- Create the rendering builder and target.
294 apio_env.builder(
295 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder()
296 )
297 graphviz_target = apio_env.builder_target(
298 builder_id=GRAPHVIZ_RENDERER_BUILDER,
299 target=apio_env.graph_target,
300 sources=dot_target,
301 always_build=True,
302 )
304 # -- Create the top level "graph" target.
305 apio_env.alias(
306 "graph",
307 source=graphviz_target,
308 always_build=True,
309 )
311 def _register_apio_lint_target(self, synth_srcs, test_srcs):
312 """Registers the 'lint' target which creates a lint configuration file
313 and runs the linter."""
315 apio_env = self.apio_env
316 params = apio_env.params
317 plugin = self.arch_plugin
319 # -- Sanity check
320 assert apio_env.targeting_one_of("lint")
321 assert params.target.HasField("lint")
323 # -- Get lint params proto.
324 lint_params = params.target.lint
326 # -- Create the builder and target of the config file creation.
327 extra_dependencies = []
329 lint_whole_project = not lint_params.file_names
330 using_vlt = lint_whole_project and (not lint_params.novlt)
331 if using_vlt:
332 # -- The auto generated verilator config file with supression
333 # -- of some librariy warnings is enable.
334 apio_env.builder(LINT_CONFIG_BUILDER, plugin.lint_config_builder())
336 lint_config_target = apio_env.builder_target(
337 builder_id=LINT_CONFIG_BUILDER,
338 target=apio_env.target,
339 sources=[],
340 )
341 extra_dependencies.append(lint_config_target)
343 # -- Create the builder and target the lint operation.
344 apio_env.builder(LINT_BUILDER, plugin.lint_builder())
346 # -- Determine the files that will be linted. If specific files were
347 # -- not specified on the command line, we take all the source and
348 # -- testbench files in the project.
349 if lint_params.file_names:
350 files_to_lint = [
351 apio_env.scons_env.File(f) for f in lint_params.file_names
352 ]
353 else:
354 files_to_lint = synth_srcs + test_srcs
356 lint_out_target = apio_env.builder_target(
357 builder_id=LINT_BUILDER,
358 target=apio_env.target,
359 sources=files_to_lint,
360 extra_dependencies=extra_dependencies,
361 )
363 # -- Create the top level "lint" target.
364 apio_env.alias(
365 "lint",
366 source=lint_out_target,
367 always_build=True,
368 )
370 def _register_apio_sim_target(self, synth_srcs, test_srcs):
371 """Registers the 'sim' targets which compiles and runs the
372 simulation of a testbench."""
374 apio_env = self.apio_env
375 params = apio_env.params
376 plugin = self.arch_plugin
378 # -- Sanity check
379 assert apio_env.targeting_one_of("sim")
380 assert params.target.HasField("sim")
382 # -- Get values.
383 sim_params: SimParams = params.target.sim
385 # -- Collect information for sim.
386 testbench_info: TestbenchInfo = get_apio_sim_testbench_info(
387 apio_env,
388 sim_params,
389 synth_srcs,
390 test_srcs,
391 )
393 # -- Compilation builder and target
395 apio_env.builder(
396 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
397 )
399 sim_out_target = apio_env.builder_target(
400 builder_id=TESTBENCH_COMPILE_BUILDER,
401 target=testbench_info.build_testbench_name,
402 sources=testbench_info.srcs,
403 always_build=sim_params.force_sim,
404 )
406 # -- Simulation builder and target..
408 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
410 sim_vcd_target = apio_env.builder_target(
411 builder_id=TESTBENCH_RUN_BUILDER,
412 target=testbench_info.build_testbench_name,
413 sources=[sim_out_target],
414 always_build=sim_params.force_sim,
415 )
417 # -- Get the gtkwave extra options (with the correct type)
418 # -- for avoiding pylance warnings
419 gtkwave_extra_options = params.apio_env_params.gtkwave_extra_options
420 gtkwave_extra_options = [str(x) for x in gtkwave_extra_options]
422 # -- The top level "sim" target.
423 gtkwave_target(
424 apio_env,
425 "sim",
426 sim_vcd_target,
427 testbench_info,
428 sim_params,
429 gtkwave_extra_options,
430 )
432 def _register_apio_test_target(self, synth_srcs, test_srcs):
433 """Registers 'test' target and its dependencies. Each testbench
434 is tested independently with its own set of sub-targets."""
436 apio_env = self.apio_env
437 params = apio_env.params
438 plugin = self.arch_plugin
440 # -- Sanity check
441 assert apio_env.targeting_one_of("test")
442 assert params.target.HasField("test")
444 # -- Collect the test related values.
445 test_params = params.target.test
446 testbenches_infos = get_apio_test_testbenches_infos(
447 apio_env,
448 test_params,
449 synth_srcs,
450 test_srcs,
451 )
453 # -- Create compilation and simulation targets.
454 apio_env.builder(
455 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
456 )
457 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
459 # -- Create targets for each testbench we are testing.
460 tests_targets = []
461 for testbench_info in testbenches_infos:
463 # -- Create the compilation target.
464 test_out_target = apio_env.builder_target(
465 builder_id=TESTBENCH_COMPILE_BUILDER,
466 target=testbench_info.build_testbench_name,
467 sources=testbench_info.srcs,
468 always_build=True,
469 )
471 # -- Create the simulation target.
472 test_vcd_target = apio_env.builder_target(
473 builder_id=TESTBENCH_RUN_BUILDER,
474 target=testbench_info.build_testbench_name,
475 sources=[test_out_target],
476 always_build=True,
477 )
479 # -- Append to the list of targets we need to execute.
480 tests_targets.append(test_vcd_target)
482 # -- The top level 'test' target.
483 apio_env.alias("test", source=tests_targets, always_build=True)
485 def execute(self):
486 """The entry point of the scons handler. It registers the builders
487 and targets for the selected command and scons executes in upon
488 return."""
490 apio_env = self.apio_env
492 # -- Collect the lists of the synthesizable files (e.g. "main.v") and a
493 # -- testbench files (e.g. "main_tb.v")
494 synth_srcs, test_srcs = get_project_source_files()
496 # -- Sanity check that we don't call the scons to do cleanup. This is
497 # -- handled directly by the 'apio clean' command.
498 assert not apio_env.scons_env.GetOption("clean")
500 # -- Get the target, we expect exactly one.
501 targets = apio_env.command_line_targets
502 assert len(targets) == 1, targets
503 target = targets[0]
505 # -- Dispatch by target.
506 # -- Not using python 'match' statement for compatibility with
507 # -- python 3.9.
508 if target == "build":
509 self._register_apio_build_target(synth_srcs)
511 elif target == "upload": 511 ↛ 512line 511 didn't jump to line 512 because the condition on line 511 was never true
512 self._register_apio_upload_target(synth_srcs)
514 elif target == "report":
515 self._register_apio_report_target(synth_srcs)
517 elif target == "graph":
518 self._register_apio_graph_target(synth_srcs)
520 elif target == "sim":
521 self._register_apio_sim_target(synth_srcs, test_srcs)
523 elif target == "test":
524 self._register_apio_test_target(synth_srcs, test_srcs)
526 elif target == "lint": 526 ↛ 530line 526 didn't jump to line 530 because the condition on line 526 was always true
527 self._register_apio_lint_target(synth_srcs, test_srcs)
529 else:
530 cerror(f"Unexpected scons target: {target}")
531 sys.exit(1)
533 # -- Note that so far we just registered builders and target.
534 # -- The actual execution is done by scons once this method returns.