Coverage for tsfpga/yosys/common.py: 96%

55 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-09 00:40 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://github.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9from __future__ import annotations 

10 

11import os 

12import re 

13import subprocess 

14from pathlib import Path 

15from shutil import which 

16 

17from vunit.ostools import Process 

18 

19 

20def run_ghdl(ghdl_path: Path | None, arguments: list[str], cwd: Path) -> bool: 

21 """ 

22 Run GHDL with the given arguments. 

23 Used for analyzing VHDL source files into an on-disk library, which can later be picked up 

24 by the ``ghdl-yosys-plugin`` when running :func:`run_yosys`. 

25 

26 Setting ``cwd`` ensures that any files produced (e.g. GHDL library files) end up in a 

27 well-known location. 

28 

29 Arguments: 

30 ghdl_path: Path to the GHDL executable. Can be set to ``None`` 

31 to use whatever version is in ``PATH``. 

32 arguments: Arguments that shall be passed on to GHDL. 

33 cwd: The GHDL process will be executed with this as the working directory. 

34 

35 Return: 

36 True if everything went well. 

37 """ 

38 cmd = [str(get_ghdl_path(ghdl_path)), *arguments] 

39 

40 try: 

41 Process(args=cmd, cwd=cwd).consume_output() 

42 except Process.NonZeroExitCode: 

43 return False 

44 return True 

45 

46 

47def run_yosys( 

48 script_file: Path, 

49 cwd: Path, 

50 yosys_path: Path | None = None, 

51 ghdl_plugin_path: Path | None = None, 

52 ghdl_path: Path | None = None, 

53 ghdl_prefix: Path | None = None, 

54) -> bool: 

55 """ 

56 Run Yosys with the given script. 

57 

58 Setting ``cwd`` ensures that any files produced (e.g. reports, netlists) end up in a 

59 well-known location. 

60 

61 Arguments: 

62 script_file: Path to a file containing the Yosys commands that shall be executed. 

63 cwd: The Yosys process will be executed with this as the working directory. 

64 yosys_path: Path to the Yosys executable. Can be set to ``None`` 

65 to use whatever version is in ``PATH``. 

66 ghdl_plugin_path: Path to the ``ghdl-yosys-plugin`` module (typically named ``ghdl.so``). 

67 Can be set to ``None`` if the plugin is already available to Yosys without explicitly 

68 loading it (e.g. if it has been installed in the Yosys plugin directory). 

69 ghdl_path: Path to the GHDL executable, used only to auto-detect 'ghdl_prefix' below 

70 when it is not given explicitly. Can be set to ``None`` to use whatever version is 

71 in ``PATH``. 

72 ghdl_prefix: Value to set the ``GHDL_PREFIX`` environment variable to for this process. 

73 The ``ghdl-yosys-plugin`` module is loaded as part of the Yosys process, and hence 

74 can not find the GHDL standard libraries (``std``, ``ieee``, ...) on its own the 

75 way the standalone ``ghdl`` executable can. 

76 Corresponds to the "library prefix" printed by ``ghdl --disp-config``. 

77 If left as ``None``, this method will try to find a value automatically by calling 

78 ``ghdl --disp-config`` (see 'ghdl_path' above) and parsing its output. 

79 Set explicitly to override the auto-detected value, or if auto-detection fails. 

80 

81 Return: 

82 True if everything went well. 

83 """ 

84 cmd = [str(get_yosys_path(yosys_path))] 

85 

86 if ghdl_plugin_path is not None: 

87 cmd += ["-m", str(ghdl_plugin_path.resolve())] 

88 

89 cmd += ["-s", str(script_file.resolve())] 

90 

91 resolved_ghdl_prefix = ( 

92 ghdl_prefix if ghdl_prefix is not None else get_ghdl_library_prefix(ghdl_path) 

93 ) 

94 

95 env = None 

96 if resolved_ghdl_prefix is not None: 

97 env = dict(os.environ) 

98 env["GHDL_PREFIX"] = str(resolved_ghdl_prefix.resolve()) 

99 

100 try: 

101 Process(args=cmd, cwd=cwd, env=env).consume_output() 

102 except Process.NonZeroExitCode: 

103 return False 

104 return True 

105 

106 

107def get_ghdl_path(ghdl_path: Path | None = None) -> Path: 

108 """ 

109 Wrapper to get a path to the GHDL executable. 

110 

111 Arguments: 

112 ghdl_path: Path to the GHDL executable. 

113 Leave as ``None`` to use whatever is available in the system ``PATH``. 

114 """ 

115 if ghdl_path is not None: 

116 return ghdl_path.resolve() 

117 

118 which_ghdl = which("ghdl") 

119 if which_ghdl is None: 

120 raise FileNotFoundError("Could not find ghdl on PATH") 

121 

122 return Path(which_ghdl).resolve() 

123 

124 

125def get_ghdl_library_prefix(ghdl_path: Path | None = None) -> Path | None: 

126 """ 

127 Try to automatically find the GHDL "library prefix", i.e. the location where the 

128 ``std``/``ieee`` standard libraries are installed, by parsing the output of 

129 ``ghdl --disp-config``. 

130 

131 GHDL can normally find this on its own, by looking at the path of its own executable. 

132 This does however not work when GHDL is used as a library, e.g. via the 

133 ``ghdl-yosys-plugin`` module loaded into Yosys (see :func:`run_yosys`), since there is no 

134 ``ghdl`` executable path to derive the prefix from in that case. 

135 Calling this function against the standalone ``ghdl`` executable lets us find the prefix 

136 anyway, so it can be forwarded explicitly. 

137 

138 Arguments: 

139 ghdl_path: Path to the GHDL executable. Can be set to ``None`` 

140 to use whatever version is in ``PATH``. 

141 

142 Return: 

143 The library prefix, or ``None`` if it could not be determined. 

144 """ 

145 try: 

146 cmd = [str(get_ghdl_path(ghdl_path)), "--disp-config"] 

147 result = subprocess.run(cmd, capture_output=True, text=True, check=True) # noqa: S603 

148 except (OSError, subprocess.CalledProcessError): 

149 return None 

150 

151 match = re.search(r"^library prefix:\s*(.+)$", result.stdout, flags=re.MULTILINE) 

152 if match is None: 

153 return None 

154 

155 return Path(match.group(1).strip()) 

156 

157 

158def to_yosys_path(path: Path) -> str: 

159 """ 

160 Return a path string in a format suitable to embed in a Yosys command script. 

161 """ 

162 return path.resolve().as_posix() 

163 

164 

165def get_yosys_path(yosys_path: Path | None = None) -> Path: 

166 """ 

167 Wrapper to get a path to the Yosys executable. 

168 

169 Arguments: 

170 yosys_path: Path to the Yosys executable. 

171 Leave as ``None`` to use whatever is available in the system ``PATH``. 

172 """ 

173 if yosys_path is not None: 

174 return yosys_path.resolve() 

175 

176 which_yosys = which("yosys") 

177 if which_yosys is None: 

178 raise FileNotFoundError("Could not find yosys on PATH") 

179 

180 return Path(which_yosys).resolve()