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

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 

8 

9"""Utilities that are available for both the apio (parent) process and the 

10scons (child) process.""" 

11 

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 

23 

24# -- A list with the file extensions of the source files. 

25SRC_SUFFIXES = [".v", ".sv"] 

26 

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") 

30 

31 

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 

36 

37 

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() 

62 

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...") 

67 

68 

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] 

79 

80 

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 

86 

87 

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") 

95 

96 

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) 

103 

104 

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)) 

118 

119 # -- Sort the files by directory and then by file name. 

120 files = sort_files(files) 

121 

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) 

135 

136 return (synth_srcs, test_srcs) 

137 

138 

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) 

146 

147 

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 

158 

159 # -- Search the command in the env path. 

160 cmd_abs_path = shutil.which(cmd[0], path=env.get("PATH")) 

161 

162 # -- Make a fresh copy of the list 

163 result = cmd.copy() 

164 

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) 

172 

173 else: 

174 # -- Command found, replace the command with the full path 

175 result[0] = cmd_abs_path 

176 

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)}") 

182 

183 # -- All done. 

184 return result