220 lines
6.8 KiB
Python
220 lines
6.8 KiB
Python
import os
|
|
import sys
|
|
import re
|
|
import subprocess
|
|
import warnings
|
|
import pathlib
|
|
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
|
|
try:
|
|
from importlib import resources as importlib_resources
|
|
try:
|
|
importlib_resources.files # py3.9+ stdlib
|
|
except AttributeError:
|
|
import importlib_resources # py3.8- shim
|
|
except ImportError:
|
|
import importlib_resources # py3.6- shim
|
|
|
|
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 data_dir(cls):
|
|
"""Get Yosys data directory.
|
|
|
|
Returns
|
|
-------
|
|
data_dir : pathlib.Path
|
|
Yosys data directory (also known as "datdir").
|
|
"""
|
|
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 data_dir(cls):
|
|
return importlib_resources.files(cls.YOSYS_PACKAGE) / "share"
|
|
|
|
@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 data_dir(cls):
|
|
popen = subprocess.Popen([require_tool(cls.YOSYS_BINARY) + "-config", "--datdir"],
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
encoding="utf-8")
|
|
stdout, stderr = popen.communicate()
|
|
if popen.returncode:
|
|
raise YosysError(stderr.strip())
|
|
return pathlib.Path(stdout.strip())
|
|
|
|
@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")
|