blob: a5e1fdd4c49d02e23024a75587738cc3d713a8c8 [file] [log] [blame]
Yavor Goulishev9c08e842020-04-29 14:03:33 -07001#!/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
7import _winreg
8import os
9import re
10import subprocess
11import sys
12
13
14def _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
32def _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
59def _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
71def _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
107def _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
118class 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"
207x86_environment_file = "%s"
208x64_environment_file = "%s"
209arm64_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
219if __name__ == '__main__':
220 sys.exit(WinTool().Dispatch(sys.argv[1:]))