Coverage for apio / scons / plugin_util.py: 83%
272 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
10"""Helper functions for apio scons plugins."""
13from glob import glob
14import sys
15import os
16import re
17import json
18import subprocess
19from dataclasses import dataclass
20from pathlib import Path
21from typing import List, Dict, Optional, Union, Callable
22from rich.table import Table
23from rich import box
24from SCons import Scanner
25from SCons.Builder import Builder
26from SCons.Action import FunctionAction, Action
27from SCons.Node.FS import File
28from SCons.Script.SConscript import SConsEnvironment
29from SCons.Node import NodeList
30from SCons.Node.Alias import Alias
31from apio.scons.apio_env import ApioEnv
32from apio.common.proto.apio_pb2 import SimParams
33from apio.common.common_util import (
34 PROJECT_BUILD_PATH,
35 has_testbench_name,
36 is_source_file,
37)
38from apio.common.apio_console import cout, cerror, cwarning, ctable
39from apio.common.apio_styles import INFO, BORDER, EMPH1, EMPH2, EMPH3
42TESTBENCH_HINT = "Testbench file names must end with '_tb.v' or '_tb.sv'."
45def map_params(params: Optional[List[Union[str, Path]]], fmt: str) -> str:
46 """A common function construct a command string snippet from a list
47 of arguments. The functon does the following:
48 1. If params arg is None replace it with []
49 2. Drops empty or white space only items.
50 3. Maps the items using the format string which contains exactly one
51 placeholder {}.
52 4. Joins the items with a white space char.
54 For examples, see the unit test at test_scons_util.py.
55 """
56 # None designates empty list. Avoiding the pylint non safe default
57 # warning.
58 if params is None:
59 params = []
61 # Convert parmas to stripped strings.
62 params = [str(x).strip() for x in params]
64 # Drop the empty params and map the rest.
65 mapped_params = [fmt.format(x) for x in params if x]
67 # Join using a single space.
68 return " ".join(mapped_params)
71def get_constraint_file(apio_env: ApioEnv, file_ext: str) -> str:
72 """Returns the name of the constraint file to use.
74 env is the sconstruction environment.
76 file_ext is a string with the constrained file extension.
77 E.g. ".pcf" for ice40.
79 Returns the file name if found or exit with an error otherwise.
80 """
82 # -- If the user specified a 'constraint-file' in apio.ini then use it.
83 user_specified = apio_env.params.apio_env_params.constraint_file
85 if user_specified:
86 path = Path(user_specified)
87 # -- Path should be relative.
88 if path.is_absolute(): 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 cerror(f"Constraint file path is not relative: {user_specified}")
90 sys.exit(1)
91 # -- Constrain file extension should match the architecture.
92 if path.suffix != file_ext:
93 cerror(
94 f"Constraint file should have the extension '{file_ext}': "
95 f"{user_specified}."
96 )
97 sys.exit(1)
98 # -- File should not be under _build
99 if PROJECT_BUILD_PATH in path.parents:
100 cerror(
101 f"Constraint file should not be under {PROJECT_BUILD_PATH}: "
102 f"{user_specified}."
103 )
104 sys.exit(1)
105 # -- Path should not contain '..' to avoid traveling outside of the
106 # -- project and coming back.
107 for part in path.parts:
108 if part == "..":
109 cerror(
110 f"Constraint file path should not contain '..': "
111 f"{user_specified}."
112 )
113 sys.exit(1)
115 # -- Constrain file looks good.
116 return user_specified
118 # -- No user specified constraint file, we will try to look for it
119 # -- in the project tree.
120 glob_files: List[str] = glob(f"**/*{file_ext}", recursive=True)
122 # -- Exclude files that are under _build
123 filtered_files: List[str] = [
124 f for f in glob_files if PROJECT_BUILD_PATH not in Path(f).parents
125 ]
127 # -- Handle by file count.
128 n = len(filtered_files)
130 # -- Case 1: No matching constrain files.
131 if n == 0:
132 cerror(f"No constraint file '*{file_ext}' found.")
133 sys.exit(1)
135 # -- Case 2: Exactly one constrain file found.
136 if n == 1:
137 result = str(filtered_files[0])
138 return result
140 # -- Case 3: Multiple matching constrain files.
141 cerror(
142 f"Found {n} constraint files '*{file_ext}' "
143 "in the project tree, which one to use?"
144 )
145 cout(
146 "Use the apio.ini constraint-file option to specify the desired file.",
147 style=INFO,
148 )
149 sys.exit(1)
152def verilog_src_scanner(apio_env: ApioEnv) -> Scanner.Base:
153 """Creates and returns a scons Scanner object for scanning verilog
154 files for dependencies.
155 """
156 # A Regex to icestudio propriaetry references for *.list files.
157 # Example:
158 # Text: ' parameter v771499 = "v771499.list"'
159 # Captures: 'v771499.list'
160 icestudio_list_re = re.compile(r"[\n|\s][^\/]?\"(.*\.list?)\"", re.M)
162 # A regex to match a verilog include directive.
163 # Example
164 # Text: `include "apio_testing.vh"
165 # Capture: 'apio_testing.vh'
166 verilog_include_re = re.compile(r'`\s*include\s+["]([^"]+)["]', re.M)
168 # A regex for inclusion via $readmemh()
169 # Example
170 # Test: '$readmemh("my_data.hex", State_buff);'
171 # Capture: 'my_data.hex'
172 readmemh_reference_re = re.compile(
173 r"\$readmemh\([\'\"]([^\'\"]+)[\'\"]", re.M
174 )
176 # -- List of required and optional files that may require a rebuild if
177 # -- changed.
178 core_dependencies = [
179 "apio.ini",
180 "boards.jsonc",
181 "fpgas.jsonc",
182 "programmers.jsonc",
183 ]
185 def verilog_src_scanner_func(
186 file_node: File, env: SConsEnvironment, ignored_path
187 ) -> List[str]:
188 """Given a [System]Verilog file, scan it and return a list of
189 references to other files it depends on. It's not require to report
190 dependency on another source file in the project since scons loads
191 anyway all the source files in the project.
193 Returns a list of files. Dependencies that don't have an existing
194 file are ignored and not returned. This is to avoid references in
195 commented out code to break scons dependencies.
196 """
197 _ = env # Unused
199 # Sanity check. Should be called only to scan verilog files. If
200 # this fails, this is a programming error rather than a user error.
201 assert is_source_file(
202 file_node.name
203 ), f"Not a src file: {file_node.name}"
205 # Get the directory of the file, relative to the project root which is
206 # the current working directory. This value is equals to "." if the
207 # file is in the project root.
208 file_dir: str = file_node.get_dir().get_path()
210 # Prepare an empty set of dependencies.
211 candidates_raw_set = set()
213 # Read the file. This returns [] if the file doesn't exist.
214 file_content = file_node.get_text_contents()
216 # Get verilog includes references.
217 candidates_raw_set.update(verilog_include_re.findall(file_content))
219 # Get $readmemh() function references.
220 candidates_raw_set.update(readmemh_reference_re.findall(file_content))
222 # Get IceStudio references.
223 candidates_raw_set.update(icestudio_list_re.findall(file_content))
225 # Since we don't know if the dependency's path is relative to the file
226 # location or the project root, we try both. We prefer to have high
227 # recall of dependencies of high precision, risking at most unnecessary
228 # rebuilds.
229 candidates_set = candidates_raw_set.copy()
230 # If the file is not in the project dir, add a dependency also relative
231 # to the project dir.
232 if file_dir != ".":
233 for raw_candidate in candidates_raw_set:
234 candidate: str = os.path.join(file_dir, raw_candidate)
235 candidates_set.add(candidate)
237 # Add the core dependencies. They are always relative to the project
238 # root.
239 candidates_set.update(core_dependencies)
241 # Filter out candidates that don't have a matching files to prevert
242 # breaking the build. This handle for example the case where the
243 # file references is in a comment or non reachable code.
244 # See also https://stackoverflow.com/q/79302552/15038713
245 dependencies = []
246 for dependency in candidates_set:
247 if Path(dependency).exists():
248 dependencies.append(dependency)
249 elif apio_env.is_debug(1): 249 ↛ 250line 249 didn't jump to line 250 because the condition on line 249 was never true
250 cout(
251 f"Dependency candidate {dependency} does not exist, "
252 "dropping."
253 )
255 # Sort the strings for determinism.
256 dependencies = sorted(list(dependencies))
258 # Debug info.
259 if apio_env.is_debug(1): 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true
260 cout(f"Dependencies of {file_node}:", style=EMPH2)
261 for dependency in dependencies:
262 cout(f" {dependency}", style=EMPH2)
264 # All done
265 return apio_env.scons_env.File(dependencies)
267 return apio_env.scons_env.Scanner(function=verilog_src_scanner_func)
270def verilator_lint_action(
271 apio_env: ApioEnv,
272 *,
273 extra_params: List[str] = None,
274 lib_dirs: List[Path] = None,
275 lib_files: List[Path] = None,
276) -> List[
277 Callable[
278 [
279 List[File],
280 List[Alias],
281 SConsEnvironment,
282 ],
283 None,
284 ]
285 | str,
286]:
287 """Construct an verilator scons action.
288 * extra_params: Optional additional arguments.
289 * libs_dirs: Optional directories for include search.
290 * lib_files: Optional additional files to include.
291 Returns an action in a form of a list with two steps, a function to call
292 and a string command.
293 """
295 # -- Sanity checks
296 assert apio_env.targeting("lint")
297 assert apio_env.params.target.HasField("lint")
299 # -- Keep short references.
300 params = apio_env.params
301 lint_params = params.target.lint
303 # -- Determine top module.
304 top_module = (
305 lint_params.top_module
306 if lint_params.top_module
307 else params.apio_env_params.top_module
308 )
310 # -- Construct the action
311 action = (
312 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
313 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 "
314 "{0} {1} {2} {3} {4} {5} {6} {7} {8} {9} $SOURCES"
315 ).format(
316 "-Wall" if lint_params.verilator_all else "",
317 "-Wno-style" if lint_params.verilator_no_style else "",
318 map_params(lint_params.verilator_no_warns, "-Wno-{}"),
319 map_params(lint_params.verilator_warns, "-Wwarn-{}"),
320 f"--top-module {top_module}",
321 get_define_flags(apio_env),
322 map_params(extra_params, "{}"),
323 map_params(lib_dirs, '-I"{}"'),
324 apio_env.target + ".vlt",
325 map_params(lib_files, '"{}"'),
326 )
328 return [source_files_issue_scanner_action(), action]
331@dataclass(frozen=True)
332class SimulationConfig:
333 """Simulation parameters, for sim and test commands."""
335 testbench_name: str # The testbench name, without the 'v' suffix.
336 build_testbench_name: str # testbench_name prefixed by build dir.
337 srcs: List[str] # List of source files to compile.
340def detached_action(api_env: ApioEnv, cmd: List[str]) -> Action:
341 """
342 Launch the given command, given as a list of tokens, in a detached
343 (non blocking) mode.
344 """
346 def action_func(
347 target: List[Alias], source: List[File], env: SConsEnvironment
348 ):
349 """A call back function to perform the detached command invocation."""
351 # -- Make the linter happy
352 # pylint: disable=consider-using-with
353 _ = (target, source, env)
355 # -- NOTE: To debug these Popen operations, comment out the stdout=
356 # -- and stderr= lines to see the output and error messages from the
357 # -- commands.
359 # -- Handle the case of Window.
360 if api_env.is_windows:
361 creationflags = (
362 subprocess.DETACHED_PROCESS
363 | subprocess.CREATE_NEW_PROCESS_GROUP
364 )
365 subprocess.Popen(
366 cmd,
367 creationflags=creationflags,
368 stdout=subprocess.DEVNULL,
369 stderr=subprocess.DEVNULL,
370 close_fds=True,
371 shell=False,
372 )
373 return 0
375 # -- Handle the rest (macOS and Linux)
376 subprocess.Popen(
377 cmd,
378 stdout=subprocess.DEVNULL,
379 stderr=subprocess.DEVNULL,
380 close_fds=True,
381 start_new_session=True,
382 shell=False,
383 )
384 return 0
386 # -- Create the command display string that will be shown to the user.
387 cmd_str: str = subprocess.list2cmdline(cmd)
388 display_str: str = "[detached] " + cmd_str
390 # -- Create the action and return.
391 action = Action(action_func, display_str)
392 return action
395def gtkwave_target(
396 api_env: ApioEnv,
397 name: str,
398 vcd_file_target: NodeList,
399 sim_config: SimulationConfig,
400 sim_params: SimParams,
401) -> List[Alias]:
402 """Construct a target to launch the QTWave signal viewer.
403 vcd_file_target is the simulator target that generated the vcd file
404 with the signals. Returns the new targets.
405 """
407 # -- Construct the list of actions.
408 actions = []
410 if sim_params.no_gtkwave: 410 ↛ 426line 410 didn't jump to line 426 because the condition on line 410 was always true
411 # -- User asked to skip gtkwave. The '@' suppresses the printing
412 # -- of the echo command itself.
413 actions.append(
414 "@echo 'Flag --no-gtkwave was found, skipping GTKWave.'"
415 )
417 else:
418 # -- Normal pase, invoking gtkwave.
420 # -- On windows we need to setup the cache. This could be done once
421 # -- when the oss-cad-suite is installed but since we currently don't
422 # -- have a package setup mechanism, we do it here on each invocation.
423 # -- The time penalty is negligible.
424 # -- With the stock oss-cad-suite windows package, this is done in the
425 # -- environment.bat script.
426 if api_env.is_windows:
427 actions.append("gdk-pixbuf-query-loaders --update-cache")
429 # -- The actual wave viewer command.
430 gtkwave_cmd = [
431 "gtkwave",
432 "--rcvar",
433 "splash_disable on",
434 "--rcvar",
435 "do_initial_zoom_fit 1",
436 str(vcd_file_target[0]),
437 sim_config.testbench_name + ".gtkw",
438 ]
440 # -- Handle the case where gtkwave is run as a detached app, not
441 # -- waiting for it to close and not showing its output.
442 if sim_params.detach_gtkwave:
443 gtkwave_action = detached_action(api_env, gtkwave_cmd)
444 else:
445 gtkwave_action = subprocess.list2cmdline(gtkwave_cmd)
447 actions.append(gtkwave_action)
449 # -- Define a target with the action(s) we created.
450 target = api_env.alias(
451 name,
452 source=vcd_file_target,
453 action=actions,
454 always_build=True,
455 )
457 return target
460def check_valid_testbench_name(testbench: str) -> None:
461 """Check if a testbench name is valid. If not, print an error message
462 and exit."""
463 if not is_source_file(testbench) or not has_testbench_name(testbench): 463 ↛ 464line 463 didn't jump to line 464 because the condition on line 463 was never true
464 cerror(f"'{testbench}' is not a valid testbench file name.")
465 cout(TESTBENCH_HINT, style=INFO)
466 sys.exit(1)
469def get_sim_config(
470 apio_env: ApioEnv,
471 testbench: str,
472 synth_srcs: List[str],
473 test_srcs: List[str],
474) -> SimulationConfig:
475 """Returns a SimulationConfig for a sim command. 'testbench' is
476 an optional testbench file name. 'synth_srcs' and 'test_srcs' are the
477 all the project's synth and testbench files found in the project as
478 returned by get_project_source_files()."""
480 # -- Handle the testbench file selection. The end result is a single
481 # -- testbench file name in testbench that we simulate, or a fatal error.
482 if testbench:
483 # -- Case 1 - Testbench file name is specified in the command or
484 # -- apio.ini. Fatal error if invalid.
485 check_valid_testbench_name(testbench)
486 elif len(test_srcs) == 0: 486 ↛ 489line 486 didn't jump to line 489 because the condition on line 486 was never true
487 # -- Case 2 Testbench name was not specified and no testbench files
488 # -- were found in the project.
489 cerror("No testbench files found in the project.")
490 cout(TESTBENCH_HINT, style=INFO)
491 sys.exit(1)
492 elif len(test_srcs) == 1: 492 ↛ 500line 492 didn't jump to line 500 because the condition on line 492 was always true
493 # -- Case 3 Testbench name was not specified but there is exactly
494 # -- one in the project.
495 testbench = test_srcs[0]
496 cout(f"Found testbench file {testbench}", style=EMPH1)
497 else:
498 # -- Case 4 Testbench name was not specified and there are multiple
499 # -- testbench files in the project.
500 cerror("Multiple testbench files found in the project.")
501 cout(
502 "Please specify the testbench file name in the command "
503 "or in apio.ini 'default-testbench' option.",
504 style=INFO,
505 )
506 sys.exit(1)
508 # -- This should not happen. If it does, it's a programming error.
509 assert testbench, "get_sim_config(): Missing testbench file name"
511 # -- Construct a SimulationParams with all the synth files + the
512 # -- testbench file.
513 testbench_name = basename(testbench)
514 build_testbench_name = str(apio_env.env_build_path / testbench_name)
515 srcs = synth_srcs + [testbench]
516 return SimulationConfig(testbench_name, build_testbench_name, srcs)
519def get_tests_configs(
520 apio_env: ApioEnv,
521 testbench: str,
522 synth_srcs: List[str],
523 test_srcs: list[str],
524) -> List[SimulationConfig]:
525 """Return a list of SimulationConfigs for each of the testbenches that
526 need to be run for a 'apio test' command. If testbench is empty,
527 all the testbenches in test_srcs will be tested. Otherwise, only the
528 testbench in testbench will be tested. synth_srcs and test_srcs are
529 source and test file lists as returned by get_project_source_files()."""
530 # List of testbenches to be tested.
532 # -- Handle the testbench files selection. The end result is a list of one
533 # -- or more testbench file names in testbenches that we test.
534 if testbench:
535 # -- Case 1 - a testbench file name is specified in the command or
536 # -- apio.ini. Fatal error if invalid.
537 check_valid_testbench_name(testbench)
538 testbenches = [testbench]
539 elif len(test_srcs) == 0: 539 ↛ 542line 539 didn't jump to line 542 because the condition on line 539 was never true
540 # -- Case 2 - Testbench file name was not specified and there are no
541 # -- testbench files in the project.
542 cerror("No testbench files found in the project.")
543 cout(TESTBENCH_HINT, style=INFO)
544 sys.exit(1)
545 else:
546 # -- Case 3 - Testbench file name was not specified but there are one
547 # -- or more testbench files in the project.
548 testbenches = test_srcs
550 # -- If this fails, it's a programming error.
551 assert testbenches, "get_tests_configs(): no testbenches"
553 # Construct a config for each testbench.
554 configs = []
555 for tb in testbenches:
556 testbench_name = basename(tb)
557 build_testbench_name = str(apio_env.env_build_path / testbench_name)
558 srcs = synth_srcs + [tb]
559 configs.append(
560 SimulationConfig(testbench_name, build_testbench_name, srcs)
561 )
563 return configs
566def announce_testbench_action() -> FunctionAction:
567 """Returns an action that prints a title with the testbench name."""
569 def announce_testbench(
570 target: List[Alias],
571 source: List[File],
572 env: SConsEnvironment,
573 ):
574 """The action function."""
575 _ = (target, env) # Unused
577 # -- We expect to find exactly one testbench.
578 testbenches = [
579 file
580 for file in source
581 if (is_source_file(file.name) and has_testbench_name(file.name))
582 ]
583 assert len(testbenches) == 1, testbenches
585 # -- Announce it.
586 cout()
587 cout(f"Testbench {testbenches[0]}", style=EMPH3)
589 # -- Run the action but don't announce the action.
590 return Action(announce_testbench, strfunction=None)
593def source_files_issue_scanner_action() -> FunctionAction:
594 """Returns a SCons action that scans the source files and print
595 error or warning messages about issues it finds."""
597 # A regex to identify "$dumpfile(" in testbenches.
598 testbench_dumpfile_re = re.compile(r"[$]dumpfile\s*[(]")
600 # A regex to identify "INTERACTIVE_SIM" in testbenches
601 interactive_sim_re = re.compile(r"INTERACTIVE_SIM")
603 def report_source_files_issues(
604 target: List[Alias],
605 source: List[File],
606 env: SConsEnvironment,
607 ):
608 """The scanner function."""
610 _ = (target, env) # Unused
612 for file in source:
614 # -- For now we report issues only in testbenches so skip
615 # -- otherwise.
616 if not is_source_file(file.name) or not has_testbench_name(
617 file.name
618 ):
619 continue
621 # -- Read the testbench file text.
622 file_text = file.get_text_contents()
624 # -- if contains $dumpfile, print a warning.
625 if testbench_dumpfile_re.findall(file_text): 625 ↛ 626line 625 didn't jump to line 626 because the condition on line 625 was never true
626 cwarning("Avoid using $dumpfile() in Apio testbenches.")
628 # -- if contains $dumpfile, print a warning.
629 if interactive_sim_re.findall(file_text): 629 ↛ 630line 629 didn't jump to line 630 because the condition on line 629 was never true
630 cwarning(
631 "The Apio macro `INTERACTIVE_SIM is deprecated. "
632 "Use `APIO_SIM (0 or 1) instead."
633 )
635 # -- Run the action but don't announce the action. We will print
636 # -- ourselves in report_source_files_issues.
637 return Action(report_source_files_issues, strfunction=None)
640def _print_pnr_utilization_report(report: Dict[str, any]):
641 table = Table(
642 show_header=True,
643 show_lines=False,
644 box=box.SQUARE,
645 border_style=BORDER,
646 title="FPGA Resource Utilization",
647 title_justify="left",
648 padding=(0, 2),
649 )
651 # -- Add columns.
652 table.add_column("RESOURCE", no_wrap=True)
653 table.add_column("USED", no_wrap=True, justify="right")
654 table.add_column("TOTAL", no_wrap=True, justify="right")
655 table.add_column("UTIL.", no_wrap=True, justify="right")
657 # -- Add rows
658 utilization = report["utilization"]
659 for resource, vals in utilization.items():
660 used = vals["used"]
661 used_str = f"{used} " if used else ""
662 available = vals["available"]
663 available_str = f"{available} "
664 percents = int(100 * used / available)
665 percents_str = f"{percents}% " if used else ""
666 style = EMPH3 if used > 0 else None
667 table.add_row(
668 resource, used_str, available_str, percents_str, style=style
669 )
671 # -- Render the table
672 cout()
673 ctable(table)
676def _maybe_print_pnr_clocks_report(
677 report: Dict[str, any], clk_name_index: int
678) -> bool:
679 clocks = report["fmax"]
680 if len(clocks) == 0:
681 return False
683 table = Table(
684 show_header=True,
685 show_lines=True,
686 box=box.SQUARE,
687 border_style=BORDER,
688 title="Clock Information",
689 title_justify="left",
690 padding=(0, 2),
691 )
693 # -- Add columns
694 table.add_column("CLOCK", no_wrap=True)
695 table.add_column(
696 "MAX SPEED [Mhz]", no_wrap=True, justify="right", style=EMPH3
697 )
699 # -- Add rows.
700 clocks = report["fmax"]
701 for clk_net, vals in clocks.items():
702 # -- Extract clock name from the net name.
703 clk_signal = clk_net.split("$")[clk_name_index]
704 # -- Extract speed
705 max_mhz = vals["achieved"]
706 # -- Add row.
707 table.add_row(clk_signal, f"{max_mhz:.2f}")
709 # -- Render the table
710 cout()
711 ctable(table)
712 return True
715def _print_pnr_report(
716 json_txt: str,
717 clk_name_index: int,
718 verbose: bool,
719) -> None:
720 """Accepts the text of the pnr json report and prints it in
721 a user friendly way. Used by the 'apio report' command."""
722 # -- Parse the json text into a tree of dicts.
723 report: Dict[str, any] = json.loads(json_txt)
725 # -- Print the utilization table.
726 _print_pnr_utilization_report(report)
728 # -- Print the optional clocks table.
729 clock_report_printed = _maybe_print_pnr_clocks_report(
730 report, clk_name_index
731 )
733 # -- Print summary.
734 cout("")
735 if not clock_report_printed:
736 cout("No clocks were found in the design.", style=INFO)
737 if not verbose: 737 ↛ exitline 737 didn't return from function '_print_pnr_report' because the condition on line 737 was always true
738 cout("Run 'apio report --verbose' for more details.", style=INFO)
741def report_action(clk_name_index: int, verbose: bool) -> FunctionAction:
742 """Returns a SCons action to format and print the PNR reort from the
743 PNR json report file. Used by the 'apio report' command.
744 'script_id' identifies the calling SConstruct script and 'verbose'
745 indicates if the --verbose flag was invoked."""
747 def print_pnr_report(
748 target: List[Alias],
749 source: List[File],
750 env: SConsEnvironment,
751 ):
752 """Action function. Loads the pnr json report and print in a user
753 friendly way."""
754 _ = (target, env) # Unused
755 json_file: File = source[0]
756 json_txt: str = json_file.get_text_contents()
757 _print_pnr_report(json_txt, clk_name_index, verbose)
759 return Action(print_pnr_report, "Formatting pnr report.")
762def get_programmer_cmd(apio_env: ApioEnv) -> str:
763 """Return the programmer command as derived from the scons "prog"
764 arg."""
766 # Should be called only if scons paramsm has 'upload' target parmas.
767 params = apio_env.params
768 assert params.target.HasField("upload"), params
770 # Get the programer command template arg.
771 programmer_cmd = params.target.upload.programmer_cmd
772 assert programmer_cmd, params
774 # -- [NOTE] Generally speaking we would expect the command to include
775 # -- $SOURCE for the binary file path but since we allow custom commands
776 # -- using apio.ini's 'programmer-cmd' option, we don't check for it here.
778 return programmer_cmd
781def get_define_flags(apio_env: ApioEnv) -> str:
782 """Return a string with the -D flags for the verilog defines. Returns
783 an empty string if there are no defines."""
784 flags: List[str] = []
785 for define in apio_env.params.apio_env_params.defines: 785 ↛ 786line 785 didn't jump to line 786 because the loop on line 785 never started
786 flags.append("-D" + define)
788 return " ".join(flags)
791def iverilog_action(
792 apio_env: ApioEnv,
793 *,
794 verbose: bool,
795 vcd_output_name: str,
796 is_interactive: bool,
797 extra_params: List[str] = None,
798 lib_dirs: List[Path] = None,
799 lib_files: List[Path] = None,
800) -> str:
801 """Construct an iverilog scons action string.
802 * env: Rhe scons environment.
803 * verbose: IVerilog will show extra info.
804 * vcd_output_name: Value for the macro VCD_OUTPUT.
805 * is_interactive: True for apio sim, False otherwise.
806 * extra_params: Optional list of additional IVerilog params.
807 * lib_dirs: Optional list of dir pathes to include.
808 * lib_files: Optional list of library files to compile.
809 *
810 * Returns the scons action string for the IVerilog command.
811 """
813 # pylint: disable=too-many-arguments
815 # Escaping for windows. '\' -> '\\'
816 escaped_vcd_output_name = vcd_output_name.replace("\\", "\\\\")
818 # -- Construct the action string.
819 # -- The -g2012 is for system-verilog support.
820 action = (
821 "iverilog -g2012 {0} -o $TARGET {1} {2} {3} {4} {5} {6} {7} $SOURCES"
822 ).format(
823 "-v" if verbose else "",
824 f"-DVCD_OUTPUT={escaped_vcd_output_name}",
825 get_define_flags(apio_env),
826 f"-DAPIO_SIM={int(is_interactive)}",
827 # 'INTERACTIVE_SIM is deprecated and will go away.
828 "-DINTERACTIVE_SIM" if is_interactive else "",
829 map_params(extra_params, "{}"),
830 map_params(lib_dirs, '-I"{}"'),
831 map_params(lib_files, '"{}"'),
832 )
834 return action
837def basename(file_name: str) -> str:
838 """Given a file name, returns it with the extension removed."""
839 result, _ = os.path.splitext(file_name)
840 return result
843def make_verilator_config_builder(lib_path: Path):
844 """Create a scons Builder that writes a verilator config file
845 (hardware.vlt) that suppresses warnings in the lib directory."""
846 assert isinstance(lib_path, Path), lib_path
848 # -- Construct a glob of all library files.
849 glob_path = str(lib_path / "*")
851 # -- Escape for windows. A single backslash is converted into two.
852 glob_str = str(glob_path).replace("\\", "\\\\")
854 # -- Generate the files lines. We suppress a union of all the errors we
855 # -- encountered in all the architectures.
856 lines = ["`verilator_config"]
857 for rule in [
858 "COMBDLY",
859 "WIDTHEXPAND",
860 "SPECIFYIGN",
861 "PINMISSING",
862 "ASSIGNIN",
863 "WIDTHTRUNC",
864 "INITIALDLY",
865 ]:
866 lines.append(f'lint_off -rule {rule} -file "{glob_str}"')
868 # -- Join the lines into text.
869 text = "\n".join(lines) + "\n"
871 def verilator_config_func(target, source, env):
872 """Creates a verilator .vlt config files."""
873 _ = (source, env) # Unused
874 with open(target[0].get_path(), "w", encoding="utf-8") as target_file:
875 target_file.write(text)
876 return 0
878 return Builder(
879 action=Action(
880 verilator_config_func, "Creating verilator config file."
881 ),
882 suffix=".vlt",
883 )