Teach check_tool_requirements to check for edalize versions

This is a patch by @rswarbrick ported from
https://github.com/lowRISC/ibex/pull/774 in order to align the
scripts between Ibex and OT.

Original commit message:

We need this specific edalize version because recent verilators have
got pickier about string parameter passing, breaking the
"MultiplierImplementation" parameter.

As well as teaching check_tool_requirements.py to get the edalize
version from pip3, this patch also does a bit of tidying up, coping
better if tool_requirements.py is missing or malformed.

Original author: Rupert Swarbrick <rswarbrick@lowrisc.org>

Signed-off-by: Philipp Wagner <phw@lowrisc.org>
diff --git a/util/check_tool_requirements.py b/util/check_tool_requirements.py
index 5a59d4c..3c6757d 100755
--- a/util/check_tool_requirements.py
+++ b/util/check_tool_requirements.py
@@ -6,15 +6,50 @@
 from distutils.version import StrictVersion
 import logging as log
 import os
+import re
 import subprocess
 import sys
 
 # Display INFO log messages and up.
 log.basicConfig(level=log.INFO, format="%(levelname)s: %(message)s")
 
-# Populate __TOOL_REQUIREMENTS__
-topsrcdir = os.path.join(os.path.dirname(__file__), '..')
-exec(open(os.path.join(topsrcdir, 'tool_requirements.py')).read())
+
+def get_tool_requirements_path():
+    '''Return the path to tool_requirements.py, at the top of the repo'''
+    # top_src_dir is the top of the repository
+    top_src_dir = os.path.normpath(os.path.join(os.path.dirname(__file__),
+                                                '..'))
+
+    return os.path.join(top_src_dir, 'tool_requirements.py')
+
+
+def read_tool_requirements(path=None):
+    '''Read tool requirements from a Python file'''
+    if path is None:
+        path = get_tool_requirements_path()
+
+    with open(path, 'r') as pyfile:
+        globs = {}
+        exec(pyfile.read(), globs)
+
+        # We expect the exec call to have populated globs with a
+        # __TOOL_REQUIREMENTS__ dictionary.
+        reqs = globs.get('__TOOL_REQUIREMENTS__')
+        if reqs is None:
+            log.error('The Python file at {} did not define '
+                      '__TOOL_REQUIREMENTS__.'
+                      .format(path))
+            return None
+
+        # reqs should be a dictionary (mapping tool name to minimum version)
+        if not isinstance(reqs, dict):
+            log.error('The Python file at {} defined '
+                      '__TOOL_REQUIREMENTS__, but it is not a dict.'
+                      .format(path))
+            return None
+
+        return reqs
+
 
 def get_verilator_version():
     try:
@@ -32,8 +67,44 @@
         log.error(e.stdout)
         return None
 
-def check_version(tool_name, required_version, actual_version):
-    if required_version is None or actual_version is None:
+
+def pip3_get_version(tool):
+    '''Run pip3 to find the version of an installed module'''
+    cmd = ['pip3', 'show', tool]
+    try:
+        proc = subprocess.run(cmd,
+                              check=True,
+                              stderr=subprocess.STDOUT,
+                              stdout=subprocess.PIPE,
+                              universal_newlines=True)
+    except subprocess.CalledProcessError as err:
+        log.error('pip3 command failed: {}'.format(err))
+        log.error("Failed to get version of {} with pip3: is it installed?"
+                  .format(tool))
+        log.error(err.stdout)
+        return None
+
+    version_re = 'Version: (.*)'
+    for line in proc.stdout.splitlines():
+        match = re.match(version_re, line)
+        if match:
+            return match.group(1)
+
+    # If we get here, we never saw a version line.
+    log.error('No output line from running {} started with "Version: ".'
+              .format(cmd))
+    return None
+
+
+def check_version(requirements, tool_name, getter):
+    required_version = requirements.get(tool_name)
+    if required_version is None:
+        log.error('Requirements file does not specify version for {}.'
+                  .format(tool_name))
+        return False
+
+    actual_version = getter()
+    if actual_version is None:
         return False
 
     if StrictVersion(actual_version) < StrictVersion(required_version):
@@ -47,17 +118,26 @@
 
 
 def main():
-    any_failed = False
+    # Get tool requirements
+    tool_requirements = read_tool_requirements()
+    if tool_requirements is None:
+        return 1
 
-    if not check_version('verilator', __TOOL_REQUIREMENTS__['verilator'],
-                         get_verilator_version()):
-        any_failed = True
+    all_good = True
+    all_good &= check_version(tool_requirements,
+                              'verilator',
+                              get_verilator_version)
+    all_good &= check_version(tool_requirements,
+                              'edalize',
+                              lambda: pip3_get_version('edalize'))
 
-    if any_failed:
+    if not all_good:
         log.error("Tool requirements not fulfilled. "
                   "Please update the tools and retry.")
         return 1
+
     return 0
 
+
 if __name__ == "__main__":
     sys.exit(main())