Improve compiler version detection

This patch adds a matcher for compiler versions in tool/util.py, to be
used in SConstruct. We now have a new `utils.CompilerInformation` class,
that can be created from the path of the compiler. It can then be
compared with a string describing the requirements:

    compiler = util.CompilerInformat("/path/to/c++")

    # Check the compiler is clang.
    if compiler == 'clang':
      # Check its version.
      if compiler == 'clang-3.8':
        # Valid only using clang version 3.8 exactly
      if compiler >= 'clang-3.6':
        # Add options that weren't supported in clang before 3.6

    # Check the compiler is at least gcc 5:
    if compiler >= 'gcc-5'
      # Add new options
    if compiler == 'gcc-4.8':
      # Workaround gcc 4.8

Finally, this patch uses this to make sure warnings about the `override`
keyword are supported.

Change-Id: I5c779ba88d6b476b12b66a86b966efe280ae2f8d
diff --git a/tools/util.py b/tools/util.py
index e869b40..2c11213 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -24,7 +24,9 @@
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+from distutils.version import LooseVersion
 import glob
+import operator
 import os
 import re
 import shlex
@@ -72,30 +74,115 @@
 def relrealpath(path, start=os.getcwd()):
   return os.path.relpath(os.path.realpath(path), start)
 
-# Query the target architecture of the compiler. The 'target' architecture of
-# the compiler used to build VIXL is considered to be the 'host' architecture of
-# VIXL itself.
-def GetHostArch(cxx):
+# Query the compiler about its preprocessor directives and return all of them as
+# a dictionnary.
+def GetCompilerDirectives(cxx):
   # Instruct the compiler to dump all its preprocessor macros.
   dump = subprocess.Popen([cxx, '-E', '-dM', '-'], stdin=subprocess.PIPE,
                           stdout=subprocess.PIPE)
   out, _ = dump.communicate()
-  macro_list = [
-    # Extract the macro name.
-    match.group(1)
+  return {
+    # Extract the macro name as key and value as element.
+    match.group(1): match.group(2)
     for match in [
       # Capture macro name.
-      re.search('#define (.*?) 1', macro)
+      re.search('^#define (\S+?) (.+)$', macro)
       for macro in out.split('\n')
     ]
     # Filter out non-matches.
     if match
-  ]
-  if "__x86_64__" in macro_list:
+  }
+
+# Query the target architecture of the compiler. The 'target' architecture of
+# the compiler used to build VIXL is considered to be the 'host' architecture of
+# VIXL itself.
+def GetHostArch(cxx):
+  directives = GetCompilerDirectives(cxx)
+  if "__x86_64__" in directives:
     return "x86_64"
-  elif "__arm__" in macro_list:
+  elif "__arm__" in directives:
     return "aarch32"
-  elif "__aarch64__" in macro_list:
+  elif "__aarch64__" in directives:
     return "aarch64"
   else:
     raise Exception("Unsupported archtecture")
+
+# Class representing the compiler toolchain and version.
+class CompilerInformation(object):
+  def __init__(self, cxx):
+    directives = GetCompilerDirectives(cxx)
+    if '__llvm__' in directives:
+      major = int(directives['__clang_major__'])
+      minor = int(directives['__clang_minor__'])
+      self.compiler = 'clang'
+      self.version = '{}.{}'.format(major, minor)
+    elif '__GNUC__' in directives:
+      major = int(directives['__GNUC__'])
+      minor = int(directives['__GNUC_MINOR__'])
+      self.compiler = 'gcc'
+      self.version = '{}.{}'.format(major, minor)
+    else:
+      # Allow other compilers to be used for building VIXL. However, one would
+      # need to teach this class how to extract version information in order to
+      # make use of it.
+      self.compiler = None
+      self.version = None
+
+  def GetDescription(self):
+    return "{}-{}".format(self.compiler, self.version)
+
+  def __str__(self):
+    return self.GetDescription()
+
+  # Compare string descriptions with our object. The semantics are:
+  #
+  # - Equality
+  #
+  #   If the description does not have a version number, then we compare the
+  #   compiler names. For instance, "clang-3.6" is still equal to "clang" but of
+  #   course is not to "gcc".
+  #
+  # - Ordering
+  #
+  #   Asking whether a compiler is lower than another depends on the version
+  #   number. What we are really asking here when using these operator is
+  #   "Is my compiler in the allowed range?". So with this in mind, comparing
+  #   two different compilers will always return false. If the compilers are the
+  #   same, then the version numbers are compared. Of course, we cannot use
+  #   ordering operators if no version number is provided.
+
+  def __eq__(self, description):
+    if description == self.GetDescription():
+      return True
+    else:
+      # The user may not have provided a version, let's see if it matches the
+      # compiler.
+      return self.compiler == description
+
+  def __ne__(self, description):
+    return not self.__eq__(description)
+
+  def __lt__(self, description):
+    return self.CompareVersion(operator.lt, description)
+
+  def __le__(self, description):
+    return self.CompareVersion(operator.le, description)
+
+  def __ge__(self, description):
+    return self.CompareVersion(operator.ge, description)
+
+  def __gt__(self, description):
+    return self.CompareVersion(operator.gt, description)
+
+  # Comparing the provided `description` string, in the form of
+  # "{compiler}-{major}.{minor}". The comparison is done using the provided
+  # `operator` argument.
+  def CompareVersion(self, operator, description):
+    match = re.search('^(\S+)-(.*?)$', description)
+    if not match:
+      raise Exception("A version number is required when comparing compilers")
+    compiler, version = match.group(1), match.group(2)
+    # The result is false if the compilers are different, otherwise compare the
+    # version numbers.
+    return self.compiler == compiler and \
+           operator(LooseVersion(self.version), LooseVersion(version))