Coverage for apio / scons / scons_handler.py: 91%
167 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 02:31 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-25 02:31 +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 # -- Determine target file. Normally it's the bitstream file but
165 # -- if building with nextpnr --gui flag we skip the packing step
166 # -- and stop after the nextpnr step.
167 if params.nextpnr_gui: 167 ↛ 169line 167 didn't jump to line 169 because the condition on line 167 was never true
168 # -- Target is the pnr output file.
169 target_file = (
170 apio_env.target + plugin.plugin_info().pnr_file_suffix
171 )
172 else:
173 # -- Target is the packager's output bitstream file.
174 target_file = (
175 apio_env.target + plugin.plugin_info().bitstream_file_suffix
176 )
178 # -- Top level "build" target.
179 apio_env.alias(
180 "build",
181 source=target_file,
182 always_build=(
183 params.verbosity.all
184 or params.verbosity.synth
185 or params.verbosity.pnr
186 ),
187 )
189 def _register_apio_upload_target(self, synth_srcs):
190 """Register the 'upload' target which upload the binary file
191 generated by the bitstream generator."""
193 apio_env = self.apio_env
194 plugin_info = self.arch_plugin.plugin_info()
196 # -- Sanity check
197 assert apio_env.targeting_one_of("upload")
199 # -- Register the common targets for synth, pnr, and bitstream.
200 self._register_common_targets(synth_srcs)
202 # -- Create the top level 'upload' target.
203 apio_env.alias(
204 "upload",
205 source=apio_env.target + plugin_info.bitstream_file_suffix,
206 action=get_programmer_cmd(apio_env),
207 always_build=True,
208 )
210 def _register_apio_report_target(self, synth_srcs):
211 """Registers the 'report' target which a report file from the
212 PNR generated .pnr file."""
213 apio_env = self.apio_env
214 params = apio_env.params
215 plugin_info = self.arch_plugin.plugin_info()
217 # -- Sanity check
218 assert apio_env.targeting_one_of("report")
220 # -- Register the common targets for synth, pnr, and bitstream.
221 self._register_common_targets(synth_srcs)
223 # -- Register the top level 'report' target.
224 apio_env.alias(
225 "report",
226 source=apio_env.target + ".pnr",
227 action=report_action(
228 plugin_info.clk_name_index, params.verbosity.pnr
229 ),
230 always_build=True,
231 )
233 def _register_apio_graph_target(
234 self,
235 synth_srcs,
236 ):
237 """Registers the 'graph' target which generates a .dot file using
238 yosys and renders it using graphviz."""
239 apio_env = self.apio_env
240 params = apio_env.params
241 plugin = self.arch_plugin
243 # -- Sanity check
244 assert apio_env.targeting_one_of("graph")
245 assert params.target.HasField("graph")
247 # -- Create the .dot generation builder and target.
248 apio_env.builder(YOSYS_DOT_BUILDER, plugin.yosys_dot_builder())
250 dot_target = apio_env.builder_target(
251 builder_id=YOSYS_DOT_BUILDER,
252 target=apio_env.graph_target,
253 sources=synth_srcs,
254 always_build=True,
255 )
257 # -- Create the rendering builder and target.
258 apio_env.builder(
259 GRAPHVIZ_RENDERER_BUILDER, plugin.graphviz_renderer_builder()
260 )
261 graphviz_target = apio_env.builder_target(
262 builder_id=GRAPHVIZ_RENDERER_BUILDER,
263 target=apio_env.graph_target,
264 sources=dot_target,
265 always_build=True,
266 )
268 # -- Create the top level "graph" target.
269 apio_env.alias(
270 "graph",
271 source=graphviz_target,
272 always_build=True,
273 )
275 def _register_apio_lint_target(self, synth_srcs, test_srcs):
276 """Registers the 'lint' target which creates a lint configuration file
277 and runs the linter."""
279 apio_env = self.apio_env
280 params = apio_env.params
281 plugin = self.arch_plugin
283 # -- Sanity check
284 assert apio_env.targeting_one_of("lint")
285 assert params.target.HasField("lint")
287 # -- Get lint params proto.
288 lint_params = params.target.lint
290 # -- Create the builder and target of the config file creation.
291 extra_dependencies = []
293 lint_whole_project = not lint_params.file_names
294 using_vlt = lint_whole_project and (not lint_params.novlt)
295 if using_vlt:
296 # -- The auto generated verilator config file with supression
297 # -- of some librariy warnings is enable.
298 apio_env.builder(LINT_CONFIG_BUILDER, plugin.lint_config_builder())
300 lint_config_target = apio_env.builder_target(
301 builder_id=LINT_CONFIG_BUILDER,
302 target=apio_env.target,
303 sources=[],
304 )
305 extra_dependencies.append(lint_config_target)
307 # -- Create the builder and target the lint operation.
308 apio_env.builder(LINT_BUILDER, plugin.lint_builder())
310 # -- Determine the files that will be linted. If specific files were
311 # -- not specified on the command line, we take all the source and
312 # -- testbench files in the project.
313 if lint_params.file_names:
314 files_to_lint = [
315 apio_env.scons_env.File(f) for f in lint_params.file_names
316 ]
317 else:
318 files_to_lint = synth_srcs + test_srcs
320 lint_out_target = apio_env.builder_target(
321 builder_id=LINT_BUILDER,
322 target=apio_env.target,
323 sources=files_to_lint,
324 extra_dependencies=extra_dependencies,
325 )
327 # -- Create the top level "lint" target.
328 apio_env.alias(
329 "lint",
330 source=lint_out_target,
331 always_build=True,
332 )
334 def _register_apio_sim_target(self, synth_srcs, test_srcs):
335 """Registers the 'sim' targets which compiles and runs the
336 simulation of a testbench."""
338 apio_env = self.apio_env
339 params = apio_env.params
340 plugin = self.arch_plugin
342 # -- Sanity check
343 assert apio_env.targeting_one_of("sim")
344 assert params.target.HasField("sim")
346 # -- Get values.
347 sim_params: SimParams = params.target.sim
349 # -- Collect information for sim.
350 testbench_info: TestbenchInfo = get_apio_sim_testbench_info(
351 apio_env,
352 sim_params,
353 synth_srcs,
354 test_srcs,
355 )
357 # -- Compilation builder and target
359 apio_env.builder(
360 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
361 )
363 sim_out_target = apio_env.builder_target(
364 builder_id=TESTBENCH_COMPILE_BUILDER,
365 target=testbench_info.build_testbench_name,
366 sources=testbench_info.srcs,
367 always_build=sim_params.force_sim,
368 )
370 # -- Simulation builder and target..
372 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
374 sim_vcd_target = apio_env.builder_target(
375 builder_id=TESTBENCH_RUN_BUILDER,
376 target=testbench_info.build_testbench_name,
377 sources=[sim_out_target],
378 always_build=sim_params.force_sim,
379 )
381 # -- The top level "sim" target.
382 gtkwave_target(
383 apio_env,
384 "sim",
385 sim_vcd_target,
386 testbench_info,
387 sim_params,
388 params.apio_env_params.gtkwave_extra_options,
389 )
391 def _register_apio_test_target(self, synth_srcs, test_srcs):
392 """Registers 'test' target and its dependencies. Each testbench
393 is tested independently with its own set of sub-targets."""
395 apio_env = self.apio_env
396 params = apio_env.params
397 plugin = self.arch_plugin
399 # -- Sanity check
400 assert apio_env.targeting_one_of("test")
401 assert params.target.HasField("test")
403 # -- Collect the test related values.
404 test_params = params.target.test
405 testbenches_infos = get_apio_test_testbenches_infos(
406 apio_env,
407 test_params,
408 synth_srcs,
409 test_srcs,
410 )
412 # -- Create compilation and simulation targets.
413 apio_env.builder(
414 TESTBENCH_COMPILE_BUILDER, plugin.testbench_compile_builder()
415 )
416 apio_env.builder(TESTBENCH_RUN_BUILDER, plugin.testbench_run_builder())
418 # -- Create targets for each testbench we are testing.
419 tests_targets = []
420 for testbench_info in testbenches_infos:
422 # -- Create the compilation target.
423 test_out_target = apio_env.builder_target(
424 builder_id=TESTBENCH_COMPILE_BUILDER,
425 target=testbench_info.build_testbench_name,
426 sources=testbench_info.srcs,
427 always_build=True,
428 )
430 # -- Create the simulation target.
431 test_vcd_target = apio_env.builder_target(
432 builder_id=TESTBENCH_RUN_BUILDER,
433 target=testbench_info.build_testbench_name,
434 sources=[test_out_target],
435 always_build=True,
436 )
438 # -- Append to the list of targets we need to execute.
439 tests_targets.append(test_vcd_target)
441 # -- The top level 'test' target.
442 apio_env.alias("test", source=tests_targets, always_build=True)
444 def execute(self):
445 """The entry point of the scons handler. It registers the builders
446 and targets for the selected command and scons executes in upon
447 return."""
449 apio_env = self.apio_env
451 # -- Collect the lists of the synthesizable files (e.g. "main.v") and a
452 # -- testbench files (e.g. "main_tb.v")
453 synth_srcs, test_srcs = get_project_source_files()
455 # -- Sanity check that we don't call the scons to do cleanup. This is
456 # -- handled directly by the 'apio clean' command.
457 assert not apio_env.scons_env.GetOption("clean")
459 # -- Get the target, we expect exactly one.
460 targets = apio_env.command_line_targets
461 assert len(targets) == 1, targets
462 target = targets[0]
464 # -- Dispatch by target.
465 # -- Not using python 'match' statement for compatibility with
466 # -- python 3.9.
467 if target == "build":
468 self._register_apio_build_target(synth_srcs)
470 elif target == "upload": 470 ↛ 471line 470 didn't jump to line 471 because the condition on line 470 was never true
471 self._register_apio_upload_target(synth_srcs)
473 elif target == "report":
474 self._register_apio_report_target(synth_srcs)
476 elif target == "graph":
477 self._register_apio_graph_target(synth_srcs)
479 elif target == "sim":
480 self._register_apio_sim_target(synth_srcs, test_srcs)
482 elif target == "test":
483 self._register_apio_test_target(synth_srcs, test_srcs)
485 elif target == "lint": 485 ↛ 489line 485 didn't jump to line 489 because the condition on line 485 was always true
486 self._register_apio_lint_target(synth_srcs, test_srcs)
488 else:
489 cerror(f"Unexpected scons target: {target}")
490 sys.exit(1)
492 # -- Note that so far we just registered builders and target.
493 # -- The actual execution is done by scons once this method returns.