
This isn't used yet (the only Yosys warning we can get is useless), but will be handy for CXXRTL.
186 lines
5.8 KiB
Python
186 lines
5.8 KiB
Python
import os
|
|
import sys
|
|
import re
|
|
import subprocess
|
|
import warnings
|
|
try:
|
|
from importlib import metadata as importlib_metadata # py3.8+ stdlib
|
|
except ImportError:
|
|
try:
|
|
import importlib_metadata # py3.7- shim
|
|
except ImportError:
|
|
importlib_metadata = None # not installed
|
|
|
|
from ._toolchain import has_tool, require_tool
|
|
|
|
|
|
__all__ = ["YosysError", "YosysBinary", "find_yosys"]
|
|
|
|
|
|
class YosysError(Exception):
|
|
pass
|
|
|
|
|
|
class YosysWarning(Warning):
|
|
pass
|
|
|
|
|
|
class YosysBinary:
|
|
@classmethod
|
|
def available(cls):
|
|
"""Check for Yosys availability.
|
|
|
|
Returns
|
|
-------
|
|
available : bool
|
|
``True`` if Yosys is installed, ``False`` otherwise. Installed binary may still not
|
|
be runnable, or might be too old to be useful.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def version(cls):
|
|
"""Get Yosys version.
|
|
|
|
Returns
|
|
-------
|
|
major : int
|
|
Major version.
|
|
minor : int
|
|
Minor version.
|
|
distance : int
|
|
Distance to last tag per ``git describe``. May not be exact for system Yosys.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def run(cls, args, stdin=""):
|
|
"""Run Yosys process.
|
|
|
|
Parameters
|
|
----------
|
|
args : list of str
|
|
Arguments, not including the program name.
|
|
stdin : str
|
|
Standard input.
|
|
|
|
Returns
|
|
-------
|
|
stdout : str
|
|
Standard output.
|
|
|
|
Exceptions
|
|
----------
|
|
YosysError
|
|
Raised if Yosys returns a non-zero code. The exception message is the standard error
|
|
output.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
@classmethod
|
|
def _process_result(cls, returncode, stdout, stderr, ignore_warnings, src_loc_at):
|
|
if returncode:
|
|
raise YosysError(stderr.strip())
|
|
if not ignore_warnings:
|
|
for match in re.finditer(r"(?ms:^Warning: (.+)\n$)", stderr):
|
|
message = match.group(1).replace("\n", " ")
|
|
warnings.warn(message, YosysWarning, stacklevel=3 + src_loc_at)
|
|
return stdout
|
|
|
|
|
|
class _BuiltinYosys(YosysBinary):
|
|
YOSYS_PACKAGE = "nmigen_yosys"
|
|
|
|
@classmethod
|
|
def available(cls):
|
|
if importlib_metadata is None:
|
|
return False
|
|
try:
|
|
importlib_metadata.version(cls.YOSYS_PACKAGE)
|
|
return True
|
|
except importlib_metadata.PackageNotFoundError:
|
|
return False
|
|
|
|
@classmethod
|
|
def version(cls):
|
|
version = importlib_metadata.version(cls.YOSYS_PACKAGE)
|
|
match = re.match(r"^(\d+)\.(\d+)(?:\.post(\d+))?", version)
|
|
return (int(match[1]), int(match[2]), int(match[3] or 0))
|
|
|
|
@classmethod
|
|
def run(cls, args, stdin="", *, ignore_warnings=False, src_loc_at=0):
|
|
popen = subprocess.Popen([sys.executable, "-m", cls.YOSYS_PACKAGE, *args],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
encoding="utf-8")
|
|
stdout, stderr = popen.communicate(stdin)
|
|
return cls._process_result(popen.returncode, stdout, stderr, ignore_warnings, src_loc_at)
|
|
|
|
|
|
class _SystemYosys(YosysBinary):
|
|
YOSYS_BINARY = "yosys"
|
|
|
|
@classmethod
|
|
def available(cls):
|
|
return has_tool(cls.YOSYS_BINARY)
|
|
|
|
@classmethod
|
|
def version(cls):
|
|
version = cls.run(["-V"])
|
|
match = re.match(r"^Yosys (\d+)\.(\d+)(?:\+(\d+))?", version)
|
|
return (int(match[1]), int(match[2]), int(match[3] or 0))
|
|
|
|
@classmethod
|
|
def run(cls, args, stdin="", *, ignore_warnings=False, src_loc_at=0):
|
|
popen = subprocess.Popen([require_tool(cls.YOSYS_BINARY), *args],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
encoding="utf-8")
|
|
stdout, stderr = popen.communicate(stdin)
|
|
# If Yosys is built with an evaluation version of Verific, then Verific license
|
|
# information is printed first. It consists of empty lines and lines starting with `--`,
|
|
# which are not normally a part of Yosys output, and can be fairly safely removed.
|
|
#
|
|
# This is not ideal, but Verific license conditions rule out any other solution.
|
|
stdout = re.sub(r"\A(-- .+\n|\n)*", "", stdout)
|
|
return cls._process_result(popen.returncode, stdout, stderr, ignore_warnings, src_loc_at)
|
|
|
|
|
|
def find_yosys(requirement):
|
|
"""Find an available Yosys executable of required version.
|
|
|
|
Parameters
|
|
----------
|
|
requirement : function
|
|
Version check. Should return ``True`` if the version is acceptable, ``False`` otherwise.
|
|
|
|
Returns
|
|
-------
|
|
yosys_binary : subclass of YosysBinary
|
|
Proxy for running the requested version of Yosys.
|
|
|
|
Exceptions
|
|
----------
|
|
YosysError
|
|
Raised if required Yosys version is not found.
|
|
"""
|
|
proxies = []
|
|
clauses = os.environ.get("NMIGEN_USE_YOSYS", "system,builtin").split(",")
|
|
for clause in clauses:
|
|
if clause == "builtin":
|
|
proxies.append(_BuiltinYosys)
|
|
elif clause == "system":
|
|
proxies.append(_SystemYosys)
|
|
else:
|
|
raise YosysError("The NMIGEN_USE_YOSYS environment variable contains "
|
|
"an unrecognized clause {!r}"
|
|
.format(clause))
|
|
for proxy in proxies:
|
|
if proxy.available() and requirement(proxy.version()):
|
|
return proxy
|
|
else:
|
|
if "NMIGEN_USE_YOSYS" in os.environ:
|
|
raise YosysError("Could not find an acceptable Yosys binary. Searched: {}"
|
|
.format(", ".join(clauses)))
|
|
else:
|
|
raise YosysError("Could not find an acceptable Yosys binary. The `nmigen_yosys` PyPI "
|
|
"package, if available for this platform, can be used as fallback")
|