--- /dev/null
+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")