Coverage for apio/scons/plugin_util.py: 88%
244 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
10"""Helper functions for apio scons plugins."""
13import sys
14import os
15import re
16import json
17from dataclasses import dataclass
18from pathlib import Path
19from typing import List, Dict, Optional, Union, Callable
20from rich.table import Table
21from rich import box
22from SCons import Scanner
23from SCons.Builder import Builder
24from SCons.Action import FunctionAction, Action
25from SCons.Node.FS import File
26from SCons.Script.SConscript import SConsEnvironment
27from SCons.Node import NodeList
28from SCons.Node.Alias import Alias
29from apio.scons.apio_env import ApioEnv
30from apio.common.common_util import (
31 has_testbench_name,
32 is_source_file,
33)
34from apio.common.apio_console import cout, cerror, cwarning, ctable
35from apio.common.apio_styles import INFO, BORDER, EMPH1, EMPH2, EMPH3
38TESTBENCH_HINT = "Testbench file names must end with '_tb.v' or '_tb.sv'."
41def map_params(params: Optional[List[Union[str, Path]]], fmt: str) -> str:
42 """A common function construct a command string snippet from a list
43 of arguments. The functon does the following:
44 1. If params arg is None replace it with []
45 2. Drops empty or white space only items.
46 3. Maps the items using the format string which contains exactly one
47 placeholder {}.
48 4. Joins the items with a white space char.
50 For examples, see the unit test at test_scons_util.py.
51 """
52 # None designates empty list. Avoiding the pylint non safe default
53 # warning.
54 if params is None:
55 params = []
57 # Convert parmas to stripped strings.
58 params = [str(x).strip() for x in params]
60 # Drop the empty params and map the rest.
61 mapped_params = [fmt.format(x) for x in params if x]
63 # Join using a single space.
64 return " ".join(mapped_params)
67def get_constraint_file(apio_env: ApioEnv, file_ext: str) -> str:
68 """Returns the name of the constraint file to use.
70 env is the sconstruction environment.
72 file_ext is a string with the constrained file extension.
73 E.g. ".pcf" for ice40.
75 Returns the file name if found or a default name otherwise otherwise.
76 """
77 # -- If the user specified a 'constraint-file' in apio.ini then use it.
78 user_specified = apio_env.params.apio_env_params.constraint_file
79 if user_specified:
80 # -- Check for proper extension for this architecture.
81 if not user_specified.endswith(file_ext):
82 cerror(
83 f"Constraint file '{user_specified}' should have "
84 f"the extension '{file_ext}'."
85 )
86 sys.exit(1)
87 # -- Check for valid chars only, e.g. dir separators are not allowed.
88 # -- This must be a simple file name that is expected to be found in
89 # -- the root directory of the project.
90 forbidden_chars = '<>:"/\\|?*\0'
91 for c in user_specified:
92 if c in forbidden_chars:
93 cerror(
94 f"Constrain filename '{user_specified}' contains an "
95 f"illegal character: '{c}'"
96 )
97 sys.exit(1)
99 return user_specified
101 # -- No user specified constraint file, try to look for it.
102 # --
103 # -- Get existing files in alphabetical order. We search only in the root
104 # -- directory of the project.
105 files = apio_env.scons_env.Glob(f"*{file_ext}")
106 n = len(files)
108 # -- Case 1: No matching constrain files.
109 if n == 0:
110 cerror(
111 f"No constraint file '*{file_ext}' found, expected exactly one."
112 )
113 sys.exit(1)
114 # -- Case 2: Exactly one constrain file found.
115 if n == 1:
116 result = str(files[0])
117 return result
118 # -- Case 3: Multiple matching constrain files.
119 cerror(
120 f"Found multiple '*{file_ext}' "
121 "constraint files, expecting exactly one."
122 )
123 sys.exit(1)
126def verilog_src_scanner(apio_env: ApioEnv) -> Scanner.Base:
127 """Creates and returns a scons Scanner object for scanning verilog
128 files for dependencies.
129 """
130 # A Regex to icestudio propriaetry references for *.list files.
131 # Example:
132 # Text: ' parameter v771499 = "v771499.list"'
133 # Captures: 'v771499.list'
134 icestudio_list_re = re.compile(r"[\n|\s][^\/]?\"(.*\.list?)\"", re.M)
136 # A regex to match a verilog include directive.
137 # Example
138 # Text: `include "apio_testing.vh"
139 # Capture: 'apio_testing.vh'
140 verilog_include_re = re.compile(r'`\s*include\s+["]([^"]+)["]', re.M)
142 # A regex for inclusion via $readmemh()
143 # Example
144 # Test: '$readmemh("my_data.hex", State_buff);'
145 # Capture: 'my_data.hex'
146 readmemh_reference_re = re.compile(
147 r"\$readmemh\([\'\"]([^\'\"]+)[\'\"]", re.M
148 )
150 # -- List of required and optional files that may require a rebuild if
151 # -- changed.
152 core_dependencies = [
153 "apio.ini",
154 "boards.jsonc",
155 "fpgas.jsonc",
156 "programmers.jsonc",
157 ]
159 def verilog_src_scanner_func(
160 file_node: File, env: SConsEnvironment, ignored_path
161 ) -> List[str]:
162 """Given a [System]Verilog file, scan it and return a list of
163 references to other files it depends on. It's not require to report
164 dependency on another source file in the project since scons loads
165 anyway all the source files in the project.
167 Returns a list of files. Dependencies that don't have an existing
168 file are ignored and not returned. This is to avoid references in
169 commented out code to break scons dependencies.
170 """
171 _ = env # Unused
173 # Sanity check. Should be called only to scan verilog files. If
174 # this fails, this is a programming error rather than a user error.
175 assert is_source_file(
176 file_node.name
177 ), f"Not a src file: {file_node.name}"
179 # Get the directory of the file, relative to the project root which is
180 # the current working directory. This value is equals to "." if the
181 # file is in the project root.
182 file_dir: str = file_node.get_dir().get_path()
184 # Prepare an empty set of dependencies.
185 candidates_raw_set = set()
187 # Read the file. This returns [] if the file doesn't exist.
188 file_content = file_node.get_text_contents()
190 # Get verilog includes references.
191 candidates_raw_set.update(verilog_include_re.findall(file_content))
193 # Get $readmemh() function references.
194 candidates_raw_set.update(readmemh_reference_re.findall(file_content))
196 # Get IceStudio references.
197 candidates_raw_set.update(icestudio_list_re.findall(file_content))
199 # Since we don't know if the dependency's path is relative to the file
200 # location or the project root, we try both. We prefer to have high
201 # recall of dependencies of high precision, risking at most unnecessary
202 # rebuilds.
203 candidates_set = candidates_raw_set.copy()
204 # If the file is not in the project dir, add a dependency also relative
205 # to the project dir.
206 if file_dir != ".":
207 for raw_candidate in candidates_raw_set:
208 candidate: str = os.path.join(file_dir, raw_candidate)
209 candidates_set.add(candidate)
211 # Add the core dependencies. They are always relative to the project
212 # root.
213 candidates_set.update(core_dependencies)
215 # Filter out candidates that don't have a matching files to prevert
216 # breaking the build. This handle for example the case where the
217 # file references is in a comment or non reachable code.
218 # See also https://stackoverflow.com/q/79302552/15038713
219 dependencies = []
220 for dependency in candidates_set:
221 if Path(dependency).exists():
222 dependencies.append(dependency)
223 elif apio_env.is_debug(1): 223 ↛ 224line 223 didn't jump to line 224 because the condition on line 223 was never true
224 cout(
225 f"Dependency candidate {dependency} does not exist, "
226 "dropping."
227 )
229 # Sort the strings for determinism.
230 dependencies = sorted(list(dependencies))
232 # Debug info.
233 if apio_env.is_debug(1): 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true
234 cout(f"Dependencies of {file_node}:", style=EMPH2)
235 for dependency in dependencies:
236 cout(f" {dependency}", style=EMPH2)
238 # All done
239 return apio_env.scons_env.File(dependencies)
241 return apio_env.scons_env.Scanner(function=verilog_src_scanner_func)
244def verilator_lint_action(
245 apio_env: ApioEnv,
246 *,
247 extra_params: List[str] = None,
248 lib_dirs: List[Path] = None,
249 lib_files: List[Path] = None,
250) -> List[
251 Callable[
252 [
253 List[File],
254 List[Alias],
255 SConsEnvironment,
256 ],
257 None,
258 ]
259 | str,
260]:
261 """Construct an verilator scons action.
262 * extra_params: Optional additional arguments.
263 * libs_dirs: Optional directories for include search.
264 * lib_files: Optional additional files to include.
265 Returns an action in a form of a list with two steps, a function to call
266 and a string command.
267 """
269 # -- Sanity checks
270 assert apio_env.targeting("lint")
271 assert apio_env.params.target.HasField("lint")
273 # -- Keep short references.
274 params = apio_env.params
275 lint_params = params.target.lint
277 # -- Determine top module.
278 top_module = (
279 lint_params.top_module
280 if lint_params.top_module
281 else params.apio_env_params.top_module
282 )
284 # -- Construct the action
285 action = (
286 "verilator_bin --lint-only --quiet --bbox-unsup --timing "
287 "-Wno-TIMESCALEMOD -Wno-MULTITOP -DAPIO_SIM=0 "
288 "{0} {1} {2} {3} {4} {5} {6} {7} {8} {9} $SOURCES"
289 ).format(
290 "-Wall" if lint_params.verilator_all else "",
291 "-Wno-style" if lint_params.verilator_no_style else "",
292 map_params(lint_params.verilator_no_warns, "-Wno-{}"),
293 map_params(lint_params.verilator_warns, "-Wwarn-{}"),
294 f"--top-module {top_module}",
295 get_define_flags(apio_env),
296 map_params(extra_params, "{}"),
297 map_params(lib_dirs, '-I"{}"'),
298 apio_env.target + ".vlt",
299 map_params(lib_files, '"{}"'),
300 )
302 return [source_files_issue_scanner_action(), action]
305@dataclass(frozen=True)
306class SimulationConfig:
307 """Simulation parameters, for sim and test commands."""
309 testbench_name: str # The testbench name, without the 'v' suffix.
310 build_testbench_name: str # testbench_name prefixed by build dir.
311 srcs: List[str] # List of source files to compile.
314def waves_target(
315 api_env: ApioEnv,
316 name: str,
317 vcd_file_target: NodeList,
318 sim_config: SimulationConfig,
319 no_gtkwave: bool,
320) -> List[Alias]:
321 """Construct a target to launch the QTWave signal viewer.
322 vcd_file_target is the simulator target that generated the vcd file
323 with the signals. Returns the new targets.
324 """
326 # -- Construct the commands list.
327 commands = []
329 if no_gtkwave: 329 ↛ 345line 329 didn't jump to line 345 because the condition on line 329 was always true
330 # -- User asked to skip gtkwave. The '@' suppresses the printing
331 # -- of the echo command itself.
332 commands.append(
333 "@echo 'Flag --no-gtkwave was found, skipping GTKWave.'"
334 )
336 else:
337 # -- Normal pase, invoking gtkwave.
339 # -- On windows we need to setup the cache. This could be done once
340 # -- when the oss-cad-suite is installed but since we currently don't
341 # -- have a package setup mechanism, we do it here on each invocation.
342 # -- The time penalty is negligible.
343 # -- With the stock oss-cad-suite windows package, this is done in the
344 # -- environment.bat script.
345 if api_env.is_windows:
346 commands.append("gdk-pixbuf-query-loaders --update-cache")
348 # -- The actual wave viewer command.
349 commands.append(
350 "gtkwave {0} {1} {2}.gtkw".format(
351 '--rcvar "splash_disable on" --rcvar "do_initial_zoom_fit 1"',
352 vcd_file_target[0],
353 sim_config.testbench_name,
354 )
355 )
357 target = api_env.alias(
358 name,
359 source=vcd_file_target,
360 action=commands,
361 always_build=True,
362 )
364 return target
367def check_valid_testbench_name(testbench: str) -> None:
368 """Check if a testbench name is valid. If not, print an error message
369 and exit."""
370 if not is_source_file(testbench) or not has_testbench_name(testbench): 370 ↛ 371line 370 didn't jump to line 371 because the condition on line 370 was never true
371 cerror(f"'{testbench}' is not a valid testbench file name.")
372 cout(TESTBENCH_HINT, style=INFO)
373 sys.exit(1)
376def get_sim_config(
377 apio_env: ApioEnv,
378 testbench: str,
379 synth_srcs: List[str],
380 test_srcs: List[str],
381) -> SimulationConfig:
382 """Returns a SimulationConfig for a sim command. 'testbench' is
383 an optional testbench file name. 'synth_srcs' and 'test_srcs' are the
384 all the project's synth and testbench files found in the project as
385 returned by get_project_source_files()."""
387 # -- Handle the testbench file selection. The end result is a single
388 # -- testbench file name in testbench that we simulate, or a fatal error.
389 if testbench:
390 # -- Case 1 - Testbench file name is specified in the command or
391 # -- apio.ini. Fatal error if invalid.
392 check_valid_testbench_name(testbench)
393 elif len(test_srcs) == 0: 393 ↛ 396line 393 didn't jump to line 396 because the condition on line 393 was never true
394 # -- Case 2 Testbench name was not specified and no testbench files
395 # -- were found in the project.
396 cerror("No testbench files found in the project.")
397 cout(TESTBENCH_HINT, style=INFO)
398 sys.exit(1)
399 elif len(test_srcs) == 1: 399 ↛ 407line 399 didn't jump to line 407 because the condition on line 399 was always true
400 # -- Case 3 Testbench name was not specified but there is exactly
401 # -- one in the project.
402 testbench = test_srcs[0]
403 cout(f"Found testbench file {testbench}", style=EMPH1)
404 else:
405 # -- Case 4 Testbench name was not specified and there are multiple
406 # -- testbench files in the project.
407 cerror("Multiple testbench files found in the project.")
408 cout(
409 "Please specify the testbench file name in the command "
410 "or in apio.ini 'default-testbench' option.",
411 style=INFO,
412 )
413 sys.exit(1)
415 # -- This should not happen. If it does, it's a programming error.
416 assert testbench, "get_sim_config(): Missing testbench file name"
418 # -- Construct a SimulationParams with all the synth files + the
419 # -- testbench file.
420 testbench_name = basename(testbench)
421 build_testbench_name = str(apio_env.env_build_path / testbench_name)
422 srcs = synth_srcs + [testbench]
423 return SimulationConfig(testbench_name, build_testbench_name, srcs)
426def get_tests_configs(
427 apio_env: ApioEnv,
428 testbench: str,
429 synth_srcs: List[str],
430 test_srcs: list[str],
431) -> List[SimulationConfig]:
432 """Return a list of SimulationConfigs for each of the testbenches that
433 need to be run for a 'apio test' command. If testbench is empty,
434 all the testbenches in test_srcs will be tested. Otherwise, only the
435 testbench in testbench will be tested. synth_srcs and test_srcs are
436 source and test file lists as returned by get_project_source_files()."""
437 # List of testbenches to be tested.
439 # -- Handle the testbench files selection. The end result is a list of one
440 # -- or more testbench file names in testbenches that we test.
441 if testbench:
442 # -- Case 1 - a testbench file name is specified in the command or
443 # -- apio.ini. Fatal error if invalid.
444 check_valid_testbench_name(testbench)
445 testbenches = [testbench]
446 elif len(test_srcs) == 0: 446 ↛ 449line 446 didn't jump to line 449 because the condition on line 446 was never true
447 # -- Case 2 - Testbench file name was not specified and there are no
448 # -- testbench files in the project.
449 cerror("No testbench files found in the project.")
450 cout(TESTBENCH_HINT, style=INFO)
451 sys.exit(1)
452 else:
453 # -- Case 3 - Testbench file name was not specified but there are one
454 # -- or more testbench files in the project.
455 testbenches = test_srcs
457 # -- If this fails, it's a programming error.
458 assert testbenches, "get_tests_configs(): no testbenches"
460 # Construct a config for each testbench.
461 configs = []
462 for tb in testbenches:
463 testbench_name = basename(tb)
464 build_testbench_name = str(apio_env.env_build_path / testbench_name)
465 srcs = synth_srcs + [tb]
466 configs.append(
467 SimulationConfig(testbench_name, build_testbench_name, srcs)
468 )
470 return configs
473def announce_testbench_action() -> FunctionAction:
474 """Returns an action that prints a title with the testbench name."""
476 def announce_testbench(
477 source: List[File],
478 target: List[Alias],
479 env: SConsEnvironment,
480 ):
481 """The action function."""
482 _ = (target, env) # Unused
484 # -- We expect to find exactly one testbench.
485 testbenches = [
486 file
487 for file in source
488 if (is_source_file(file.name) and has_testbench_name(file.name))
489 ]
490 assert len(testbenches) == 1, testbenches
492 # -- Announce it.
493 cout()
494 cout(f"Testbench {testbenches[0]}", style=EMPH3)
496 # -- Run the action but don't announce the action.
497 return Action(announce_testbench, strfunction=None)
500def source_files_issue_scanner_action() -> FunctionAction:
501 """Returns a SCons action that scans the source files and print
502 error or warning messages about issues it finds."""
504 # A regex to identify "$dumpfile(" in testbenches.
505 testbench_dumpfile_re = re.compile(r"[$]dumpfile\s*[(]")
507 # A regex to identify "INTERACTIVE_SIM" in testbenches
508 interactive_sim_re = re.compile(r"INTERACTIVE_SIM")
510 def report_source_files_issues(
511 source: List[File],
512 target: List[Alias],
513 env: SConsEnvironment,
514 ):
515 """The scanner function."""
517 _ = (target, env) # Unused
519 for file in source:
521 # -- For now we report issues only in testbenches so skip
522 # -- otherwise.
523 if not is_source_file(file.name) or not has_testbench_name(
524 file.name
525 ):
526 continue
528 # -- Read the testbench file text.
529 file_text = file.get_text_contents()
531 # -- if contains $dumpfile, print a warning.
532 if testbench_dumpfile_re.findall(file_text): 532 ↛ 533line 532 didn't jump to line 533 because the condition on line 532 was never true
533 cwarning("Avoid using $dumpfile() in Apio testbenches.")
535 # -- if contains $dumpfile, print a warning.
536 if interactive_sim_re.findall(file_text): 536 ↛ 537line 536 didn't jump to line 537 because the condition on line 536 was never true
537 cwarning(
538 "The Apio macro `INTERACTIVE_SIM is deprecated. "
539 "Use `APIO_SIM (0 or 1) instead."
540 )
542 # -- Run the action but don't announce the action. We will print
543 # -- ourselves in report_source_files_issues.
544 return Action(report_source_files_issues, strfunction=None)
547def _print_pnr_utilization_report(report: Dict[str, any]):
548 table = Table(
549 show_header=True,
550 show_lines=False,
551 box=box.SQUARE,
552 border_style=BORDER,
553 title="FPGA Resource Utilization",
554 title_justify="left",
555 padding=(0, 2),
556 )
558 # -- Add columns.
559 table.add_column("RESOURCE", no_wrap=True)
560 table.add_column("USED", no_wrap=True, justify="right")
561 table.add_column("TOTAL", no_wrap=True, justify="right")
562 table.add_column("UTIL.", no_wrap=True, justify="right")
564 # -- Add rows
565 utilization = report["utilization"]
566 for resource, vals in utilization.items():
567 used = vals["used"]
568 used_str = f"{used} " if used else ""
569 available = vals["available"]
570 available_str = f"{available} "
571 percents = int(100 * used / available)
572 percents_str = f"{percents}% " if used else ""
573 style = EMPH3 if used > 0 else None
574 table.add_row(
575 resource, used_str, available_str, percents_str, style=style
576 )
578 # -- Render the table
579 cout()
580 ctable(table)
583def _maybe_print_pnr_clocks_report(
584 report: Dict[str, any], clk_name_index: int
585) -> bool:
586 clocks = report["fmax"]
587 if len(clocks) == 0:
588 return False
590 table = Table(
591 show_header=True,
592 show_lines=True,
593 box=box.SQUARE,
594 border_style=BORDER,
595 title="Clock Information",
596 title_justify="left",
597 padding=(0, 2),
598 )
600 # -- Add columns
601 table.add_column("CLOCK", no_wrap=True)
602 table.add_column(
603 "MAX SPEED [Mhz]", no_wrap=True, justify="right", style=EMPH3
604 )
606 # -- Add rows.
607 clocks = report["fmax"]
608 for clk_net, vals in clocks.items():
609 # -- Extract clock name from the net name.
610 clk_signal = clk_net.split("$")[clk_name_index]
611 # -- Extract speed
612 max_mhz = vals["achieved"]
613 # -- Add row.
614 table.add_row(clk_signal, f"{max_mhz:.2f}")
616 # -- Render the table
617 cout()
618 ctable(table)
619 return True
622def _print_pnr_report(
623 json_txt: str,
624 clk_name_index: int,
625 verbose: bool,
626) -> None:
627 """Accepts the text of the pnr json report and prints it in
628 a user friendly way. Used by the 'apio report' command."""
629 # -- Parse the json text into a tree of dicts.
630 report: Dict[str, any] = json.loads(json_txt)
632 # -- Print the utilization table.
633 _print_pnr_utilization_report(report)
635 # -- Print the optional clocks table.
636 clock_report_printed = _maybe_print_pnr_clocks_report(
637 report, clk_name_index
638 )
640 # -- Print summary.
641 cout("")
642 if not clock_report_printed:
643 cout("No clocks were found in the design.", style=INFO)
644 if not verbose: 644 ↛ exitline 644 didn't return from function '_print_pnr_report' because the condition on line 644 was always true
645 cout("Run 'apio report --verbose' for more details.", style=INFO)
648def report_action(clk_name_index: int, verbose: bool) -> FunctionAction:
649 """Returns a SCons action to format and print the PNR reort from the
650 PNR json report file. Used by the 'apio report' command.
651 'script_id' identifies the calling SConstruct script and 'verbose'
652 indicates if the --verbose flag was invoked."""
654 def print_pnr_report(
655 source: List[File],
656 target: List[Alias],
657 env: SConsEnvironment,
658 ):
659 """Action function. Loads the pnr json report and print in a user
660 friendly way."""
661 _ = (target, env) # Unused
662 json_file: File = source[0]
663 json_txt: str = json_file.get_text_contents()
664 _print_pnr_report(json_txt, clk_name_index, verbose)
666 return Action(print_pnr_report, "Formatting pnr report.")
669def get_programmer_cmd(apio_env: ApioEnv) -> str:
670 """Return the programmer command as derived from the scons "prog"
671 arg."""
673 # Should be called only if scons paramsm has 'upload' target parmas.
674 params = apio_env.params
675 assert params.target.HasField("upload"), params
677 # Get the programer command template arg.
678 programmer_cmd = params.target.upload.programmer_cmd
679 assert programmer_cmd, params
681 # -- [NOTE] Generally speaking we would expect the command to include
682 # -- $SOURCE for the binary file path but since we allow custom commands
683 # -- using apio.ini's 'programmer-cmd' option, we don't check for it here.
685 return programmer_cmd
688def get_define_flags(apio_env: ApioEnv) -> str:
689 """Return a string with the -D flags for the verilog defines. Returns
690 an empty string if there are no defines."""
691 flags: List[str] = []
692 for define in apio_env.params.apio_env_params.defines: 692 ↛ 693line 692 didn't jump to line 693 because the loop on line 692 never started
693 flags.append("-D" + define)
695 return " ".join(flags)
698def iverilog_action(
699 apio_env: ApioEnv,
700 *,
701 verbose: bool,
702 vcd_output_name: str,
703 is_interactive: bool,
704 extra_params: List[str] = None,
705 lib_dirs: List[Path] = None,
706 lib_files: List[Path] = None,
707) -> str:
708 """Construct an iverilog scons action string.
709 * env: Rhe scons environment.
710 * verbose: IVerilog will show extra info.
711 * vcd_output_name: Value for the macro VCD_OUTPUT.
712 * is_interactive: True for apio sim, False otherwise.
713 * extra_params: Optional list of additional IVerilog params.
714 * lib_dirs: Optional list of dir pathes to include.
715 * lib_files: Optional list of library files to compile.
716 *
717 * Returns the scons action string for the IVerilog command.
718 """
720 # pylint: disable=too-many-arguments
722 # Escaping for windows. '\' -> '\\'
723 escaped_vcd_output_name = vcd_output_name.replace("\\", "\\\\")
725 # -- Construct the action string.
726 # -- The -g2012 is for system-verilog support.
727 action = (
728 "iverilog -g2012 {0} -o $TARGET {1} {2} {3} {4} {5} {6} {7} $SOURCES"
729 ).format(
730 "-v" if verbose else "",
731 f"-DVCD_OUTPUT={escaped_vcd_output_name}",
732 get_define_flags(apio_env),
733 f"-DAPIO_SIM={int(is_interactive)}",
734 # 'INTERACTIVE_SIM is deprecated and will go away.
735 "-DINTERACTIVE_SIM" if is_interactive else "",
736 map_params(extra_params, "{}"),
737 map_params(lib_dirs, '-I"{}"'),
738 map_params(lib_files, '"{}"'),
739 )
741 return action
744def basename(file_name: str) -> str:
745 """Given a file name, returns it with the extension removed."""
746 result, _ = os.path.splitext(file_name)
747 return result
750def make_verilator_config_builder(lib_path: Path):
751 """Create a scons Builder that writes a verilator config file
752 (hardware.vlt) that suppresses warnings in the lib directory."""
753 assert isinstance(lib_path, Path), lib_path
755 # -- Construct a glob of all library files.
756 glob_path = str(lib_path / "*")
758 # -- Escape for windows. A single backslash is converted into two.
759 glob_str = str(glob_path).replace("\\", "\\\\")
761 # -- Generate the files lines. We suppress a union of all the errors we
762 # -- encountered in all the architectures.
763 lines = ["`verilator_config"]
764 for rule in [
765 "COMBDLY",
766 "WIDTHEXPAND",
767 "SPECIFYIGN",
768 "PINMISSING",
769 "ASSIGNIN",
770 "WIDTHTRUNC",
771 "INITIALDLY",
772 ]:
773 lines.append(f'lint_off -rule {rule} -file "{glob_str}"')
775 # -- Join the lines into text.
776 text = "\n".join(lines) + "\n"
778 def verilator_config_func(target, source, env):
779 """Creates a verilator .vlt config files."""
780 _ = (source, env) # Unused
781 with open(target[0].get_path(), "w", encoding="utf-8") as target_file:
782 target_file.write(text)
783 return 0
785 return Builder(
786 action=Action(
787 verilator_config_func, "Creating verilator config file."
788 ),
789 suffix=".vlt",
790 )