Bug 1289286 - Automatically find MSVC from the registry if it's not in $PATH. r?gps draft
authorMike Hommey <mh+mozilla@glandium.org>
Fri, 22 Jul 2016 15:51:34 +0900
changeset 392733 96fd422cc15a50a6214264538866a5f2238d14ca
parent 392647 944dc7380c63ba149c1b0614845015273bcc590a
child 392734 d2d505b48fa6ced1684e05ecec994feba36c927c
push id24107
push userbmo:mh+mozilla@glandium.org
push dateTue, 26 Jul 2016 05:57:40 +0000
reviewersgps
bugs1289286
milestone50.0a1
Bug 1289286 - Automatically find MSVC from the registry if it's not in $PATH. r?gps
build/moz.configure/toolchain.configure
build/moz.configure/util.configure
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -395,16 +395,92 @@ def check_compiler(compiler, language, t
         version=info.version,
         target_cpu=info.cpu,
         target_kernel=info.kernel,
         target_endianness=info.endianness,
         flags=flags,
     )
 
 
+@imports(_from='collections', _import='defaultdict')
+@imports(_from='__builtin__', _import='sorted')
+def get_vc_paths(base):
+    vc = defaultdict(lambda: defaultdict(dict))
+    subkey = r'Microsoft\VisualStudio\VC\*\*\*\Compiler'
+    for v, h, t, p in get_registry_values(base + '\\' + subkey):
+        vc[v][h][t] = p
+    if not vc:
+        return
+    version, data = sorted(vc.iteritems(), key=lambda x: Version(x[0]))[-1]
+    return data
+
+
+@depends(host)
+@imports('platform')
+def vc_compiler_path(host):
+    if host.kernel != 'WINNT':
+        return
+    vc_host = {
+        'x86': 'x86',
+        'AMD64': 'x64',
+    }.get(platform.machine())
+    if vc_host is None:
+        return
+    vc_target = {
+        'x86': 'x86',
+        'x86_64': 'x64',
+        'arm': 'arm',
+    }.get(host.cpu)
+    if vc_target is None:
+        return
+
+    base_key = r'HKEY_LOCAL_MACHINE\SOFTWARE'
+    data = get_vc_paths(base_key)
+    if not data:
+        data = get_vc_paths(base_key + r'\Wow6432Node')
+    if not data:
+        return
+
+    path = data.get(vc_host, {}).get(vc_target)
+    if not path and vc_host == 'x64':
+        vc_host = 'x86'
+        path = data.get(vc_host, {}).get(vc_target)
+    if not path:
+        return
+    path = os.path.dirname(path)
+    if vc_host != vc_target:
+        other_path = data.get(vc_host, {}).get(vc_host)
+        if other_path:
+            return (path, os.path.dirname(other_path))
+    return (path,)
+
+
+@depends(vc_compiler_path)
+@imports('os')
+def toolchain_search_path(vc_compiler_path):
+    if vc_compiler_path:
+        result = [os.environ.get('PATH')]
+        result.extend(vc_compiler_path)
+        return result
+
+
+# Normally, we'd just have CC, etc. set to absolute paths, but the build system
+# doesn't currently handle properly the case where the paths contain spaces.
+# Additionally, there's the issue described further below in valid_compiler().
+@depends(toolchain_search_path)
+@imports('os')
+def alter_path(toolchain_search_path):
+    if toolchain_search_path:
+        path = os.pathsep.join(toolchain_search_path)
+        os.environ['PATH'] = path
+        return path
+
+set_config('PATH', alter_path)
+
+
 @template
 def default_c_compilers(host_or_target):
     '''Template defining the set of default C compilers for the host and
     target platforms.
     `host_or_target` is either `host` or `target` (the @depends functions
     from init.configure.
     '''
     assert host_or_target in (host, target)
@@ -536,17 +612,18 @@ def compiler(language, host_or_target, c
                     for k, v in other_compiler.__dict__.iteritems()
                 })
 
     # Normally, we'd use `var` instead of `_var`, but the interaction with
     # old-configure complicates things, and for now, we a) can't take the plain
     # result from check_prog as CC/CXX/HOST_CC/HOST_CXX and b) have to let
     # old-configure AC_SUBST it (because it's autoconf doing it, not us)
     compiler = check_prog('_%s' % var, what=what, progs=default_compilers,
-                          input=delayed_getattr(provided_compiler, 'compiler'))
+                          input=delayed_getattr(provided_compiler, 'compiler'),
+                          paths=toolchain_search_path)
 
     @depends(compiler, provided_compiler, compiler_wrapper, host_or_target)
     @checking('whether %s can be used' % what, lambda x: bool(x))
     @imports(_from='mozbuild.shellutil', _import='quote')
     def valid_compiler(compiler, provided_compiler, compiler_wrapper,
                        host_or_target):
         wrapper = list(compiler_wrapper or ())
         if provided_compiler:
--- a/build/moz.configure/util.configure
+++ b/build/moz.configure/util.configure
@@ -87,16 +87,115 @@ def find_program(file, paths=None):
 
 def unique_list(l):
     result = []
     for i in l:
         if l not in result:
             result.append(i)
     return result
 
+
+# Get values out of the Windows registry. This function can only be called on
+# Windows.
+# The `pattern` argument is a string starting with HKEY_ and giving the full
+# "path" of the registry key to get the value for, with backslash separators.
+# The string can contains wildcards ('*').
+# The result of this functions is an enumerator yielding tuples for each
+# match. Each of these tuples contains the key name matching wildcards
+# followed by the value.
+#
+# Examples:
+#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
+#                       r'Windows Kits\Installed Roots\KitsRoot*')
+#   yields e.g.:
+#     ('KitsRoot81', r'C:\Program Files (x86)\Windows Kits\8.1\')
+#     ('KitsRoot10', r'C:\Program Files (x86)\Windows Kits\10\')
+#
+#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
+#                       r'Windows Kits\Installed Roots\KitsRoot8.1')
+#   yields e.g.:
+#     (r'C:\Program Files (x86)\Windows Kits\8.1\',)
+#
+#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
+#                       r'Windows Kits\*\KitsRoot*')
+#   yields e.g.:
+#     ('Installed Roots', 'KitsRoot81',
+#      r'C:\Program Files (x86)\Windows Kits\8.1\')
+#     ('Installed Roots', 'KitsRoot10',
+#      r'C:\Program Files (x86)\Windows Kits\10\')
+#
+#   get_registry_values(r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\'
+#                       r'VisualStudio\VC\*\x86\*\Compiler')
+#   yields e.g.:
+#     ('19.0', 'arm', r'C:\...\amd64_arm\cl.exe')
+#     ('19.0', 'x64', r'C:\...\amd64\cl.exe')
+#     ('19.0', 'x86', r'C:\...\amd64_x86\cl.exe')
+@imports(_import='_winreg', _as='winreg')
+@imports(_from='__builtin__', _import='WindowsError')
+@imports(_from='fnmatch', _import='fnmatch')
+def get_registry_values(pattern):
+    def enum_helper(func, key):
+        i = 0
+        while True:
+            try:
+                yield func(key, i)
+            except WindowsError:
+                break
+            i += 1
+
+    def get_keys(key, pattern):
+        try:
+            s = winreg.OpenKey(key, '\\'.join(pattern[:-1]))
+        except WindowsError:
+            return
+        for k in enum_helper(winreg.EnumKey, s):
+            if fnmatch(k, pattern[-1]):
+                try:
+                    yield k, winreg.OpenKey(s, k)
+                except WindowsError:
+                    pass
+
+    def get_values(key, pattern):
+        try:
+            s = winreg.OpenKey(key, '\\'.join(pattern[:-1]))
+        except WindowsError:
+            return
+        for k, v, t in enum_helper(winreg.EnumValue, s):
+            if fnmatch(k, pattern[-1]):
+                yield k, v
+
+    def split_pattern(pattern):
+        subpattern = []
+        for p in pattern:
+            subpattern.append(p)
+            if '*' in p:
+                yield subpattern
+                subpattern = []
+        if subpattern:
+            yield subpattern
+
+    pattern = pattern.split('\\')
+    assert pattern[0].startswith('HKEY_')
+    keys = [(getattr(winreg, pattern[0]),)]
+    pattern = list(split_pattern(pattern[1:]))
+    for i, p in enumerate(pattern):
+        next_keys = []
+        for base_key in keys:
+            matches = base_key[:-1]
+            base_key = base_key[-1]
+            if i == len(pattern) - 1:
+                want_name = '*' in p[-1]
+                for name, value in get_values(base_key, p):
+                    yield matches + ((name, value) if want_name else (value,))
+            else:
+                for name, k in get_keys(base_key, p):
+                    next_keys.append(matches + (name, k))
+        keys = next_keys
+
+
 @imports(_from='mozbuild.configure.util', _import='Version', _as='_Version')
 def Version(v):
     'A version number that can be compared usefully.'
     return _Version(v)
 
 # Denotes a deprecated option. Combines option() and @depends:
 # @deprecated_option('--option')
 # def option(value):