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
 
 
 
 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
 """.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,
 
 
 
 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)
               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
 
         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.
 
 from ..hdl.ast import *
 from ..hdl.ir import *
 from ..back import rtlil
-from .._toolchain import get_tool
+from .._toolchain import require_tool
 
 
 __all__ = ["FHDLTestCase"]
             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)
 
         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":
 
     device  = abstractproperty()
     package = abstractproperty()
 
+    required_tools = [
+        "yosys",
+        "nextpnr-ice40",
+        "icepack",
+    ]
+
     _nextpnr_device_options = {
         "iCE40LP384": "--lp384",
         "iCE40LP1K":  "--lp1k",
 
     package = abstractproperty()
     speed   = abstractproperty()
 
+    required_tools = ["vivado"]
+
     file_templates = {
         **TemplatedPlatform.build_script_templates,
         "{{name}}.v": r"""
 
     package = abstractproperty()
     speed   = abstractproperty()
 
+    required_tools = [
+        "xst",
+        "ngdbuild",
+        "map",
+        "par",
+        "bitgen",
+    ]
+
     @property
     def family(self):
         device = self.device.upper()