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
« 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
9"""Apio scons related utilities.."""
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
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"
47class SconsHandler:
48 """Base apio scons handler"""
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
55 @staticmethod
56 def start() -> None:
57 """This static method is called from SConstruct to create and
58 execute an SconsHandler."""
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()
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")
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
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()
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 )
92 # -- Create the apio environment.
93 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params)
95 # -- Sanity check that we are consistent on the ApioPlatform selection.
96 assert apio_env.apio_platform is apio_platform
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 )
117 # -- Create the handler.
118 scons_handler = SconsHandler(apio_env, plugin)
120 # -- Invoke the handler. This services the scons request.
121 scons_handler.execute()
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 """
128 apio_env = self.apio_env
129 params = apio_env.params
130 plugin = self.arch_plugin
132 # -- Sanity check
133 assert apio_env.targeting_one_of("build", "upload", "report")
135 # -- Synth builder and target.
136 apio_env.add_builder(SYNTH_BUILDER, plugin.make_synth_builder())
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 )
149 # -- Place-and-route builder and target
150 apio_env.add_builder(PNR_BUILDER, plugin.make_pnr_builder())
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 )
160 # -- Bitstream builder builder and target
161 apio_env.add_builder(
162 BITSTREAM_BUILDER, plugin.make_bitstream_builder()
163 )
165 apio_env.add_builder_target(
166 builder_id=BITSTREAM_BUILDER,
167 target=apio_env.target,
168 sources=pnr_target,
169 )
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
177 # -- Sanity check
178 assert apio_env.targeting_one_of("build")
180 # -- Register the common targets for synth, pnr, and bitstream.
181 self._register_common_targets(synth_srcs)
183 # -- Target is the packager's output bitstream file.
184 target_file = (
185 apio_env.target + plugin.plugin_info().bitstream_file_suffix
186 )
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 )
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."""
203 apio_env = self.apio_env
204 plugin_info = self.arch_plugin.plugin_info()
206 # -- Sanity check
207 assert apio_env.targeting_one_of("upload")
209 # -- Register the common targets for synth, pnr, and bitstream.
210 self._register_common_targets(synth_srcs)
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 )
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
226 # -- Sanity check
227 assert apio_env.targeting_one_of("report")
229 # -- Register the common targets for synth, pnr, and bitstream.
230 self._register_common_targets(synth_srcs)
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 )
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
250 # -- Sanity check
251 assert apio_env.targeting_one_of("graph")
252 assert params.target.HasField("graph")
254 # -- Create the .dot generation builder and target.
255 apio_env.add_builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder())
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 )
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 )
275 # -- Create the top level "graph" target.
276 apio_env.add_alias(
277 "graph",
278 source=graphviz_target,
279 always_build=True,
280 )
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."""
286 apio_env = self.apio_env
287 params = apio_env.params
288 plugin = self.arch_plugin
290 # -- Sanity check
291 assert apio_env.targeting_one_of("lint")
292 assert params.target.HasField("lint")
294 # -- Get lint params proto.
295 lint_params = params.target.lint
297 # -- Create the builder and target of the config file creation.
298 extra_dependencies = []
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 )
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)
316 # -- Create the builder and target the lint operation.
317 apio_env.add_builder(LINT_BUILDER, plugin.make_lint_builder())
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
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 )
336 # -- Create the top level "lint" target.
337 apio_env.add_alias(
338 "lint",
339 source=lint_out_target,
340 always_build=True,
341 )
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."""
347 apio_env = self.apio_env
348 params = apio_env.params
349 plugin = self.arch_plugin
351 # -- Sanity check
352 assert apio_env.targeting_one_of("sim")
353 assert params.target.HasField("sim")
355 # -- Get values.
356 sim_params: SimParams = params.target.sim
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 )
366 # -- Compilation builder and target
368 apio_env.add_builder(
369 TESTBENCH_COMPILE_BUILDER, plugin.make_testbench_compile_builder()
370 )
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 )
379 # -- Simulation builder and target..
381 apio_env.add_builder(
382 TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder()
383 )
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 )
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 ]
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 )
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."""
412 apio_env = self.apio_env
413 params = apio_env.params
414 plugin = self.arch_plugin
416 # -- Sanity check
417 assert apio_env.targeting_one_of("test")
418 assert params.target.HasField("test")
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 )
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 )
437 # -- Create targets for each testbench we are testing.
438 tests_targets = []
439 for testbench_info in testbenches_infos:
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 )
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 )
457 # -- Append to the list of targets we need to execute.
458 tests_targets.append(test_vcd_target)
460 # -- The top level 'test' target.
461 apio_env.add_alias("test", source=tests_targets, always_build=True)
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."""
468 apio_env = self.apio_env
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()
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")
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]
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)
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)
492 elif target == "report":
493 self._register_apio_report_target(synth_srcs)
495 elif target == "graph":
496 self._register_apio_graph_target(synth_srcs)
498 elif target == "sim":
499 self._register_apio_sim_target(synth_srcs, test_srcs)
501 elif target == "test":
502 self._register_apio_test_target(synth_srcs, test_srcs)
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)
507 else:
508 fatal_error(f"Unexpected scons target: {target}")
510 # -- Note that so far we just registered builders and target.
511 # -- The actual execution is done by scons once this method returns.