diff --git a/nmigen/_yosys.py b/nmigen/_yosys.py new file mode 100644 index 0000000..913d5cc --- /dev/null +++ b/nmigen/_yosys.py @@ -0,0 +1,176 @@ +import os +import sys +import re +import subprocess +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 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 + + +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=""): + 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) + if popen.returncode: + raise YosysError(stderr.strip()) + else: + return stdout + + +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=""): + 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) + if popen.returncode: + raise YosysError(stderr.strip()) + else: + return stdout + + +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") diff --git a/nmigen/back/verilog.py b/nmigen/back/verilog.py index 04e19b6..3c7be2b 100644 --- a/nmigen/back/verilog.py +++ b/nmigen/back/verilog.py @@ -3,31 +3,17 @@ import re import subprocess import itertools -from .._toolchain import * +from .._yosys import * from . import rtlil __all__ = ["YosysError", "convert", "convert_fragment"] -class YosysError(Exception): - pass - - -def _yosys_version(): - yosys_path = require_tool("yosys") - version = subprocess.check_output([yosys_path, "-V"], encoding="utf-8") - # If Yosys is built with Verific, then Verific license information is printed first. - # See below for details. - m = re.search(r"^Yosys ([\d.]+)(?:\+(\d+))?", version, flags=re.M) - tag, offset = m[1], m[2] or 0 - return tuple(map(int, tag.split("."))), offset - - def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog_opts=()): - version, offset = _yosys_version() - if version < (0, 9): - raise YosysError("Yosys {}.{} is not supported".format(*version)) + # this version requirement needs to be synchronized with the one in setup.py! + yosys = find_yosys(lambda ver: ver >= (0, 9)) + yosys_version = yosys.version() attr_map = [] if strip_internal_attrs: @@ -37,7 +23,7 @@ def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog attr_map.append("-remove nmigen.hierarchy") attr_map.append("-remove nmigen.decoding") - script = """ + return yosys.run(["-q", "-"], """ # Convert nMigen's RTLIL to readable Verilog. read_ilang <=0.9.*"], + }, packages=find_packages(), entry_points={ "console_scripts": [