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
« 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
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 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
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())
68 # -- Compare the params timestamp to the timestamp in the command.
69 timestamp = ARGUMENTS["timestamp"]
70 assert params.timestamp == timestamp
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()
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 )
82 # -- Create the apio environment.
83 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params)
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)
98 # -- Create the handler.
99 scons_handler = SconsHandler(apio_env, plugin)
101 # -- Invoke the handler. This services the scons request.
102 scons_handler.execute()
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 """
109 apio_env = self.apio_env
110 params = apio_env.params
111 plugin = self.arch_plugin
113 # -- Sanity check
114 assert apio_env.targeting("build", "upload", "report")
116 # -- Synth builder and target.
117 apio_env.builder(SYNTH_BUILDER, plugin.synth_builder())
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 )
126 # -- Place-and-route builder and target
127 apio_env.builder(PNR_BUILDER, plugin.pnr_builder())
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 )
136 # -- Bitstream builder builder and target
137 apio_env.builder(BITSTREAM_BUILDER, plugin.bitstream_builder())
139 apio_env.builder_target(
140 builder_id=BITSTREAM_BUILDER,
141 target=apio_env.target,
142 sources=pnr_target,
143 )
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
151 # -- Sanity check
152 assert apio_env.targeting("build")
154 # -- Register the common targets for synth, pnr, and bitstream.
155 self._register_common_targets(synth_srcs)
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 )
168 def _register_upload_target(self, synth_srcs):
169 """Register the 'upload' target which upload the binary file
170 generated by the bitstream generator."""
172 apio_env = self.apio_env
173 plugin_info = self.arch_plugin.plugin_info()
175 # -- Sanity check
176 assert apio_env.targeting("upload")
178 # -- Register the common targets for synth, pnr, and bitstream.
179 self._register_common_targets(synth_srcs)
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 )
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()
196 # -- Sanity check
197 assert apio_env.targeting("report")
199 # -- Register the common targets for synth, pnr, and bitstream.
200 self._register_common_targets(synth_srcs)
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 )
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
222 # -- Sanity check
223 assert apio_env.targeting("graph")
224 assert params.target.HasField("graph")
226 # -- Create the .dot generation builder and target.
227 apio_env.builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder())
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 )
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 )
247 # -- Create the top level "graph" target.
248 apio_env.alias(
249 "graph",
250 source=graphviz_target,
251 always_build=True,
252 )
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."""
258 apio_env = self.apio_env
259 params = apio_env.params
260 plugin = self.arch_plugin
262 # -- Sanity check
263 assert apio_env.targeting("lint")
264 assert params.target.HasField("lint")
266 # -- Create the builder and target of the config file creation.
267 apio_env.builder(LINT_CONFIG_BUILDER, plugin.lint_config_builder())
269 lint_config_target = apio_env.builder_target(
270 builder_id=LINT_CONFIG_BUILDER,
271 target=apio_env.target,
272 sources=[],
273 )
275 # -- Create the builder and target the lint operation.
276 apio_env.builder(LINT_BUILDER, plugin.lint_builder())
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 )
285 # -- Create the top level "lint" target.
286 apio_env.alias(
287 "lint",
288 source=lint_out_target,
289 always_build=True,
290 )
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."""
296 apio_env = self.apio_env
297 params = apio_env.params
298 plugin = self.arch_plugin
300 # -- Sanity check
301 assert apio_env.targeting("sim")
302 assert params.target.HasField("sim")
304 # -- Get values.
305 sim_params = params.target.sim
306 testbench = sim_params.testbench # Optional.
308 # -- Collect information for sim.
309 sim_config = get_sim_config(apio_env, testbench, synth_srcs, test_srcs)
311 # -- Compilation builder and target
313 apio_env.builder(
314 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
315 )
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 )
324 # -- Simulation builder and target..
326 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
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 )
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 )
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."""
348 apio_env = self.apio_env
349 params = apio_env.params
350 plugin = self.arch_plugin
352 # -- Sanity check
353 assert apio_env.targeting("test")
354 assert params.target.HasField("test")
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 )
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())
368 # -- Create targets for each testbench we are testing.
369 tests_targets = []
370 for test_config in tests_configs:
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 )
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 )
388 # -- Append to the list of targets we need to execute.
389 tests_targets.append(test_vcd_target)
391 # -- The top level 'test' target.
392 apio_env.alias("test", source=tests_targets, always_build=True)
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."""
399 apio_env = self.apio_env
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()
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")
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]
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)
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)
423 elif target == "report":
424 self._register_report_target(synth_srcs)
426 elif target == "graph":
427 self._register_graph_target(synth_srcs)
429 elif target == "sim":
430 self._register_sim_target(synth_srcs, test_srcs)
432 elif target == "test":
433 self._register_test_target(synth_srcs, test_srcs)
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)
438 else:
439 cerror(f"Unexpected scons target: {target}")
440 sys.exit(1)
442 # -- Note that so far we just registered builders and target.
443 # -- The actual execution is done by scons once this method returns.