Coverage for apio/managers/scons_manager.py: 87%
107 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""A manager class for to dispatch the Apio SCONS targets."""
11import os
12import sys
13import time
14import shutil
16from datetime import datetime
17from google.protobuf import text_format
18from apio.common.debug_util import is_debug
19from apio.common import apio_console
20from apio.common.apio_console import (
21 cout,
22 cstyle,
23 cunstyle,
24)
25from apio.common.apio_styles import SUCCESS, ERROR, EMPH3
26from apio.utils import util
27from apio.apio_context import ApioContext
28from apio.managers.scons_filter import SconsFilter
30# from apio.common.proto.stubs.apio_common_pb2 import ApioArch
31from apio.common.proto.stubs.apio_scons_pb2 import (
32 FORCE_PIPE,
33 FORCE_TERMINAL,
34 Verbosity,
35 Environment,
36 SconsParams,
37 TargetParams,
38 ApioEnvParams,
39 GraphParams,
40 LintParams,
41 SimParams,
42 ApioTestParams,
43 UploadParams,
44)
47class SConsManager:
48 """Class for managing the scons tools"""
50 def __init__(self, apio_ctx: ApioContext):
51 """Initialization."""
52 # -- Cache the apio context.
53 self.apio_ctx = apio_ctx
55 # -- Change to the project's folder.
56 os.chdir(apio_ctx.project_dir)
58 def graph(
59 self, graph_params: GraphParams, verbosity: Verbosity
60 ) -> int | None:
61 """Runs a scons subprocess with the 'graph' target. Returns process
62 exit code, 0 if ok."""
64 # -- Construct scons params with graph command info.
65 scons_params = self.construct_scons_params(
66 target_params=TargetParams(graph=graph_params),
67 verbosity=verbosity,
68 )
70 # -- Run the scons process.
71 return self._run_scons_subprocess("graph", scons_params=scons_params)
73 def lint(self, lint_params: LintParams) -> int | None:
74 """Runs a scons subprocess with the 'lint' target. Returns process
75 exit code, 0 if ok."""
77 # -- Construct scons params with graph command info.
78 scons_params = self.construct_scons_params(
79 target_params=TargetParams(lint=lint_params)
80 )
82 # -- Run the scons process.
83 return self._run_scons_subprocess("lint", scons_params=scons_params)
85 def sim(self, sim_params: SimParams) -> int | None:
86 """Runs a scons subprocess with the 'sim' target. Returns process
87 exit code, 0 if ok."""
89 # -- Construct scons params with graph command info.
90 scons_params = self.construct_scons_params(
91 target_params=TargetParams(sim=sim_params)
92 )
94 # -- Run the scons process.
95 return self._run_scons_subprocess("sim", scons_params=scons_params)
97 def test(self, test_params: ApioTestParams) -> int | None:
98 """Runs a scons subprocess with the 'test' target. Returns process
99 exit code, 0 if ok."""
101 # -- Construct scons params with graph command info.
102 scons_params = self.construct_scons_params(
103 target_params=TargetParams(test=test_params)
104 )
106 # -- Run the scons process.
107 return self._run_scons_subprocess("test", scons_params=scons_params)
109 def build(self, verbosity: Verbosity) -> int | None:
110 """Runs a scons subprocess with the 'build' target. Returns process
111 exit code, 0 if ok."""
113 # -- Construct the scons params object.
114 scons_params = self.construct_scons_params(
115 verbosity=verbosity,
116 )
118 # -- Run the scons process.
119 return self._run_scons_subprocess("build", scons_params=scons_params)
121 def report(self, verbosity: Verbosity) -> int | None:
122 """Runs a scons subprocess with the 'report' target. Returns process
123 exit code, 0 if ok."""
125 # -- Construct the scons params object.
126 scons_params = self.construct_scons_params(
127 verbosity=verbosity,
128 )
130 # -- Run the scons process.
131 return self._run_scons_subprocess("report", scons_params=scons_params)
133 def upload(self, upload_params: UploadParams) -> int | None:
134 """Runs a scons subprocess with the 'time' target. Returns process
135 exit code, 0 if ok.
136 """
138 # -- Construct the scons params.
139 scons_params = self.construct_scons_params(
140 target_params=TargetParams(upload=upload_params)
141 )
143 # -- Execute Scons for uploading!
144 exit_code = self._run_scons_subprocess(
145 "upload", scons_params=scons_params
146 )
148 return exit_code
150 def construct_scons_params(
151 self,
152 *,
153 target_params: TargetParams | None = None,
154 verbosity: Verbosity | None = None,
155 ) -> SconsParams:
156 """Populate and return the SconsParam proto to pass to the scons
157 process."""
159 # -- Create a shortcut.
160 apio_ctx = self.apio_ctx
162 # -- Create an empty proto object that will be populated.
163 result = SconsParams()
165 # -- Populate the timestamp. We use to to make sure scons reads the
166 # -- correct version of the scons.params file.
167 ts = datetime.now()
168 result.timestamp = ts.strftime("%d%H%M%S%f")[:-3]
170 # -- Get the project data. All commands that invoke scons are expected
171 # -- to be in a project context.
172 assert apio_ctx.has_project, "Scons encountered a missing project."
173 project = apio_ctx.project
175 # -- Get the project resources.
176 pr = apio_ctx.project_resources
178 # -- Get the fpga definition.
179 fpga_definition = pr.fpga_definition
180 # result.arch = fpga_definition.arch
182 # -- Populate the the 'fpga_definition' field.
183 result.fpga_definition.CopyFrom(fpga_definition)
185 # -- Populate the optional Verbosity params.
186 if verbosity:
187 result.verbosity.MergeFrom(verbosity)
188 assert result.verbosity.IsInitialized(), result
190 # -- Populate the Environment params.
191 env_consts = apio_ctx.config.platform_env.define_consts
192 assert "YOSYS_LIB" in env_consts, env_consts
193 assert "TRELLIS" in env_consts, env_consts
194 assert "PRJXRAY_DB_DIR" in env_consts, env_consts
196 # -- Mutations that scons should apply to the env of teh tools it
197 # -- invokes.
198 tools_env_mutations = apio_ctx.get_env_mutations_for_subprocess(
199 include_apio_packages=True
200 )
202 result.environment.MergeFrom(
203 Environment(
204 platform_id=apio_ctx.platform.id,
205 terminal_mode=(
206 FORCE_TERMINAL
207 if apio_console.is_terminal()
208 else FORCE_PIPE
209 ),
210 theme_name=apio_console.current_theme_name(),
211 yosys_path=env_consts["YOSYS_LIB"],
212 trellis_path=env_consts["TRELLIS"],
213 scons_shell_id=apio_ctx.scons_shell_id(),
214 xilinx_prjxray_db_path=env_consts["PRJXRAY_DB_DIR"],
215 tools_env_mutations=tools_env_mutations,
216 )
217 )
218 assert result.environment.IsInitialized(), result
220 # -- Populate the Project params.
221 result.apio_env_params.MergeFrom(
222 ApioEnvParams(
223 env_name=apio_ctx.project.env_name,
224 board_id=pr.board_id,
225 top_module=project.get_str_option("top-module"),
226 defines=apio_ctx.project.get_list_option(
227 "defines", default=[]
228 ),
229 yosys_extra_options=apio_ctx.project.get_list_option(
230 "yosys-extra-options", None
231 ),
232 nextpnr_extra_options=apio_ctx.project.get_list_option(
233 "nextpnr-extra-options", None
234 ),
235 gtkwave_extra_options=apio_ctx.project.get_list_option(
236 "gtkwave-extra-options", None
237 ),
238 verilator_extra_options=apio_ctx.project.get_list_option(
239 "verilator-extra-options", None
240 ),
241 constraint_file=apio_ctx.project.get_str_option(
242 "constraint-file", None
243 ),
244 )
245 )
246 assert result.apio_env_params.IsInitialized(), result
248 # -- Populate the optional command specific params.
249 if target_params:
250 result.target.MergeFrom(target_params)
251 assert result.target.IsInitialized(), result
253 # -- All done.
254 assert result.IsInitialized(), result
255 return result
257 def _run_scons_subprocess(
258 self, scons_target: str, *, scons_params: SconsParams
259 ) -> int | None:
260 """Invoke an scons subprocess."""
262 # pylint: disable=too-many-locals
264 # -- Create a shortcut.
265 apio_ctx = self.apio_ctx
267 # -- Pass to the scons process the name of the sconstruct file it
268 # -- should use.
269 scons_dir = util.get_path_in_apio_package("scons")
270 scons_file_path = scons_dir / "SConstruct"
271 variables = ["-f", f"{scons_file_path}"]
273 # -- Pass the path to the proto params file. The path is relative
274 # -- to the project root.
275 params_file_path = apio_ctx.env_build_path / "scons.params"
276 variables += [f"params={str(params_file_path)}"]
278 # -- Pass to the scons process the timestamp of the scons params we
279 # -- pass via a file. This is for verification purposes only.
280 variables += [f"timestamp={scons_params.timestamp}"]
282 if is_debug(1): 282 ↛ 283line 282 didn't jump to line 283 because the condition on line 282 was never true
283 cout("\nSCONS CALL:", style=EMPH3)
284 cout(f"* target: {scons_target}")
285 cout(f"* variables: {variables}")
286 cout(f"* scons params: \n{scons_params}")
287 cout()
289 # -- Get the terminal width (typically 80)
290 terminal_width, _ = shutil.get_terminal_size()
292 # -- Read the time (for measuring how long does it take
293 # -- to execute the apio command)
294 start_time = time.time()
296 # -- Subtracting 1 to avoid line overflow on windows, Observed with
297 # -- Windows 10 and cmd.exe shell.
298 if apio_ctx.is_windows: 298 ↛ 299line 298 didn't jump to line 299 because the condition on line 298 was never true
299 terminal_width -= 1
301 # -- Print a horizontal line
302 cout("-" * terminal_width)
304 # -- Create the scons debug options. See details at
305 # -- https://scons.org/doc/2.4.1/HTML/scons-man.html
306 debug_options = (
307 ["--debug=explain,prepare,stacktrace", "--tree=all"]
308 if is_debug(1)
309 else []
310 )
312 # -- Construct the scons command line.
313 # --
314 # -- sys.executable is resolved to the full path of the python
315 # -- interpreter or to apio if running from a pyinstall setup.
316 # -- See https://github.com/orgs/pyinstaller/discussions/9023 for more
317 # -- information.
318 # --
319 # -- We use exec -m SCons instead of scones also for non pyinstaller
320 # -- deployment in case the scons binary is not on the PATH.
321 cmd = (
322 [sys.executable, "-m", "apio", "--scons"]
323 + ["-Q", scons_target]
324 + debug_options
325 + variables
326 )
328 # -- An output filter that manipulates the scons stdout/err lines as
329 # -- needed and write them to stdout.
330 scons_filter = SconsFilter(
331 colors_enabled=apio_console.is_colors_enabled()
332 )
334 # -- Write the scons parameters to a temp file in the build
335 # -- directory. It will be cleaned up as part of 'apio cleanup'.
336 # -- At this point, the project is the current directory, even if
337 # -- the command used the --project-dir option.
338 os.makedirs(apio_ctx.env_build_path, exist_ok=True)
339 with open(params_file_path, "w", encoding="utf8") as f:
340 f.write(text_format.MessageToString(scons_params))
342 if is_debug(1): 342 ↛ 343line 342 didn't jump to line 343 because the condition on line 342 was never true
343 cout(f"\nFull scons command: {cmd}\n\n")
345 # -- Execute the scons builder.
346 # --
347 # -- Note that the env settings for the apio packages is passed to
348 # -- the scons process in the params rather than setting them directly
349 # -- to the scons subprocess itself. The scons subprocess will apply
350 # -- those env mutations to the tools it will dispatch.
351 result = util.exec_command(
352 cmd,
353 stdout=util.AsyncPipe(scons_filter.on_stdout_line),
354 stderr=util.AsyncPipe(scons_filter.on_stderr_line),
355 env=apio_ctx.get_env_for_subprocess(include_apio_packages=False),
356 )
358 # -- Is there an error? True/False
359 is_error = result.exit_code != 0
361 # -- Calculate the time it took to execute the command
362 duration = time.time() - start_time
364 # -- Determine status message
365 if is_error: 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true
366 styled_status = cstyle("ERROR", style=ERROR)
367 else:
368 styled_status = cstyle("SUCCESS", style=SUCCESS)
370 # -- Determine the summary text
371 summary = f"Took {duration:.2f} seconds"
373 # -- Construct the entire message.
374 styled_msg = f" [{styled_status}] {summary} "
375 msg_len = len(cunstyle(styled_msg))
377 # -- Determine the lengths of the paddings before and after
378 # -- the message. Should be correct for odd and even terminal
379 # -- widths.
380 pad1_len = (terminal_width - msg_len) // 2
381 pad2_len = terminal_width - pad1_len - msg_len
383 # -- Print the entire line.
384 cout(f"{'=' * pad1_len}{styled_msg}{'=' * pad2_len}")
386 # -- Return the exit code
387 return result.exit_code