Coverage for apio/scons/scons_handler.py: 93%
175 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +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.."""
13from pathlib import Path
14from SCons.Script import ARGUMENTS, COMMAND_LINE_TARGETS
15from google.protobuf import text_format
16from apio.common.common_util import get_project_source_files
17from apio.scons.plugin_ice40 import PluginIce40
18from apio.scons.plugin_ecp5 import PluginEcp5
19from apio.scons.plugin_gowin import PluginGowin
20from apio.scons.plugin_xilinx import PluginXilinx
21from apio.common.proto.apio_common_pb2 import ApioArch
22from apio.common.proto.apio_scons_pb2 import SimParams, SconsParams
23from apio.common import apio_console, proto_util
24from apio.scons.apio_env import ApioEnv
25from apio.scons.plugin_base import PluginBase
26from apio.common import rich_lib_windows
27from apio.scons.plugin_util import (
28 get_apio_sim_testbench_info,
29 get_apio_test_testbenches_infos,
30 gtkwave_target,
31 report_action,
32 get_programmer_cmd,
33 TestbenchInfo,
34)
35from apio.common.apio_console import fatal_error
37# -- Scons builders ids.
38SYNTH_BUILDER = "SYNTH_BUILDER"
39PNR_BUILDER = "PNR_BUILDER"
40BITSTREAM_BUILDER = "BITSTREAM_BUILDER"
41TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER"
42TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER"
43YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER"
44GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER"
45LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER"
46LINT_BUILDER = "LINT_BUILDER"
49class SconsHandler:
50 """Base apio scons handler"""
52 def __init__(self, apio_env: ApioEnv, arch_plugin: PluginBase):
53 """Do not call directly, use SconsHandler.start()."""
54 self.apio_env = apio_env
55 self.arch_plugin = arch_plugin
57 @staticmethod
58 def start() -> None:
59 """This static method is called from SConstruct to create and
60 execute an SconsHandler."""
62 # -- Read the text of the scons params file.
63 params_path = Path(ARGUMENTS["params"])
64 with open(params_path, "r", encoding="utf8") as f:
65 proto_text = f.read()
67 # -- Parse the text into SconsParams object.
68 params: SconsParams = text_format.Parse(proto_text, SconsParams())
69 proto_util.check_is_initialized(params, "Failed to parse scons params")
71 # -- Compare the params timestamp to the timestamp in the command.
72 # -- This verified that we got the intended file instance.
73 proto_util.check_is_required(params, "timestamp")
74 timestamp = ARGUMENTS["timestamp"]
75 assert params.timestamp == timestamp
77 # -- If running on windows, apply the lib library workaround
78 proto_util.check_is_required(params, "environment.is_windows")
79 if params.environment.is_windows: 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true
80 rich_lib_windows.apply_workaround()
82 # -- Set terminal mode and theme to match the apio process.
83 proto_util.check_is_required(
84 params, "environment.terminal_mode", "environment.theme_name"
85 )
86 apio_console.configure(
87 terminal_mode=params.environment.terminal_mode,
88 theme_name=params.environment.theme_name,
89 )
91 # -- Create the apio environment.
92 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params)
94 # -- Select the plugin.
95 proto_util.check_is_required(params, "arch")
96 plugin: PluginBase
97 match params.arch:
98 case ApioArch.ice40:
99 plugin = PluginIce40(apio_env)
100 case ApioArch.ecp5:
101 plugin = PluginEcp5(apio_env)
102 case ApioArch.gowin:
103 plugin = PluginGowin(apio_env)
104 case ApioArch.xilinx: 104 ↛ 106line 104 didn't jump to line 106 because the pattern on line 104 always matched
105 plugin = PluginXilinx(apio_env)
106 case _:
107 fatal_error(
108 "Apio SConstruct dispatch error: unknown "
109 + f"arch [{ApioArch.Name(params.arch)}]"
110 )
112 # -- Create the handler.
113 scons_handler = SconsHandler(apio_env, plugin)
115 # -- Invoke the handler. This services the scons request.
116 scons_handler.execute()
118 def _register_common_targets(self, synth_srcs):
119 """Register the common synth, pnr, and bitstream operations which
120 are used by a few top level targets.
121 """
123 apio_env = self.apio_env
124 params = apio_env.params
125 plugin = self.arch_plugin
127 # -- Sanity check
128 assert apio_env.targeting_one_of("build", "upload", "report")
130 # -- Synth builder and target.
131 apio_env.add_builder(SYNTH_BUILDER, plugin.make_synth_builder())
133 # -- Yosys synthesis builder and target.
134 # -- Verbosity is an optional field so we rely on the protocol buffer
135 # -- implicit default value false for non populated boolean fields.
136 proto_util.check_not_required(params, "verbosity")
137 synth_target = apio_env.add_builder_target(
138 builder_id=SYNTH_BUILDER,
139 target=apio_env.target,
140 sources=[synth_srcs],
141 always_build=(params.verbosity.all or params.verbosity.synth),
142 )
144 # -- Place-and-route builder and target
145 apio_env.add_builder(PNR_BUILDER, plugin.make_pnr_builder())
147 proto_util.check_not_required(params, "verbosity")
148 pnr_target = apio_env.add_builder_target(
149 builder_id=PNR_BUILDER,
150 target=apio_env.target,
151 sources=[synth_target, self.arch_plugin.constrain_file()],
152 always_build=(params.verbosity.all or params.verbosity.pnr),
153 )
155 # -- Bitstream builder builder and target
156 apio_env.add_builder(
157 BITSTREAM_BUILDER, plugin.make_bitstream_builder()
158 )
160 apio_env.add_builder_target(
161 builder_id=BITSTREAM_BUILDER,
162 target=apio_env.target,
163 sources=pnr_target,
164 )
166 def _register_apio_build_target(self, synth_srcs):
167 """Register the 'build' target which creates the binary bitstream."""
168 apio_env = self.apio_env
169 params = apio_env.params
170 plugin = self.arch_plugin
172 # -- Sanity check
173 assert apio_env.targeting_one_of("build")
175 # -- Register the common targets for synth, pnr, and bitstream.
176 self._register_common_targets(synth_srcs)
178 # -- Target is the packager's output bitstream file.
179 target_file = (
180 apio_env.target + plugin.plugin_info().bitstream_file_suffix
181 )
183 # -- Top level "build" target.
184 apio_env.add_alias(
185 "build",
186 source=target_file,
187 always_build=(
188 params.verbosity.all
189 or params.verbosity.synth
190 or params.verbosity.pnr
191 ),
192 )
194 def _register_apio_upload_target(self, synth_srcs):
195 """Register the 'upload' target which upload the binary file
196 generated by the bitstream generator."""
198 apio_env = self.apio_env
199 plugin_info = self.arch_plugin.plugin_info()
201 # -- Sanity check
202 assert apio_env.targeting_one_of("upload")
204 # -- Register the common targets for synth, pnr, and bitstream.
205 self._register_common_targets(synth_srcs)
207 # -- Create the top level 'upload' target.
208 apio_env.add_alias(
209 "upload",
210 source=apio_env.target + plugin_info.bitstream_file_suffix,
211 action=get_programmer_cmd(apio_env),
212 always_build=True,
213 )
215 def _register_apio_report_target(self, synth_srcs):
216 """Registers the 'report' target which a report file from the
217 PNR generated .pnr file."""
218 apio_env = self.apio_env
219 params = apio_env.params
221 # -- Sanity check
222 assert apio_env.targeting_one_of("report")
224 # -- Register the common targets for synth, pnr, and bitstream.
225 self._register_common_targets(synth_srcs)
227 # -- Register the top level 'report' target.
228 apio_env.add_alias(
229 "report",
230 source=apio_env.target + ".pnr",
231 action=report_action(params.verbosity.pnr),
232 always_build=True,
233 )
235 def _register_apio_graph_target(
236 self,
237 synth_srcs,
238 ):
239 """Registers the 'graph' target which generates a .dot file using
240 yosys and renders it using graphviz."""
241 apio_env = self.apio_env
242 params = apio_env.params
243 plugin = self.arch_plugin
245 # -- Sanity check
246 assert apio_env.targeting_one_of("graph")
247 assert params.target.HasField("graph")
249 # -- Create the .dot generation builder and target.
250 apio_env.add_builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder())
252 dot_target = apio_env.add_builder_target(
253 builder_id=YOSYS_DOT_BUILDER,
254 target=apio_env.graph_target,
255 sources=synth_srcs,
256 always_build=True,
257 )
259 # -- Create the rendering builder and target.
260 apio_env.add_builder(
261 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder()
262 )
263 graphviz_target = apio_env.add_builder_target(
264 builder_id=GRAPHVIZ_RENDERER_BUILDER,
265 target=apio_env.graph_target,
266 sources=dot_target,
267 always_build=True,
268 )
270 # -- Create the top level "graph" target.
271 apio_env.add_alias(
272 "graph",
273 source=graphviz_target,
274 always_build=True,
275 )
277 def _register_apio_lint_target(self, synth_srcs, test_srcs):
278 """Registers the 'lint' target which creates a lint configuration file
279 and runs the linter."""
281 apio_env = self.apio_env
282 params = apio_env.params
283 plugin = self.arch_plugin
285 # -- Sanity check
286 assert apio_env.targeting_one_of("lint")
287 assert params.target.HasField("lint")
289 # -- Get lint params proto.
290 lint_params = params.target.lint
292 # -- Create the builder and target of the config file creation.
293 extra_dependencies = []
295 lint_whole_project = not lint_params.file_names
296 using_vlt = lint_whole_project and (not lint_params.novlt)
297 if using_vlt:
298 # -- The auto generated verilator config file with supression
299 # -- of some librariy warnings is enable.
300 apio_env.add_builder(
301 LINT_CONFIG_BUILDER, plugin.make_lint_config_builder()
302 )
304 lint_config_target = apio_env.add_builder_target(
305 builder_id=LINT_CONFIG_BUILDER,
306 target=apio_env.target,
307 sources=[],
308 )
309 extra_dependencies.append(lint_config_target)
311 # -- Create the builder and target the lint operation.
312 apio_env.add_builder(LINT_BUILDER, plugin.make_lint_builder())
314 # -- Determine the files that will be linted. If specific files were
315 # -- not specified on the command line, we take all the source and
316 # -- testbench files in the project.
317 if lint_params.file_names:
318 files_to_lint = [
319 apio_env.scons_env.File(f) for f in lint_params.file_names
320 ]
321 else:
322 files_to_lint = synth_srcs + test_srcs
324 lint_out_target = apio_env.add_builder_target(
325 builder_id=LINT_BUILDER,
326 target=apio_env.target,
327 sources=files_to_lint,
328 extra_dependencies=extra_dependencies,
329 )
331 # -- Create the top level "lint" target.
332 apio_env.add_alias(
333 "lint",
334 source=lint_out_target,
335 always_build=True,
336 )
338 def _register_apio_sim_target(self, synth_srcs, test_srcs) -> None:
339 """Registers the 'sim' targets which compiles and runs the
340 simulation of a testbench."""
342 apio_env = self.apio_env
343 params = apio_env.params
344 plugin = self.arch_plugin
346 # -- Sanity check
347 assert apio_env.targeting_one_of("sim")
348 assert params.target.HasField("sim")
350 # -- Get values.
351 sim_params: SimParams = params.target.sim
353 # -- Collect information for sim.
354 testbench_info: TestbenchInfo = get_apio_sim_testbench_info(
355 apio_env,
356 sim_params,
357 synth_srcs,
358 test_srcs,
359 )
361 # -- Compilation builder and target
363 apio_env.add_builder(
364 TESTBENCH_COMPILE_BUILDER, plugin.make_testbench_compile_builder()
365 )
367 sim_out_target = apio_env.add_builder_target(
368 builder_id=TESTBENCH_COMPILE_BUILDER,
369 target=testbench_info.build_testbench_name,
370 sources=testbench_info.srcs,
371 always_build=sim_params.force_sim,
372 )
374 # -- Simulation builder and target..
376 apio_env.add_builder(
377 TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder()
378 )
380 sim_vcd_target = apio_env.add_builder_target(
381 builder_id=TESTBENCH_RUN_BUILDER,
382 target=testbench_info.build_testbench_name,
383 sources=[sim_out_target],
384 always_build=sim_params.force_sim,
385 )
387 # -- Get the gtkwave extra options (with the correct type)
388 # -- for avoiding pylance warnings
389 gtkwave_extra_options: list[str] = [
390 str(x) for x in params.apio_env_params.gtkwave_extra_options
391 ]
393 # -- The top level "sim" target.
394 gtkwave_target(
395 apio_env,
396 "sim",
397 sim_vcd_target,
398 testbench_info,
399 sim_params,
400 gtkwave_extra_options,
401 )
403 def _register_apio_test_target(self, synth_srcs, test_srcs):
404 """Registers 'test' target and its dependencies. Each testbench
405 is tested independently with its own set of sub-targets."""
407 apio_env = self.apio_env
408 params = apio_env.params
409 plugin = self.arch_plugin
411 # -- Sanity check
412 assert apio_env.targeting_one_of("test")
413 assert params.target.HasField("test")
415 # -- Collect the test related values.
416 test_params = params.target.test
417 testbenches_infos = get_apio_test_testbenches_infos(
418 apio_env,
419 test_params,
420 synth_srcs,
421 test_srcs,
422 )
424 # -- Create compilation and simulation targets.
425 apio_env.add_builder(
426 TESTBENCH_COMPILE_BUILDER, plugin.make_testbench_compile_builder()
427 )
428 apio_env.add_builder(
429 TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder()
430 )
432 # -- Create targets for each testbench we are testing.
433 tests_targets = []
434 for testbench_info in testbenches_infos:
436 # -- Create the compilation target.
437 test_out_target = apio_env.add_builder_target(
438 builder_id=TESTBENCH_COMPILE_BUILDER,
439 target=testbench_info.build_testbench_name,
440 sources=testbench_info.srcs,
441 always_build=True,
442 )
444 # -- Create the simulation target.
445 test_vcd_target = apio_env.add_builder_target(
446 builder_id=TESTBENCH_RUN_BUILDER,
447 target=testbench_info.build_testbench_name,
448 sources=[test_out_target],
449 always_build=True,
450 )
452 # -- Append to the list of targets we need to execute.
453 tests_targets.append(test_vcd_target)
455 # -- The top level 'test' target.
456 apio_env.add_alias("test", source=tests_targets, always_build=True)
458 def execute(self):
459 """The entry point of the scons handler. It registers the builders
460 and targets for the selected command and scons executes in upon
461 return."""
463 apio_env = self.apio_env
465 # -- Collect the lists of the synthesizable files (e.g. "main.v") and a
466 # -- testbench files (e.g. "main_tb.v")
467 synth_srcs, test_srcs = get_project_source_files()
469 # -- Sanity check that we don't call the scons to do cleanup. This is
470 # -- handled directly by the 'apio clean' command.
471 assert not apio_env.scons_env.GetOption("clean")
473 # -- Get the target, we expect exactly one.
474 targets = apio_env.command_line_targets
475 assert len(targets) == 1, targets
476 target = targets[0]
478 # -- Dispatch by target.
479 # -- Not using python 'match' statement for compatibility with
480 # -- python 3.9.
481 if target == "build":
482 self._register_apio_build_target(synth_srcs)
484 elif target == "upload": 484 ↛ 485line 484 didn't jump to line 485 because the condition on line 484 was never true
485 self._register_apio_upload_target(synth_srcs)
487 elif target == "report":
488 self._register_apio_report_target(synth_srcs)
490 elif target == "graph":
491 self._register_apio_graph_target(synth_srcs)
493 elif target == "sim":
494 self._register_apio_sim_target(synth_srcs, test_srcs)
496 elif target == "test":
497 self._register_apio_test_target(synth_srcs, test_srcs)
499 elif target == "lint": 499 ↛ 503line 499 didn't jump to line 503 because the condition on line 499 was always true
500 self._register_apio_lint_target(synth_srcs, test_srcs)
502 else:
503 fatal_error(f"Unexpected scons target: {target}")
505 # -- Note that so far we just registered builders and target.
506 # -- The actual execution is done by scons once this method returns.