_toolchain,build.plat,vendor.*: add required_tools list and checks.
authorEmily <vcs@emily.moe>
Fri, 30 Aug 2019 23:27:22 +0000 (00:27 +0100)
committerwhitequark <cz@m-labs.hk>
Sat, 31 Aug 2019 00:05:47 +0000 (00:05 +0000)
nmigen/_toolchain.py
nmigen/back/verilog.py
nmigen/build/plat.py
nmigen/test/tools.py
nmigen/vendor/lattice_ecp5.py
nmigen/vendor/lattice_ice40.py
nmigen/vendor/xilinx_7series.py
nmigen/vendor/xilinx_spartan_3_6.py

index 43fd53dd436277f1d60f6b8fcf3900312bc788af..4878a3261a659cb505df62d7a609e0cae2e17485 100644 (file)
@@ -1,11 +1,44 @@
 import os
+import shutil
 
 
-__all__ = ["get_tool"]
+__all__ = ["ToolNotFound", "get_tool", "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(name.upper().replace("-", "_"), overrides.get(name, name))
+    return os.environ.get(_tool_env_var(name), overrides.get(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 path == name:
+            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))
+        else:
+            if os.getenv(env_var):
+                via = "the {} environment variable".format(env_var)
+            else:
+                via = "your packager's toolchain overrides. This is either an " \
+                      "nMigen bug or a packaging error"
+            raise ToolNotFound("Could not find required tool {} in {} as "
+                               "specified via {}".format(name, path, via))
+    return path
 
 
 # Packages for systems like Nix can inject full paths to certain tools by adding them in
index 190f31a3788f3554480c47f0f46f3037873006b0..d1a403226f0d9fb216c4a0da0a49e69ab3041b82 100644 (file)
@@ -14,19 +14,8 @@ class YosysError(Exception):
 
 
 def _yosys_version():
-    yosys_path = get_tool("yosys")
-    try:
-        version = subprocess.check_output([yosys_path, "-V"], encoding="utf-8")
-    except FileNotFoundError as e:
-        if os.getenv("YOSYS"):
-            raise YosysError("Could not find Yosys in {} as specified via the YOSYS environment "
-                             "variable".format(os.getenv("YOSYS"))) from e
-        elif yosys_path == "yosys":
-            raise YosysError("Could not find Yosys in PATH. Place `yosys` in PATH or specify "
-                             "path explicitly via the YOSYS environment variable") from e
-        else:
-            raise
-
+    yosys_path = require_tool("yosys")
+    version = subprocess.check_output([yosys_path, "-V"], encoding="utf-8")
     m = re.match(r"^Yosys ([\d.]+)(?:\+(\d+))?", version)
     tag, offset = m[1], m[2] or 0
     return tuple(map(int, tag.split("."))), offset
@@ -57,7 +46,7 @@ write_verilog -norename
 """.format(il_text, " ".join(attr_map),
            prune="# " if version == (0, 9) and offset == 0 else "")
 
-    popen = subprocess.Popen([os.getenv("YOSYS", "yosys"), "-q", "-"],
+    popen = subprocess.Popen([require_tool("yosys"), "-q", "-"],
         stdin=subprocess.PIPE,
         stdout=subprocess.PIPE,
         stderr=subprocess.PIPE,
index 477e17a6f2f1f40d12688f302af9fe231f28421f..1c35cf60df4f9d38d839b5f427185387618a0a1a 100644 (file)
@@ -20,10 +20,11 @@ __all__ = ["Platform", "TemplatedPlatform"]
 
 
 class Platform(ResourceManager, metaclass=ABCMeta):
-    resources   = abstractproperty()
-    connectors  = abstractproperty()
-    default_clk = None
-    default_rst = None
+    resources      = abstractproperty()
+    connectors     = abstractproperty()
+    default_clk    = None
+    default_rst    = None
+    required_tools = abstractproperty()
 
     def __init__(self):
         super().__init__(self.resources, self.connectors)
@@ -63,6 +64,9 @@ class Platform(ResourceManager, metaclass=ABCMeta):
               build_dir="build", do_build=True,
               program_opts=None, do_program=False,
               **kwargs):
+        for tool in self.required_tools:
+            require_tool(tool)
+
         plan = self.prepare(elaboratable, name, **kwargs)
         if not do_build:
             return plan
@@ -73,6 +77,9 @@ class Platform(ResourceManager, metaclass=ABCMeta):
 
         self.toolchain_program(products, name, **(program_opts or {}))
 
+    def has_required_tools(self):
+        return all(has_tool(name) for name in self.required_tools)
+
     @abstractmethod
     def create_missing_domain(self, name):
         # Simple instantiation of a clock domain driven directly by the board clock and reset.
index ba9846f6a47572e9d6f142ece23d6224a99bfb4a..0329e43b047bcf9f195d37aaa5a4a977cf8db47b 100644 (file)
@@ -11,7 +11,7 @@ from contextlib import contextmanager
 from ..hdl.ast import *
 from ..hdl.ir import *
 from ..back import rtlil
-from .._toolchain import get_tool
+from .._toolchain import require_tool
 
 
 __all__ = ["FHDLTestCase"]
@@ -95,7 +95,7 @@ class FHDLTestCase(unittest.TestCase):
             script=script,
             rtlil=rtlil.convert(Fragment.get(spec, platform="formal"))
         )
-        with subprocess.Popen([get_tool("sby"), "-f", "-d", spec_name], cwd=spec_dir,
+        with subprocess.Popen([require_tool("sby"), "-f", "-d", spec_name], cwd=spec_dir,
                               universal_newlines=True,
                               stdin=subprocess.PIPE, stdout=subprocess.PIPE) as proc:
             stdout, stderr = proc.communicate(config)
index 25dc24ccb52af0722b3180a3e6a5976c99ebefcb..2497f9918fea22364342885bf172e70a9fc6e5db 100644 (file)
@@ -239,6 +239,14 @@ class LatticeECP5Platform(TemplatedPlatform):
         assert toolchain in ("Trellis", "Diamond")
         self.toolchain = toolchain
 
+    @property
+    def required_tools(self):
+        if self.toolchain == "Trellis":
+            return ["yosys", "nextpnr-ecp5", "ecppack"]
+        if self.toolchain == "Diamond":
+            return ["pnmainc", "ddtcmd"]
+        assert False
+
     @property
     def file_templates(self):
         if self.toolchain == "Trellis":
index 21e9297517d60651497f7140f30a35ed4ab99781..e403341f4af0f4e62dd497330a81dfebdbe52439 100644 (file)
@@ -39,6 +39,12 @@ class LatticeICE40Platform(TemplatedPlatform):
     device  = abstractproperty()
     package = abstractproperty()
 
+    required_tools = [
+        "yosys",
+        "nextpnr-ice40",
+        "icepack",
+    ]
+
     _nextpnr_device_options = {
         "iCE40LP384": "--lp384",
         "iCE40LP1K":  "--lp1k",
index be5901746b80cca9d6074b2f5d41c53a7a90af1d..9529597e51c93ee2218787472dcaf24728e50033 100644 (file)
@@ -50,6 +50,8 @@ class Xilinx7SeriesPlatform(TemplatedPlatform):
     package = abstractproperty()
     speed   = abstractproperty()
 
+    required_tools = ["vivado"]
+
     file_templates = {
         **TemplatedPlatform.build_script_templates,
         "{{name}}.v": r"""
index 050a3e30bfc07d35063302c4b8add327e7d1c6a5..9f152dca7788d7ea92eab6941e4dd648c0218b0e 100644 (file)
@@ -57,6 +57,14 @@ class XilinxSpartan3Or6Platform(TemplatedPlatform):
     package = abstractproperty()
     speed   = abstractproperty()
 
+    required_tools = [
+        "xst",
+        "ngdbuild",
+        "map",
+        "par",
+        "bitgen",
+    ]
+
     @property
     def family(self):
         device = self.device.upper()