Coverage for apio / scons / scons_handler.py: 91%
159 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 02:47 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-08 02:47 +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_apio_sim_testbench_info,
34 get_apio_test_testbenches_infos,
35 gtkwave_target,
36 report_action,
37 get_programmer_cmd,
38 TestbenchInfo,
39)
40from apio.common.apio_console import cerror, cout
42# -- Scons builders ids.
43SYNTH_BUILDER = "SYNTH_BUILDER"
44PNR_BUILDER = "PNR_BUILDER"
45BITSTREAM_BUILDER = "BITSTREAM_BUILDER"
46TESTBENCH_COMPILE_BUILDER = "TESTBENCH_COMPILE_BUILDER"
47TESTBENCH_RUN_BUILDER = "TESTBENCH_RUN_BUILDER"
48YOSYS_DOT_BUILDER = "YOSYS_DOT_BUILDER"
49GRAPHVIZ_RENDERER_BUILDER = "GRAPHVIZ_RENDERER_BUILDER"
50LINT_CONFIG_BUILDER = "LINT_CONFIG_BUILDER"
51LINT_BUILDER = "LINT_BUILDER"
54class SconsHandler:
55 """Base apio scons handler"""
57 def __init__(self, apio_env: ApioEnv, arch_plugin: PluginBase):
58 """Do not call directly, use SconsHandler.start()."""
59 self.apio_env = apio_env
60 self.arch_plugin = arch_plugin
62 @staticmethod
63 def start() -> None:
64 """This static method is called from SConstruct to create and
65 execute an SconsHandler."""
67 # -- Read the text of the scons params file.
68 params_path = Path(ARGUMENTS["params"])
69 with open(params_path, "r", encoding="utf8") as f:
70 proto_text = f.read()
72 # -- Parse the text into SconsParams object.
73 params: SconsParams = text_format.Parse(proto_text, SconsParams())
75 # -- Compare the params timestamp to the timestamp in the command.
76 timestamp = ARGUMENTS["timestamp"]
77 assert params.timestamp == timestamp
79 # -- If running on windows, apply the lib library workaround
80 if params.environment.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 apio_console.configure(
85 terminal_mode=params.environment.terminal_mode,
86 theme_name=params.environment.theme_name,
87 )
89 # -- Create the apio environment.
90 apio_env = ApioEnv(COMMAND_LINE_TARGETS, params)
92 # -- Select the plugin.
93 if params.arch == ICE40:
94 plugin = PluginIce40(apio_env)
95 elif params.arch == ECP5:
96 plugin = PluginEcp5(apio_env)
97 elif params.arch == GOWIN: 97 ↛ 100line 97 didn't jump to line 100 because the condition on line 97 was always true
98 plugin = PluginGowin(apio_env)
99 else:
100 cout(
101 f"Apio SConstruct dispatch error: unknown arch [{params.arch}]"
102 )
103 sys.exit(1)
105 # -- Create the handler.
106 scons_handler = SconsHandler(apio_env, plugin)
108 # -- Invoke the handler. This services the scons request.
109 scons_handler.execute()
111 def _register_common_targets(self, synth_srcs):
112 """Register the common synth, pnr, and bitstream operations which
113 are used by a few top level targets.
114 """
116 apio_env = self.apio_env
117 params = apio_env.params
118 plugin = self.arch_plugin
120 # -- Sanity check
121 assert apio_env.targeting_one_of("build", "upload", "report")
123 # -- Synth builder and target.
124 apio_env.builder(SYNTH_BUILDER, plugin.synth_builder())
126 synth_target = apio_env.builder_target(
127 builder_id=SYNTH_BUILDER,
128 target=apio_env.target,
129 sources=[synth_srcs],
130 always_build=(params.verbosity.all or params.verbosity.synth),
131 )
133 # -- Place-and-route builder and target
134 apio_env.builder(PNR_BUILDER, plugin.pnr_builder())
136 pnr_target = apio_env.builder_target(
137 builder_id=PNR_BUILDER,
138 target=apio_env.target,
139 sources=[synth_target, self.arch_plugin.constrain_file()],
140 always_build=(params.verbosity.all or params.verbosity.pnr),
141 )
143 # -- Bitstream builder builder and target
144 apio_env.builder(BITSTREAM_BUILDER, plugin.bitstream_builder())
146 apio_env.builder_target(
147 builder_id=BITSTREAM_BUILDER,
148 target=apio_env.target,
149 sources=pnr_target,
150 )
152 def _register_apio_build_target(self, synth_srcs):
153 """Register the 'build' target which creates the binary bitstream."""
154 apio_env = self.apio_env
155 params = apio_env.params
156 plugin = self.arch_plugin
158 # -- Sanity check
159 assert apio_env.targeting_one_of("build")
161 # -- Register the common targets for synth, pnr, and bitstream.
162 self._register_common_targets(synth_srcs)
164 # -- Top level "build" target.
165 apio_env.alias(
166 "build",
167 source=apio_env.target + plugin.plugin_info().bin_file_suffix,
168 always_build=(
169 params.verbosity.all
170 or params.verbosity.synth
171 or params.verbosity.pnr
172 ),
173 )
175 def _register_apio_upload_target(self, synth_srcs):
176 """Register the 'upload' target which upload the binary file
177 generated by the bitstream generator."""
179 apio_env = self.apio_env
180 plugin_info = self.arch_plugin.plugin_info()
182 # -- Sanity check
183 assert apio_env.targeting_one_of("upload")
185 # -- Register the common targets for synth, pnr, and bitstream.
186 self._register_common_targets(synth_srcs)
188 # -- Create the top level 'upload' target.
189 apio_env.alias(
190 "upload",
191 source=apio_env.target + plugin_info.bin_file_suffix,
192 action=get_programmer_cmd(apio_env),
193 always_build=True,
194 )
196 def _register_apio_report_target(self, synth_srcs):
197 """Registers the 'report' target which a report file from the
198 PNR generated .pnr file."""
199 apio_env = self.apio_env
200 params = apio_env.params
201 plugin_info = self.arch_plugin.plugin_info()
203 # -- Sanity check
204 assert apio_env.targeting_one_of("report")
206 # -- Register the common targets for synth, pnr, and bitstream.
207 self._register_common_targets(synth_srcs)
209 # -- Register the top level 'report' target.
210 apio_env.alias(
211 "report",
212 source=apio_env.target + ".pnr",
213 action=report_action(
214 plugin_info.clk_name_index, params.verbosity.pnr
215 ),
216 always_build=True,
217 )
219 def _register_apio_graph_target(
220 self,
221 synth_srcs,
222 ):
223 """Registers the 'graph' target which generates a .dot file using
224 yosys and renders it using graphviz."""
225 apio_env = self.apio_env
226 params = apio_env.params
227 plugin = self.arch_plugin
229 # -- Sanity check
230 assert apio_env.targeting_one_of("graph")
231 assert params.target.HasField("graph")
233 # -- Create the .dot generation builder and target.
234 apio_env.builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder())
236 dot_target = apio_env.builder_target(
237 builder_id=YOSYS_DOT_BUILDER,
238 target=apio_env.graph_target,
239 sources=synth_srcs,
240 always_build=True,
241 )
243 # -- Create the rendering builder and target.
244 apio_env.builder(
245 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder()
246 )
247 graphviz_target = apio_env.builder_target(
248 builder_id=GRAPHVIZ_RENDERER_BUILDER,
249 target=apio_env.graph_target,
250 sources=dot_target,
251 always_build=True,
252 )
254 # -- Create the top level "graph" target.
255 apio_env.alias(
256 "graph",
257 source=graphviz_target,
258 always_build=True,
259 )
261 def _register_apio_lint_target(self, synth_srcs, test_srcs):
262 """Registers the 'lint' target which creates a lint configuration file
263 and runs the linter."""
265 apio_env = self.apio_env
266 params = apio_env.params
267 plugin = self.arch_plugin
269 # -- Sanity check
270 assert apio_env.targeting_one_of("lint")
271 assert params.target.HasField("lint")
273 # -- Get lint params proto.
274 lint_params = params.target.lint
276 # -- Create the builder and target of the config file creation.
277 extra_dependencies = []
279 if not lint_params.novlt: 279 ↛ 292line 279 didn't jump to line 292 because the condition on line 279 was always true
280 # -- The auto generated verilator config file with supression
281 # -- of some librariy warnings is enable.
282 apio_env.builder(LINT_CONFIG_BUILDER, plugin.lint_config_builder())
284 lint_config_target = apio_env.builder_target(
285 builder_id=LINT_CONFIG_BUILDER,
286 target=apio_env.target,
287 sources=[],
288 )
289 extra_dependencies.append(lint_config_target)
291 # -- Create the builder and target the lint operation.
292 apio_env.builder(LINT_BUILDER, plugin.lint_builder())
294 lint_out_target = apio_env.builder_target(
295 builder_id=LINT_BUILDER,
296 target=apio_env.target,
297 sources=synth_srcs + test_srcs,
298 extra_dependencies=extra_dependencies,
299 )
301 # -- Create the top level "lint" target.
302 apio_env.alias(
303 "lint",
304 source=lint_out_target,
305 always_build=True,
306 )
308 def _register_apio_sim_target(self, synth_srcs, test_srcs):
309 """Registers the 'sim' targets which compiles and runs the
310 simulation of a testbench."""
312 apio_env = self.apio_env
313 params = apio_env.params
314 plugin = self.arch_plugin
316 # -- Sanity check
317 assert apio_env.targeting_one_of("sim")
318 assert params.target.HasField("sim")
320 # -- Get values.
321 sim_params: SimParams = params.target.sim
323 # -- Collect information for sim.
324 testbench_info: TestbenchInfo = get_apio_sim_testbench_info(
325 apio_env,
326 sim_params,
327 synth_srcs,
328 test_srcs,
329 )
331 # -- Compilation builder and target
333 apio_env.builder(
334 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
335 )
337 sim_out_target = apio_env.builder_target(
338 builder_id=TESTBENCH_COMPILE_BUILDER,
339 target=testbench_info.build_testbench_name,
340 sources=testbench_info.srcs,
341 always_build=sim_params.force_sim,
342 )
344 # -- Simulation builder and target..
346 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
348 sim_vcd_target = apio_env.builder_target(
349 builder_id=TESTBENCH_RUN_BUILDER,
350 target=testbench_info.build_testbench_name,
351 sources=[sim_out_target],
352 always_build=sim_params.force_sim,
353 )
355 # -- The top level "sim" target.
356 gtkwave_target(
357 apio_env,
358 "sim",
359 sim_vcd_target,
360 testbench_info,
361 sim_params,
362 params.apio_env_params.gtkwave_extra_options,
363 )
365 def _register_apio_test_target(self, synth_srcs, test_srcs):
366 """Registers 'test' target and its dependencies. Each testbench
367 is tested independently with its own set of sub-targets."""
369 apio_env = self.apio_env
370 params = apio_env.params
371 plugin = self.arch_plugin
373 # -- Sanity check
374 assert apio_env.targeting_one_of("test")
375 assert params.target.HasField("test")
377 # -- Collect the test related values.
378 test_params = params.target.test
379 testbenches_infos = get_apio_test_testbenches_infos(
380 apio_env,
381 test_params,
382 synth_srcs,
383 test_srcs,
384 )
386 # -- Create compilation and simulation targets.
387 apio_env.builder(
388 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
389 )
390 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
392 # -- Create targets for each testbench we are testing.
393 tests_targets = []
394 for testbench_info in testbenches_infos:
396 # -- Create the compilation target.
397 test_out_target = apio_env.builder_target(
398 builder_id=TESTBENCH_COMPILE_BUILDER,
399 target=testbench_info.build_testbench_name,
400 sources=testbench_info.srcs,
401 always_build=True,
402 )
404 # -- Create the simulation target.
405 test_vcd_target = apio_env.builder_target(
406 builder_id=TESTBENCH_RUN_BUILDER,
407 target=testbench_info.build_testbench_name,
408 sources=[test_out_target],
409 always_build=True,
410 )
412 # -- Append to the list of targets we need to execute.
413 tests_targets.append(test_vcd_target)
415 # -- The top level 'test' target.
416 apio_env.alias("test", source=tests_targets, always_build=True)
418 def execute(self):
419 """The entry point of the scons handler. It registers the builders
420 and targets for the selected command and scons executes in upon
421 return."""
423 apio_env = self.apio_env
425 # -- Collect the lists of the synthesizable files (e.g. "main.v") and a
426 # -- testbench files (e.g. "main_tb.v")
427 synth_srcs, test_srcs = get_project_source_files()
429 # -- Sanity check that we don't call the scons to do cleanup. This is
430 # -- handled directly by the 'apio clean' command.
431 assert not apio_env.scons_env.GetOption("clean")
433 # -- Get the target, we expect exactly one.
434 targets = apio_env.command_line_targets
435 assert len(targets) == 1, targets
436 target = targets[0]
438 # -- Dispatch by target.
439 # -- Not using python 'match' statement for compatibility with
440 # -- python 3.9.
441 if target == "build":
442 self._register_apio_build_target(synth_srcs)
444 elif target == "upload": 444 ↛ 445line 444 didn't jump to line 445 because the condition on line 444 was never true
445 self._register_apio_upload_target(synth_srcs)
447 elif target == "report":
448 self._register_apio_report_target(synth_srcs)
450 elif target == "graph":
451 self._register_apio_graph_target(synth_srcs)
453 elif target == "sim":
454 self._register_apio_sim_target(synth_srcs, test_srcs)
456 elif target == "test":
457 self._register_apio_test_target(synth_srcs, test_srcs)
459 elif target == "lint": 459 ↛ 463line 459 didn't jump to line 463 because the condition on line 459 was always true
460 self._register_apio_lint_target(synth_srcs, test_srcs)
462 else:
463 cerror(f"Unexpected scons target: {target}")
464 sys.exit(1)
466 # -- Note that so far we just registered builders and target.
467 # -- The actual execution is done by scons once this method returns.