From: whitequark Date: Thu, 2 Jul 2020 18:26:08 +0000 (+0000) Subject: _yosys→_toolchain.yosys X-Git-Url: https://git.libre-soc.org/?a=commitdiff_plain;h=be5aabb5c721a430175eba8b1f71cf55afe0e3b3;p=nmigen.git _yosys→_toolchain.yosys --- diff --git a/nmigen/_toolchain.py b/nmigen/_toolchain.py deleted file mode 100644 index fca2bac..0000000 --- a/nmigen/_toolchain.py +++ /dev/null @@ -1,37 +0,0 @@ -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 diff --git a/nmigen/_toolchain/__init__.py b/nmigen/_toolchain/__init__.py new file mode 100644 index 0000000..fca2bac --- /dev/null +++ b/nmigen/_toolchain/__init__.py @@ -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 diff --git a/nmigen/_toolchain/yosys.py b/nmigen/_toolchain/yosys.py new file mode 100644 index 0000000..c223306 --- /dev/null +++ b/nmigen/_toolchain/yosys.py @@ -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") diff --git a/nmigen/_yosys.py b/nmigen/_yosys.py deleted file mode 100644 index e84da53..0000000 --- a/nmigen/_yosys.py +++ /dev/null @@ -1,229 +0,0 @@ -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 ._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 - ------- - ``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") diff --git a/nmigen/back/cxxrtl.py b/nmigen/back/cxxrtl.py index 38cb41b..f7c18ec 100644 --- a/nmigen/back/cxxrtl.py +++ b/nmigen/back/cxxrtl.py @@ -1,4 +1,4 @@ -from .._yosys import * +from .._toolchain.yosys import * from . import rtlil diff --git a/nmigen/back/verilog.py b/nmigen/back/verilog.py index 1d2cb4a..31db02b 100644 --- a/nmigen/back/verilog.py +++ b/nmigen/back/verilog.py @@ -1,4 +1,4 @@ -from .._yosys import * +from .._toolchain.yosys import * from . import rtlil