back.pysim: simplify.
[nmigen.git] / nmigen / _yosys.py
1 import os
2 import sys
3 import re
4 import subprocess
5 import warnings
6 import pathlib
7 try:
8 from importlib import metadata as importlib_metadata # py3.8+ stdlib
9 except ImportError:
10 try:
11 import importlib_metadata # py3.7- shim
12 except ImportError:
13 importlib_metadata = None # not installed
14 try:
15 from importlib import resources as importlib_resources
16 try:
17 importlib_resources.files # py3.9+ stdlib
18 except AttributeError:
19 import importlib_resources # py3.8- shim
20 except ImportError:
21 import importlib_resources # py3.6- shim
22
23 from ._toolchain import has_tool, require_tool
24
25
26 __all__ = ["YosysError", "YosysBinary", "find_yosys"]
27
28
29 class YosysError(Exception):
30 pass
31
32
33 class YosysWarning(Warning):
34 pass
35
36
37 class YosysBinary:
38 @classmethod
39 def available(cls):
40 """Check for Yosys availability.
41
42 Returns
43 -------
44 available : bool
45 ``True`` if Yosys is installed, ``False`` otherwise. Installed binary may still not
46 be runnable, or might be too old to be useful.
47 """
48 raise NotImplementedError
49
50 @classmethod
51 def version(cls):
52 """Get Yosys version.
53
54 Returns
55 -------
56 ``None`` if version number could not be determined, or a 3-tuple ``(major, minor, distance)`` if it could.
57
58 major : int
59 Major version.
60 minor : int
61 Minor version.
62 distance : int
63 Distance to last tag per ``git describe``. May not be exact for system Yosys.
64 """
65 raise NotImplementedError
66
67 @classmethod
68 def data_dir(cls):
69 """Get Yosys data directory.
70
71 Returns
72 -------
73 data_dir : pathlib.Path
74 Yosys data directory (also known as "datdir").
75 """
76 raise NotImplementedError
77
78 @classmethod
79 def run(cls, args, stdin=""):
80 """Run Yosys process.
81
82 Parameters
83 ----------
84 args : list of str
85 Arguments, not including the program name.
86 stdin : str
87 Standard input.
88
89 Returns
90 -------
91 stdout : str
92 Standard output.
93
94 Exceptions
95 ----------
96 YosysError
97 Raised if Yosys returns a non-zero code. The exception message is the standard error
98 output.
99 """
100 raise NotImplementedError
101
102 @classmethod
103 def _process_result(cls, returncode, stdout, stderr, ignore_warnings, src_loc_at):
104 if returncode:
105 raise YosysError(stderr.strip())
106 if not ignore_warnings:
107 for match in re.finditer(r"(?ms:^Warning: (.+)\n$)", stderr):
108 message = match.group(1).replace("\n", " ")
109 warnings.warn(message, YosysWarning, stacklevel=3 + src_loc_at)
110 return stdout
111
112
113 class _BuiltinYosys(YosysBinary):
114 YOSYS_PACKAGE = "nmigen_yosys"
115
116 @classmethod
117 def available(cls):
118 if importlib_metadata is None:
119 return False
120 try:
121 importlib_metadata.version(cls.YOSYS_PACKAGE)
122 return True
123 except importlib_metadata.PackageNotFoundError:
124 return False
125
126 @classmethod
127 def version(cls):
128 version = importlib_metadata.version(cls.YOSYS_PACKAGE)
129 match = re.match(r"^(\d+)\.(\d+)(?:\.post(\d+))?", version)
130 return (int(match[1]), int(match[2]), int(match[3] or 0))
131
132 @classmethod
133 def data_dir(cls):
134 return importlib_resources.files(cls.YOSYS_PACKAGE) / "share"
135
136 @classmethod
137 def run(cls, args, stdin="", *, ignore_warnings=False, src_loc_at=0):
138 popen = subprocess.Popen([sys.executable, "-m", cls.YOSYS_PACKAGE, *args],
139 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
140 encoding="utf-8")
141 stdout, stderr = popen.communicate(stdin)
142 return cls._process_result(popen.returncode, stdout, stderr, ignore_warnings, src_loc_at)
143
144
145 class _SystemYosys(YosysBinary):
146 YOSYS_BINARY = "yosys"
147
148 @classmethod
149 def available(cls):
150 return has_tool(cls.YOSYS_BINARY)
151
152 @classmethod
153 def version(cls):
154 version = cls.run(["-V"])
155 match = re.match(r"^Yosys (\d+)\.(\d+)(?:\+(\d+))?", version)
156 if match:
157 return (int(match[1]), int(match[2]), int(match[3] or 0))
158 else:
159 return None
160
161 @classmethod
162 def data_dir(cls):
163 popen = subprocess.Popen([require_tool(cls.YOSYS_BINARY) + "-config", "--datdir"],
164 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
165 encoding="utf-8")
166 stdout, stderr = popen.communicate()
167 if popen.returncode:
168 raise YosysError(stderr.strip())
169 return pathlib.Path(stdout.strip())
170
171 @classmethod
172 def run(cls, args, stdin="", *, ignore_warnings=False, src_loc_at=0):
173 popen = subprocess.Popen([require_tool(cls.YOSYS_BINARY), *args],
174 stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
175 encoding="utf-8")
176 stdout, stderr = popen.communicate(stdin)
177 # If Yosys is built with an evaluation version of Verific, then Verific license
178 # information is printed first. It consists of empty lines and lines starting with `--`,
179 # which are not normally a part of Yosys output, and can be fairly safely removed.
180 #
181 # This is not ideal, but Verific license conditions rule out any other solution.
182 stdout = re.sub(r"\A(-- .+\n|\n)*", "", stdout)
183 return cls._process_result(popen.returncode, stdout, stderr, ignore_warnings, src_loc_at)
184
185
186 def find_yosys(requirement):
187 """Find an available Yosys executable of required version.
188
189 Parameters
190 ----------
191 requirement : function
192 Version check. Should return ``True`` if the version is acceptable, ``False`` otherwise.
193
194 Returns
195 -------
196 yosys_binary : subclass of YosysBinary
197 Proxy for running the requested version of Yosys.
198
199 Exceptions
200 ----------
201 YosysError
202 Raised if required Yosys version is not found.
203 """
204 proxies = []
205 clauses = os.environ.get("NMIGEN_USE_YOSYS", "system,builtin").split(",")
206 for clause in clauses:
207 if clause == "builtin":
208 proxies.append(_BuiltinYosys)
209 elif clause == "system":
210 proxies.append(_SystemYosys)
211 else:
212 raise YosysError("The NMIGEN_USE_YOSYS environment variable contains "
213 "an unrecognized clause {!r}"
214 .format(clause))
215 for proxy in proxies:
216 if proxy.available():
217 version = proxy.version()
218 if version is not None and requirement(version):
219 return proxy
220 else:
221 if "NMIGEN_USE_YOSYS" in os.environ:
222 raise YosysError("Could not find an acceptable Yosys binary. Searched: {}"
223 .format(", ".join(clauses)))
224 else:
225 raise YosysError("Could not find an acceptable Yosys binary. The `nmigen-yosys` PyPI "
226 "package, if available for this platform, can be used as fallback")