Coverage for apio / scons / scons_handler.py: 92%
156 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-24 01:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-24 01: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.."""
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_sim_config,
34 get_tests_configs,
35 gtkwave_target,
36 report_action,
37 get_programmer_cmd,
38)
39from apio.common.apio_console import cerror, cout
41# -- Scons builders ids.
42SYNTH_BUILDER = "SYNTH_BUILDER"
43PNR_BUILDER = "PNR_BUILDER"
44BITSTREAM_BUILDER = "BITSTREAM_BUILDER"
45TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER"
46TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER"
47YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER"
48GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER"
49LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER"
50LINT_BUILDER = "LINT_BUILDER"
53class SconsHandler:
54 """Base apio scons handler"""
56 def __init__(self, apio_env: ApioEnv, arch_plugin: PluginBase):
57 """Do not call directly, use SconsHandler.start()."""
58 self.apio_env = apio_env
59 self.arch_plugin = arch_plugin
61 @staticmethod
62 def start() -> None:
63 """This static method is called from SConstruct to create and
64 execute an SconsHandler."""
66 # -- Read the text of the scons params file.
67 params_path = Path(ARGUMENTS["params"])
68 with open(params_path, "r", encoding="utf8") as f:
69 proto_text = f.read()
71 # -- Parse the text into SconsParams object.
72 params: SconsParams = text_format.Parse(proto_text, SconsParams())
74 # -- Compare the params timestamp to the timestamp in the command.
75 timestamp = ARGUMENTS["timestamp"]
76 assert params.timestamp == timestamp
78 # -- If running on windows, apply the lib library workaround
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 apio_console.configure(
84 terminal_mode=params.environment.terminal_mode,
85 theme_name=params.environment.theme_name,
86 )
88 # -- Create the apio environment.
89 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params)
91 # -- Select the plugin.
92 if params.arch == ICE40:
93 plugin = PluginIce40(apio_env)
94 elif params.arch == ECP5:
95 plugin = PluginEcp5(apio_env)
96 elif params.arch == GOWIN: 96 ↛ 99line 96 didn't jump to line 99 because the condition on line 96 was always true
97 plugin = PluginGowin(apio_env)
98 else:
99 cout(
100 f"Apio SConstruct dispatch error: unknown arch [{params.arch}]"
101 )
102 sys.exit(1)
104 # -- Create the handler.
105 scons_handler = SconsHandler(apio_env, plugin)
107 # -- Invoke the handler. This services the scons request.
108 scons_handler.execute()
110 def _register_common_targets(self, synth_srcs):
111 """Register the common synth, pnr, and bitstream operations which
112 are used by a few top level targets.
113 """
115 apio_env = self.apio_env
116 params = apio_env.params
117 plugin = self.arch_plugin
119 # -- Sanity check
120 assert apio_env.targeting("build", "upload", "report")
122 # -- Synth builder and target.
123 apio_env.builder(SYNTH_BUILDER, plugin.synth_builder())
125 synth_target = apio_env.builder_target(
126 builder_id=SYNTH_BUILDER,
127 target=apio_env.target,
128 sources=[synth_srcs],
129 always_build=(params.verbosity.all or params.verbosity.synth),
130 )
132 # -- Place-and-route builder and target
133 apio_env.builder(PNR_BUILDER, plugin.pnr_builder())
135 pnr_target = apio_env.builder_target(
136 builder_id=PNR_BUILDER,
137 target=apio_env.target,
138 sources=[synth_target, self.arch_plugin.constrain_file()],
139 always_build=(params.verbosity.all or params.verbosity.pnr),
140 )
142 # -- Bitstream builder builder and target
143 apio_env.builder(BITSTREAM_BUILDER, plugin.bitstream_builder())
145 apio_env.builder_target(
146 builder_id=BITSTREAM_BUILDER,
147 target=apio_env.target,
148 sources=pnr_target,
149 )
151 def _register_build_target(self, synth_srcs):
152 """Register the 'build' target which creates the binary bitstream."""
153 apio_env = self.apio_env
154 params = apio_env.params
155 plugin = self.arch_plugin
157 # -- Sanity check
158 assert apio_env.targeting("build")
160 # -- Register the common targets for synth, pnr, and bitstream.
161 self._register_common_targets(synth_srcs)
163 # -- Top level "build" target.
164 apio_env.alias(
165 "build",
166 source=apio_env.target + plugin.plugin_info().bin_file_suffix,
167 always_build=(
168 params.verbosity.all
169 or params.verbosity.synth
170 or params.verbosity.pnr
171 ),
172 )
174 def _register_upload_target(self, synth_srcs):
175 """Register the 'upload' target which upload the binary file
176 generated by the bitstream generator."""
178 apio_env = self.apio_env
179 plugin_info = self.arch_plugin.plugin_info()
181 # -- Sanity check
182 assert apio_env.targeting("upload")
184 # -- Register the common targets for synth, pnr, and bitstream.
185 self._register_common_targets(synth_srcs)
187 # -- Create the top level 'upload' target.
188 apio_env.alias(
189 "upload",
190 source=apio_env.target + plugin_info.bin_file_suffix,
191 action=get_programmer_cmd(apio_env),
192 always_build=True,
193 )
195 def _register_report_target(self, synth_srcs):
196 """Registers the 'report' target which a report file from the
197 PNR generated .pnr file."""
198 apio_env = self.apio_env
199 params = apio_env.params
200 plugin_info = self.arch_plugin.plugin_info()
202 # -- Sanity check
203 assert apio_env.targeting("report")
205 # -- Register the common targets for synth, pnr, and bitstream.
206 self._register_common_targets(synth_srcs)
208 # -- Register the top level 'report' target.
209 apio_env.alias(
210 "report",
211 source=apio_env.target + ".pnr",
212 action=report_action(
213 plugin_info.clk_name_index, params.verbosity.pnr
214 ),
215 always_build=True,
216 )
218 def _register_graph_target(
219 self,
220 synth_srcs,
221 ):
222 """Registers the 'graph' target which generates a .dot file using
223 yosys and renders it using graphviz."""
224 apio_env = self.apio_env
225 params = apio_env.params
226 plugin = self.arch_plugin
228 # -- Sanity check
229 assert apio_env.targeting("graph")
230 assert params.target.HasField("graph")
232 # -- Create the .dot generation builder and target.
233 apio_env.builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder())
235 dot_target = apio_env.builder_target(
236 builder_id=YOSYS_DOT_BUILDER,
237 target=apio_env.graph_target,
238 sources=synth_srcs,
239 always_build=True,
240 )
242 # -- Create the rendering builder and target.
243 apio_env.builder(
244 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder()
245 )
246 graphviz_target = apio_env.builder_target(
247 builder_id=GRAPHVIZ_RENDERER_BUILDER,
248 target=apio_env.graph_target,
249 sources=dot_target,
250 always_build=True,
251 )
253 # -- Create the top level "graph" target.
254 apio_env.alias(
255 "graph",
256 source=graphviz_target,
257 always_build=True,
258 )
260 def _register_lint_target(self, synth_srcs, test_srcs):
261 """Registers the 'lint' target which creates a lint configuration file
262 and runs the linter."""
264 apio_env = self.apio_env
265 params = apio_env.params
266 plugin = self.arch_plugin
268 # -- Sanity check
269 assert apio_env.targeting("lint")
270 assert params.target.HasField("lint")
272 # -- Create the builder and target of the config file creation.
273 apio_env.builder(LINT_CONFIG_BUILDER, plugin.lint_config_builder())
275 lint_config_target = apio_env.builder_target(
276 builder_id=LINT_CONFIG_BUILDER,
277 target=apio_env.target,
278 sources=[],
279 )
281 # -- Create the builder and target the lint operation.
282 apio_env.builder(LINT_BUILDER, plugin.lint_builder())
284 lint_out_target = apio_env.builder_target(
285 builder_id=LINT_BUILDER,
286 target=apio_env.target,
287 sources=synth_srcs + test_srcs,
288 extra_dependencies=[lint_config_target],
289 )
291 # -- Create the top level "lint" target.
292 apio_env.alias(
293 "lint",
294 source=lint_out_target,
295 always_build=True,
296 )
298 def _register_sim_target(self, synth_srcs, test_srcs):
299 """Registers the 'sim' targets which compiles and runs the
300 simulation of a testbench."""
302 apio_env = self.apio_env
303 params = apio_env.params
304 plugin = self.arch_plugin
306 # -- Sanity check
307 assert apio_env.targeting("sim")
308 assert params.target.HasField("sim")
310 # -- Get values.
311 sim_params: SimParams = params.target.sim
312 testbench = sim_params.testbench # Optional.
314 # -- Collect information for sim.
315 sim_config = get_sim_config(apio_env, testbench, synth_srcs, test_srcs)
317 # -- Compilation builder and target
319 apio_env.builder(
320 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
321 )
323 sim_out_target = apio_env.builder_target(
324 builder_id=TESTBENCH_COMPILE_BUILDER,
325 target=sim_config.build_testbench_name,
326 sources=sim_config.srcs,
327 always_build=sim_params.force_sim,
328 )
330 # -- Simulation builder and target..
332 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
334 sim_vcd_target = apio_env.builder_target(
335 builder_id=TESTBENCH_RUN_BUILDER,
336 target=sim_config.build_testbench_name,
337 sources=[sim_out_target],
338 always_build=sim_params,
339 )
341 # -- The top level "sim" target.
342 gtkwave_target(
343 apio_env,
344 "sim",
345 sim_vcd_target,
346 sim_config,
347 sim_params,
348 )
350 def _register_test_target(self, synth_srcs, test_srcs):
351 """Registers 'test' target and its dependencies. Each testbench
352 is tested independently with its own set of sub-targets."""
354 apio_env = self.apio_env
355 params = apio_env.params
356 plugin = self.arch_plugin
358 # -- Sanity check
359 assert apio_env.targeting("test")
360 assert params.target.HasField("test")
362 # -- Collect the test related values.
363 test_params = params.target.test
364 tests_configs = get_tests_configs(
365 apio_env, test_params.testbench, synth_srcs, test_srcs
366 )
368 # -- Create compilation and simulation targets.
369 apio_env.builder(
370 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
371 )
372 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
374 # -- Create targets for each testbench we are testing.
375 tests_targets = []
376 for test_config in tests_configs:
378 # -- Create the compilation target.
379 test_out_target = apio_env.builder_target(
380 builder_id=TESTBENCH_COMPILE_BUILDER,
381 target=test_config.build_testbench_name,
382 sources=test_config.srcs,
383 always_build=True,
384 )
386 # -- Create the simulation target.
387 test_vcd_target = apio_env.builder_target(
388 builder_id=TESTBENCH_RUN_BUILDER,
389 target=test_config.build_testbench_name,
390 sources=[test_out_target],
391 always_build=True,
392 )
394 # -- Append to the list of targets we need to execute.
395 tests_targets.append(test_vcd_target)
397 # -- The top level 'test' target.
398 apio_env.alias("test", source=tests_targets, always_build=True)
400 def execute(self):
401 """The entry point of the scons handler. It registers the builders
402 and targets for the selected command and scons executes in upon
403 return."""
405 apio_env = self.apio_env
407 # -- Collect the lists of the synthesizable files (e.g. "main.v") and a
408 # -- testbench files (e.g. "main_tb.v")
409 synth_srcs, test_srcs = get_project_source_files()
411 # -- Sanity check that we don't call the scons to do cleanup. This is
412 # -- handled directly by the 'apio clean' command.
413 assert not apio_env.scons_env.GetOption("clean")
415 # -- Get the target, we expect exactly one.
416 targets = apio_env.command_line_targets
417 assert len(targets) == 1, targets
418 target = targets[0]
420 # -- Dispatch by target.
421 # -- Not using python 'match' statement for compatibility with
422 # -- python 3.9.
423 if target == "build":
424 self._register_build_target(synth_srcs)
426 elif target == "upload": 426 ↛ 427line 426 didn't jump to line 427 because the condition on line 426 was never true
427 self._register_upload_target(synth_srcs)
429 elif target == "report":
430 self._register_report_target(synth_srcs)
432 elif target == "graph":
433 self._register_graph_target(synth_srcs)
435 elif target == "sim":
436 self._register_sim_target(synth_srcs, test_srcs)
438 elif target == "test":
439 self._register_test_target(synth_srcs, test_srcs)
441 elif target == "lint": 441 ↛ 445line 441 didn't jump to line 445 because the condition on line 441 was always true
442 self._register_lint_target(synth_srcs, test_srcs)
444 else:
445 cerror(f"Unexpected scons target: {target}")
446 sys.exit(1)
448 # -- Note that so far we just registered builders and target.
449 # -- The actual execution is done by scons once this method returns.