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