Yavor Goulishev | 9c08e84 | 2020-04-29 14:03:33 -0700 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | # Copyright 2017 The Chromium Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | import _winreg |
| 8 | import os |
| 9 | import re |
| 10 | import subprocess |
| 11 | import sys |
| 12 | |
| 13 | |
| 14 | def _RegistryGetValue(key, value): |
| 15 | """Use the _winreg module to obtain the value of a registry key. |
| 16 | |
| 17 | Args: |
| 18 | key: The registry key. |
| 19 | value: The particular registry value to read. |
| 20 | Return: |
| 21 | contents of the registry key's value, or None on failure. |
| 22 | """ |
| 23 | try: |
| 24 | root, subkey = key.split('\\', 1) |
| 25 | assert root == 'HKLM' # Only need HKLM for now. |
| 26 | with _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, subkey) as hkey: |
| 27 | return _winreg.QueryValueEx(hkey, value)[0] |
| 28 | except WindowsError: |
| 29 | return None |
| 30 | |
| 31 | |
| 32 | def _ExtractImportantEnvironment(output_of_set): |
| 33 | """Extracts environment variables required for the toolchain to run from |
| 34 | a textual dump output by the cmd.exe 'set' command.""" |
| 35 | envvars_to_save = ( |
| 36 | 'include', |
| 37 | 'lib', |
| 38 | 'libpath', |
| 39 | 'path', |
| 40 | 'pathext', |
| 41 | 'systemroot', |
| 42 | 'temp', |
| 43 | 'tmp', |
| 44 | ) |
| 45 | env = {} |
| 46 | for line in output_of_set.splitlines(): |
| 47 | for envvar in envvars_to_save: |
| 48 | if re.match(envvar + '=', line.lower()): |
| 49 | var, setting = line.split('=', 1) |
| 50 | env[var.upper()] = setting |
| 51 | break |
| 52 | for required in ('SYSTEMROOT', 'TEMP', 'TMP'): |
| 53 | if required not in env: |
| 54 | raise Exception('Environment variable "%s" ' |
| 55 | 'required to be set to valid path' % required) |
| 56 | return env |
| 57 | |
| 58 | |
| 59 | def _FormatAsEnvironmentBlock(envvar_dict): |
| 60 | """Format as an 'environment block' directly suitable for CreateProcess. |
| 61 | Briefly this is a list of key=value\0, terminated by an additional \0. See |
| 62 | CreateProcess() documentation for more details.""" |
| 63 | block = '' |
| 64 | nul = '\0' |
| 65 | for key, value in envvar_dict.iteritems(): |
| 66 | block += key + '=' + value + nul |
| 67 | block += nul |
| 68 | return block |
| 69 | |
| 70 | |
| 71 | def _GenerateEnvironmentFiles(install_dir, out_dir, script_path): |
| 72 | """It's not sufficient to have the absolute path to the compiler, linker, etc. |
| 73 | on Windows, as those tools rely on .dlls being in the PATH. We also need to |
| 74 | support both x86 and x64 compilers. Different architectures require a |
| 75 | different compiler binary, and different supporting environment variables |
| 76 | (INCLUDE, LIB, LIBPATH). So, we extract the environment here, wrap all |
| 77 | invocations of compiler tools (cl, link, lib, rc, midl, etc.) to set up the |
| 78 | environment, and then do not prefix the compiler with an absolute path, |
| 79 | instead preferring something like "cl.exe" in the rule which will then run |
| 80 | whichever the environment setup has put in the path.""" |
| 81 | archs = ('x86', 'amd64', 'arm64') |
| 82 | result = [] |
| 83 | for arch in archs: |
| 84 | # Extract environment variables for subprocesses. |
| 85 | args = [os.path.join(install_dir, script_path)] |
| 86 | script_arch_name = arch |
| 87 | if script_path.endswith('SetEnv.cmd') and arch == 'amd64': |
| 88 | script_arch_name = '/x64' |
| 89 | if arch == 'arm64': |
| 90 | script_arch_name = 'x86_arm64' |
| 91 | args.extend((script_arch_name, '&&', 'set')) |
| 92 | popen = subprocess.Popen( |
| 93 | args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| 94 | variables, _ = popen.communicate() |
| 95 | if popen.returncode != 0: |
| 96 | raise Exception('"%s" failed with error %d' % (args, popen.returncode)) |
| 97 | env = _ExtractImportantEnvironment(variables) |
| 98 | |
| 99 | env_block = _FormatAsEnvironmentBlock(env) |
| 100 | basename = 'environment.' + arch |
| 101 | with open(os.path.join(out_dir, basename), 'wb') as f: |
| 102 | f.write(env_block) |
| 103 | result.append(basename) |
| 104 | return result |
| 105 | |
| 106 | |
| 107 | def _GetEnvAsDict(arch): |
| 108 | """Gets the saved environment from a file for a given architecture.""" |
| 109 | # The environment is saved as an "environment block" (see CreateProcess() |
| 110 | # for details, which is the format required for ninja). We convert to a dict |
| 111 | # here. Drop last 2 NULs, one for list terminator, one for trailing vs. |
| 112 | # separator. |
| 113 | pairs = open(arch).read()[:-2].split('\0') |
| 114 | kvs = [item.split('=', 1) for item in pairs] |
| 115 | return dict(kvs) |
| 116 | |
| 117 | |
| 118 | class WinTool(object): |
| 119 | def Dispatch(self, args): |
| 120 | """Dispatches a string command to a method.""" |
| 121 | if len(args) < 1: |
| 122 | raise Exception("Not enough arguments") |
| 123 | |
| 124 | method = "Exec%s" % self._CommandifyName(args[0]) |
| 125 | return getattr(self, method)(*args[1:]) |
| 126 | |
| 127 | def _CommandifyName(self, name_string): |
| 128 | """Transforms a tool name like recursive-mirror to RecursiveMirror.""" |
| 129 | return name_string.title().replace('-', '') |
| 130 | |
| 131 | def ExecLinkWrapper(self, arch, *args): |
| 132 | """Filter diagnostic output from link that looks like: |
| 133 | ' Creating library ui.dll.lib and object ui.dll.exp' |
| 134 | This happens when there are exports from the dll or exe. |
| 135 | """ |
| 136 | env = _GetEnvAsDict(arch) |
| 137 | args = list(args) # *args is a tuple by default, which is read-only. |
| 138 | args[0] = args[0].replace('/', '\\') |
| 139 | link = subprocess.Popen(args, env=env, shell=True, stdout=subprocess.PIPE) |
| 140 | out, _ = link.communicate() |
| 141 | for line in out.splitlines(): |
| 142 | if (not line.startswith(' Creating library ') and |
| 143 | not line.startswith('Generating code') and |
| 144 | not line.startswith('Finished generating code')): |
| 145 | print line |
| 146 | return link.returncode |
| 147 | |
| 148 | def ExecAsmWrapper(self, arch, *args): |
| 149 | """Filter logo banner from invocations of asm.exe.""" |
| 150 | env = _GetEnvAsDict(arch) |
| 151 | popen = subprocess.Popen(args, env=env, shell=True, |
| 152 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| 153 | out, _ = popen.communicate() |
| 154 | for line in out.splitlines(): |
| 155 | if (not line.startswith('Copyright (C) Microsoft Corporation') and |
| 156 | not line.startswith('Microsoft (R) Macro Assembler') and |
| 157 | not line.startswith(' Assembling: ') and |
| 158 | line): |
| 159 | print line |
| 160 | return popen.returncode |
| 161 | |
| 162 | def ExecGetVisualStudioData(self, outdir, toolchain_path): |
| 163 | setenv_path = os.path.join('win_sdk', 'bin', 'SetEnv.cmd') |
| 164 | |
| 165 | def explicit(): |
| 166 | if os.path.exists(os.path.join(toolchain_path, setenv_path)): |
| 167 | return toolchain_path, setenv_path |
| 168 | |
| 169 | def env(): |
| 170 | from_env = os.environ.get('VSINSTALLDIR') |
| 171 | if from_env and os.path.exists(os.path.join(from_env, setenv_path)): |
| 172 | return from_env, setenv_path |
| 173 | |
| 174 | def autodetect(): |
| 175 | # Try vswhere, which will find VS2017.2+. Note that earlier VS2017s will |
| 176 | # not be found. |
| 177 | vswhere_path = os.path.join(os.environ.get('ProgramFiles(x86)'), |
| 178 | 'Microsoft Visual Studio', 'Installer', 'vswhere.exe') |
| 179 | if os.path.exists(vswhere_path): |
| 180 | installation_path = subprocess.check_output( |
| 181 | [vswhere_path, '-latest', '-property', 'installationPath']).strip() |
| 182 | if installation_path: |
| 183 | return (installation_path, |
| 184 | os.path.join('VC', 'Auxiliary', 'Build', 'vcvarsall.bat')) |
| 185 | |
| 186 | # Otherwise, try VS2015. |
| 187 | version = '14.0' |
| 188 | keys = [r'HKLM\Software\Microsoft\VisualStudio\%s' % version, |
| 189 | r'HKLM\Software\Wow6432Node\Microsoft\VisualStudio\%s' % version] |
| 190 | for key in keys: |
| 191 | path = _RegistryGetValue(key, 'InstallDir') |
| 192 | if not path: |
| 193 | continue |
| 194 | return (os.path.normpath(os.path.join(path, os.pardir, os.pardir)), |
| 195 | os.path.join('VC', 'vcvarsall.bat')) |
| 196 | |
| 197 | def fail(): raise Exception('Visual Studio installation dir not found') |
| 198 | |
| 199 | # Use an explicitly specified toolchain path, if provided and found. |
| 200 | # Otherwise, try using a standard environment variable. Finally, try |
| 201 | # autodetecting using vswhere. |
| 202 | install_dir, script_path = (explicit() or env() or autodetect() or fail()) |
| 203 | |
| 204 | x86_file, x64_file, arm64_file = _GenerateEnvironmentFiles( |
| 205 | install_dir, outdir, script_path) |
| 206 | result = '''install_dir = "%s" |
| 207 | x86_environment_file = "%s" |
| 208 | x64_environment_file = "%s" |
| 209 | arm64_environment_file = "%s"''' % (install_dir, x86_file, x64_file, arm64_file) |
| 210 | print result |
| 211 | return 0 |
| 212 | |
| 213 | def ExecStamp(self, path): |
| 214 | """Simple stamp command.""" |
| 215 | open(path, 'w').close() |
| 216 | return 0 |
| 217 | |
| 218 | |
| 219 | if __name__ == '__main__': |
| 220 | sys.exit(WinTool().Dispatch(sys.argv[1:])) |