_yosys→_toolchain.yosys
This commit is contained in:
parent
6bfff25e76
commit
369bc3e307
4 changed files with 3 additions and 3 deletions
37
nmigen/_toolchain/__init__.py
Normal file
37
nmigen/_toolchain/__init__.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
__all__ = ["ToolNotFound", "tool_env_var", "has_tool", "require_tool"]
|
||||
|
||||
|
||||
class ToolNotFound(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def tool_env_var(name):
|
||||
return name.upper().replace("-", "_")
|
||||
|
||||
|
||||
def _get_tool(name):
|
||||
return os.environ.get(tool_env_var(name), name)
|
||||
|
||||
|
||||
def has_tool(name):
|
||||
return shutil.which(_get_tool(name)) is not None
|
||||
|
||||
|
||||
def require_tool(name):
|
||||
env_var = tool_env_var(name)
|
||||
path = _get_tool(name)
|
||||
if shutil.which(path) is None:
|
||||
if env_var in os.environ:
|
||||
raise ToolNotFound("Could not find required tool {} in {} as "
|
||||
"specified via the {} environment variable".
|
||||
format(name, path, env_var))
|
||||
else:
|
||||
raise ToolNotFound("Could not find required tool {} in PATH. Place "
|
||||
"it directly in PATH or specify path explicitly "
|
||||
"via the {} environment variable".
|
||||
format(name, env_var))
|
||||
return path
|
||||
229
nmigen/_toolchain/yosys.py
Normal file
229
nmigen/_toolchain/yosys.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
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:
|
||||
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
|
||||
except ImportError:
|
||||
importlib_resources = None
|
||||
|
||||
from . 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
|
||||
-------
|
||||
``None`` if version number could not be determined, or a 3-tuple ``(major, minor, distance)`` if it could.
|
||||
|
||||
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 or importlib_resources 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)
|
||||
if match:
|
||||
return (int(match[1]), int(match[2]), int(match[3] or 0))
|
||||
else:
|
||||
return None
|
||||
|
||||
@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():
|
||||
version = proxy.version()
|
||||
if version is not None and requirement(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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue