1# Copyright 2013 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4# 5# Copies the given "win tool" (which the toolchain uses to wrap compiler 6# invocations) and the environment blocks for the 32-bit and 64-bit builds on 7# Windows to the build directory. 8# 9# The arguments are the visual studio install location and the location of the 10# win tool. The script assumes that the root build directory is the current dir 11# and the files will be written to the current directory. 12 13 14import errno 15import json 16import os 17import re 18import subprocess 19import sys 20 21sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) 22import gn_helpers 23 24SCRIPT_DIR = os.path.dirname(__file__) 25SDK_VERSION = '10.0.22621.0' 26 27 28def _ExtractImportantEnvironment(output_of_set): 29 """Extracts environment variables required for the toolchain to run from 30 a textual dump output by the cmd.exe 'set' command.""" 31 envvars_to_save = ( 32 'cipd_cache_dir', # needed by vpython 33 'homedrive', # needed by vpython 34 'homepath', # needed by vpython 35 'include', 36 'lib', 37 'libpath', 38 'luci_context', # needed by vpython 39 'path', 40 'pathext', 41 'systemroot', 42 'temp', 43 'tmp', 44 'userprofile', # needed by vpython 45 'vpython_virtualenv_root' # needed by vpython 46 ) 47 env = {} 48 # This occasionally happens and leads to misleading SYSTEMROOT error messages 49 # if not caught here. 50 if output_of_set.count('=') == 0: 51 raise Exception('Invalid output_of_set. Value is:\n%s' % output_of_set) 52 for line in output_of_set.splitlines(): 53 for envvar in envvars_to_save: 54 if re.match(envvar + '=', line.lower()): 55 var, setting = line.split('=', 1) 56 if envvar == 'path': 57 # Our own rules and actions in Chromium rely on python being in the 58 # path. Add the path to this python here so that if it's not in the 59 # path when ninja is run later, python will still be found. 60 setting = os.path.dirname(sys.executable) + os.pathsep + setting 61 if envvar in ['include', 'lib']: 62 # Make sure that the include and lib paths point to directories that 63 # exist. This ensures a (relatively) clear error message if the 64 # required SDK is not installed. 65 for part in setting.split(';'): 66 if not os.path.exists(part) and len(part) != 0: 67 raise Exception( 68 'Path "%s" from environment variable "%s" does not exist. ' 69 'Make sure the necessary SDK is installed.' % (part, envvar)) 70 env[var.upper()] = setting 71 break 72 if sys.platform in ('win32', 'cygwin'): 73 for required in ('SYSTEMROOT', 'TEMP', 'TMP'): 74 if required not in env: 75 raise Exception('Environment variable "%s" ' 76 'required to be set to valid path' % required) 77 return env 78 79 80def _DetectVisualStudioPath(): 81 """Return path to the installed Visual Studio. 82 """ 83 84 # Use the code in build/vs_toolchain.py to avoid duplicating code. 85 chromium_dir = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..', '..')) 86 sys.path.append(os.path.join(chromium_dir, 'build')) 87 import vs_toolchain 88 return vs_toolchain.DetectVisualStudioPath() 89 90 91def _LoadEnvFromBat(args): 92 """Given a bat command, runs it and returns env vars set by it.""" 93 args = args[:] 94 args.extend(('&&', 'set')) 95 popen = subprocess.Popen( 96 args, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 97 variables, _ = popen.communicate() 98 if popen.returncode != 0: 99 raise Exception('"%s" failed with error %d' % (args, popen.returncode)) 100 return variables.decode(errors='ignore') 101 102 103def _LoadToolchainEnv(cpu, toolchain_root, sdk_dir, target_store): 104 """Returns a dictionary with environment variables that must be set while 105 running binaries from the toolchain (e.g. INCLUDE and PATH for cl.exe).""" 106 # Check if we are running in the SDK command line environment and use 107 # the setup script from the SDK if so. |cpu| should be either 108 # 'x86' or 'x64' or 'arm' or 'arm64'. 109 assert cpu in ('x86', 'x64', 'arm', 'arm64') 110 if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and sdk_dir: 111 # Load environment from json file. 112 env = os.path.normpath(os.path.join(sdk_dir, 'bin/SetEnv.%s.json' % cpu)) 113 env = json.load(open(env))['env'] 114 if env['VSINSTALLDIR'] == [["..", "..\\"]]: 115 # Old-style paths were relative to the win_sdk\bin directory. 116 json_relative_dir = os.path.join(sdk_dir, 'bin') 117 else: 118 # New-style paths are relative to the toolchain directory. 119 json_relative_dir = toolchain_root 120 for k in env: 121 entries = [os.path.join(*([json_relative_dir] + e)) for e in env[k]] 122 # clang-cl wants INCLUDE to be ;-separated even on non-Windows, 123 # lld-link wants LIB to be ;-separated even on non-Windows. Path gets :. 124 # The separator for INCLUDE here must match the one used in main() below. 125 sep = os.pathsep if k == 'PATH' else ';' 126 env[k] = sep.join(entries) 127 # PATH is a bit of a special case, it's in addition to the current PATH. 128 env['PATH'] = env['PATH'] + os.pathsep + os.environ['PATH'] 129 # Augment with the current env to pick up TEMP and friends. 130 for k in os.environ: 131 if k not in env: 132 env[k] = os.environ[k] 133 134 varlines = [] 135 for k in sorted(env.keys()): 136 varlines.append('%s=%s' % (str(k), str(env[k]))) 137 variables = '\n'.join(varlines) 138 139 # Check that the json file contained the same environment as the .cmd file. 140 if sys.platform in ('win32', 'cygwin'): 141 script = os.path.normpath(os.path.join(sdk_dir, 'Bin/SetEnv.cmd')) 142 arg = '/' + cpu 143 json_env = _ExtractImportantEnvironment(variables) 144 cmd_env = _ExtractImportantEnvironment(_LoadEnvFromBat([script, arg])) 145 assert _LowercaseDict(json_env) == _LowercaseDict(cmd_env) 146 else: 147 if 'GYP_MSVS_OVERRIDE_PATH' not in os.environ: 148 os.environ['GYP_MSVS_OVERRIDE_PATH'] = _DetectVisualStudioPath() 149 # We only support x64-hosted tools. 150 script_path = os.path.normpath(os.path.join( 151 os.environ['GYP_MSVS_OVERRIDE_PATH'], 152 'VC/vcvarsall.bat')) 153 if not os.path.exists(script_path): 154 # vcvarsall.bat for VS 2017 fails if run after running vcvarsall.bat from 155 # VS 2013 or VS 2015. Fix this by clearing the vsinstalldir environment 156 # variable. Since vcvarsall.bat appends to the INCLUDE, LIB, and LIBPATH 157 # environment variables we need to clear those to avoid getting double 158 # entries when vcvarsall.bat has been run before gn gen. vcvarsall.bat 159 # also adds to PATH, but there is no clean way of clearing that and it 160 # doesn't seem to cause problems. 161 if 'VSINSTALLDIR' in os.environ: 162 del os.environ['VSINSTALLDIR'] 163 if 'INCLUDE' in os.environ: 164 del os.environ['INCLUDE'] 165 if 'LIB' in os.environ: 166 del os.environ['LIB'] 167 if 'LIBPATH' in os.environ: 168 del os.environ['LIBPATH'] 169 other_path = os.path.normpath(os.path.join( 170 os.environ['GYP_MSVS_OVERRIDE_PATH'], 171 'VC/Auxiliary/Build/vcvarsall.bat')) 172 if not os.path.exists(other_path): 173 raise Exception('%s is missing - make sure VC++ tools are installed.' % 174 script_path) 175 script_path = other_path 176 cpu_arg = "amd64" 177 if (cpu != 'x64'): 178 # x64 is default target CPU thus any other CPU requires a target set 179 cpu_arg += '_' + cpu 180 args = [script_path, cpu_arg, ] 181 # Store target must come before any SDK version declaration 182 if (target_store): 183 args.append('store') 184 # Explicitly specifying the SDK version to build with to avoid accidentally 185 # building with a new and untested SDK. This should stay in sync with the 186 # packaged toolchain in build/vs_toolchain.py. 187 args.append(SDK_VERSION) 188 variables = _LoadEnvFromBat(args) 189 return _ExtractImportantEnvironment(variables) 190 191 192def _FormatAsEnvironmentBlock(envvar_dict): 193 """Format as an 'environment block' directly suitable for CreateProcess. 194 Briefly this is a list of key=value\0, terminated by an additional \0. See 195 CreateProcess documentation for more details.""" 196 block = '' 197 nul = '\0' 198 for key, value in envvar_dict.items(): 199 block += key + '=' + value + nul 200 block += nul 201 return block 202 203 204def _LowercaseDict(d): 205 """Returns a copy of `d` with both key and values lowercased. 206 207 Args: 208 d: dict to lowercase (e.g. {'A': 'BcD'}). 209 210 Returns: 211 A dict with both keys and values lowercased (e.g.: {'a': 'bcd'}). 212 """ 213 return {k.lower(): d[k].lower() for k in d} 214 215 216def FindFileInEnvList(env, env_name, separator, file_name, optional=False): 217 parts = env[env_name].split(separator) 218 for path in parts: 219 if os.path.exists(os.path.join(path, file_name)): 220 return os.path.realpath(path) 221 assert optional, "%s is not found in %s:\n%s\nCheck if it is installed." % ( 222 file_name, env_name, '\n'.join(parts)) 223 return '' 224 225 226def main(): 227 if len(sys.argv) != 7: 228 print('Usage setup_toolchain.py ' 229 '<visual studio path> <win sdk path> ' 230 '<runtime dirs> <target_os> <target_cpu> ' 231 '<environment block name|none>') 232 sys.exit(2) 233 # toolchain_root and win_sdk_path are only read if the hermetic Windows 234 # toolchain is set, that is if DEPOT_TOOLS_WIN_TOOLCHAIN is not set to 0. 235 # With the hermetic Windows toolchain, the visual studio path in argv[1] 236 # is the root of the Windows toolchain directory. 237 toolchain_root = sys.argv[1] 238 win_sdk_path = sys.argv[2] 239 240 runtime_dirs = sys.argv[3] 241 target_os = sys.argv[4] 242 target_cpu = sys.argv[5] 243 environment_block_name = sys.argv[6] 244 if (environment_block_name == 'none'): 245 environment_block_name = '' 246 247 if (target_os == 'winuwp'): 248 target_store = True 249 else: 250 target_store = False 251 252 cpus = ('x86', 'x64', 'arm', 'arm64') 253 assert target_cpu in cpus 254 vc_bin_dir = '' 255 include = '' 256 lib = '' 257 258 def relflag(s): # Make s relative to builddir when cwd and sdk on same drive. 259 try: 260 return os.path.relpath(s).replace('\\', '/') 261 except ValueError: 262 return s 263 264 def q(s): # Quote s if it contains spaces or other weird characters. 265 return s if re.match(r'^[a-zA-Z0-9._/\\:-]*$', s) else '"' + s + '"' 266 267 for cpu in cpus: 268 if cpu == target_cpu: 269 # Extract environment variables for subprocesses. 270 env = _LoadToolchainEnv(cpu, toolchain_root, win_sdk_path, target_store) 271 env['PATH'] = runtime_dirs + os.pathsep + env['PATH'] 272 273 vc_bin_dir = FindFileInEnvList(env, 'PATH', os.pathsep, 'cl.exe') 274 275 # The separator for INCLUDE here must match the one used in 276 # _LoadToolchainEnv() above. 277 include = [p.replace('"', r'\"') for p in env['INCLUDE'].split(';') if p] 278 include = list(map(relflag, include)) 279 280 lib = [p.replace('"', r'\"') for p in env['LIB'].split(';') if p] 281 lib = list(map(relflag, lib)) 282 283 include_I = ['/I' + i for i in include] 284 include_imsvc = ['-imsvc' + i for i in include] 285 libpath_flags = ['-libpath:' + i for i in lib] 286 287 if (environment_block_name != ''): 288 env_block = _FormatAsEnvironmentBlock(env) 289 with open(environment_block_name, 'w', encoding='utf8') as f: 290 f.write(env_block) 291 292 def ListToArgString(x): 293 return gn_helpers.ToGNString(' '.join(q(i) for i in x)) 294 295 def ListToArgList(x): 296 return f'[{", ".join(gn_helpers.ToGNString(i) for i in x)}]' 297 298 print('vc_bin_dir = ' + gn_helpers.ToGNString(vc_bin_dir)) 299 assert include_I 300 print(f'include_flags_I = {ListToArgString(include_I)}') 301 print(f'include_flags_I_list = {ListToArgList(include_I)}') 302 assert include_imsvc 303 if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and win_sdk_path: 304 flags = ['/winsysroot' + relflag(toolchain_root)] 305 print(f'include_flags_imsvc = {ListToArgString(flags)}') 306 print(f'include_flags_imsvc_list = {ListToArgList(flags)}') 307 else: 308 print(f'include_flags_imsvc = {ListToArgString(include_imsvc)}') 309 print(f'include_flags_imsvc_list = {ListToArgList(include_imsvc)}') 310 print('paths = ' + gn_helpers.ToGNString(env['PATH'])) 311 assert libpath_flags 312 print(f'libpath_flags = {ListToArgString(libpath_flags)}') 313 print(f'libpath_flags_list = {ListToArgList(libpath_flags)}') 314 if bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', 1))) and win_sdk_path: 315 flags = ['/winsysroot:' + relflag(toolchain_root)] 316 print(f'libpath_lldlink_flags = {ListToArgString(flags)}') 317 print(f'libpath_lldlink_flags_list = {ListToArgList(flags)}') 318 else: 319 print(f'libpath_lldlink_flags = {ListToArgString(libpath_flags)}') 320 print(f'libpath_lldlink_flags_list = {ListToArgList(libpath_flags)}') 321 322 323if __name__ == '__main__': 324 main() 325