Coverage for apio/common/common_util.py: 68%
77 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"""Utilities that are available for both the apio (parent) process and the
10scons (child) process."""
12import os
13import sys
14import shutil
15from collections.abc import MutableMapping
16from pathlib import Path
17from glob import glob
18from typing import Any
19import debugpy
20from apio.common.apio_console import cout, fatal_error, cwarning
21from apio.common.apio_styles import EMPH3
22from apio.common.debug_util import is_debug
24# -- A list with the file extensions of the source files.
25SRC_SUFFIXES = [".v", ".sv"]
27# -- The root dir of all the env build directory. Relative to the
28# -- project dir. 'ALL' to distinguish from individual env build dirs.
29PROJECT_BUILD_PATH = Path("_build")
32def env_build_path(env_name: str) -> Path:
33 """Given an env name, return a relative path from the project dir to the
34 env build dir."""
35 return PROJECT_BUILD_PATH / env_name
38def maybe_wait_for_remote_debugger(env_var_name: str):
39 """A rendezvous point for a remote debugger. If the environment variable
40 of given name is set, the function will block until a remote
41 debugger (e.g. from Visual Studio Code) is attached.
42 """
43 if os.getenv(env_var_name) is not None: 43 ↛ 46line 43 didn't jump to line 46 because the condition on line 43 was never true
44 # NOTE: This function may be called before apio_console.py is
45 # initialized, so we use print() instead of cout().
46 print(f"Env var '{env_var_name}' was detected.")
47 print(
48 "Setting PYDEVD_DISABLE_FILE_VALIDATION=1 to disable frozen "
49 "modules warning."
50 )
51 os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
52 port = 5678
53 print(f"Apio SCons for remote debugger on port localhost:{port}.")
54 debugpy.listen(port)
55 print(
56 "Attach Visual Studio Code python remote python debugger "
57 f"to port {port}."
58 )
59 # -- Make sure the messages to the user are flushed.
60 sys.stdout.flush()
61 sys.stderr.flush()
63 # -- Block until the debugger connects.
64 debugpy.wait_for_client()
65 # -- Here the remote debugger is attached and the program continues.
66 print("Remote debugger is attached, program continues...")
69def file_sort_key_func(f: str | Path) -> Any:
70 """Given a file name or path, return a key to sort a file list.
71 The order is lexicography and case sensitive."""
72 path = Path(f)
73 # -- List of directory names in lower case.
74 parents: list[str] = [s.lower() for s in path.parent.parts]
75 # -- File name in lower case.
76 name: str = path.name.lower()
77 # -- Sort by directory and then by file name.
78 return [parents, name]
81def is_source_file(file_name: str) -> bool:
82 """Given a file name, determine by its extension if it's a verilog
83 source file (testbenches included)."""
84 _, ext = os.path.splitext(file_name)
85 return ext in SRC_SUFFIXES
88def has_testbench_name(file_name: str) -> bool:
89 """Given a file name, return true if it's base name indicates a
90 testbench. For example abc_tb.v or _build/abc_tb.out. The file extension
91 is ignored.
92 """
93 name, _ = os.path.splitext(file_name)
94 return name.lower().endswith("_tb")
97def sort_files(files: list[str]) -> list[str]:
98 """Sort a list of files by directory and then by file name.
99 A new sorted list is returned.
100 """
101 # -- Sort the files by directory and then by file name.
102 return sorted(files, key=file_sort_key_func)
105def get_project_source_files() -> tuple[list[str], list[str]]:
106 """Get the list of source files in the directory tree under the current
107 directory, splitted into synth and testbench lists.
108 If source file has the suffix _tb it's is classified st a testbench,
109 otherwise as a synthesis file.
110 """
111 # -- Get a list of all source files in the project dir.
112 # -- Ideally we should use the scons env.Glob() method but it doesn't
113 # -- work with the recursive=True option. So we use the glob() function
114 # -- instead.
115 files: list[str] = []
116 for ext in SRC_SUFFIXES:
117 files.extend(glob(f"**/*{ext}", recursive=True))
119 # -- Sort the files by directory and then by file name.
120 files = sort_files(files)
122 # -- Split file names to synth files and testbench file lists
123 synth_srcs = []
124 test_srcs = []
125 for file in files:
126 if PROJECT_BUILD_PATH in Path(file).parents:
127 # -- Ignore source files from the _build directory.
128 continue
129 if has_testbench_name(file):
130 # -- Handle a testbench file.
131 test_srcs.append(file)
132 else:
133 # -- Handle a synthesis file.
134 synth_srcs.append(file)
136 return (synth_srcs, test_srcs)
139def dump_env(msg: str, env: MutableMapping[str, str]) -> None:
140 """Dump a given env dict."""
141 cout()
142 cout(msg, style=EMPH3)
143 for key in sorted(env, key=str.casefold):
144 cout(f"* {key}={env[key]}")
145 cout("----\n", style=EMPH3)
148def cmd_in_env(
149 cmd: list[str], env: MutableMapping[str, str], *, strict: bool = True
150) -> list[str]:
151 """Given a subprocess command in a form of list parts, and an env for
152 that subprocess, change the first part of the command for the full
153 path of that program using the env PATH. This is useful for windows
154 where the command is searched with the parent env and not with the
155 subprocess env."""
156 assert isinstance(cmd, list)
157 assert len(cmd) > 0
159 # -- Search the command in the env path.
160 cmd_abs_path = shutil.which(cmd[0], path=env.get("PATH"))
162 # -- Make a fresh copy of the list
163 result = cmd.copy()
165 if cmd_abs_path is None: 165 ↛ 167line 165 didn't jump to line 167 because the condition on line 165 was never true
166 # -- handle the case where the command was not found in PATH.
167 msg = f"Command {cmd[0]} not found in env."
168 if strict:
169 fatal_error(msg)
170 else:
171 cwarning(msg)
173 else:
174 # -- Command found, replace the command with the full path
175 result[0] = cmd_abs_path
177 # -- Dump for debugging
178 if is_debug(1): 178 ↛ 179line 178 didn't jump to line 179 because the condition on line 178 was never true
179 cout("cmd_in_env():", style=EMPH3)
180 cout(f" Before: {str(cmd)}")
181 cout(f" After: {str(result)}")
183 # -- All done.
184 return result