1#!/usr/bin/env python 2# Copyright 2014 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import glob 7import json 8import os 9import pipes 10import platform 11import re 12import shutil 13import stat 14import subprocess 15import sys 16from gn_helpers import ToGNString 17 18 19script_dir = os.path.dirname(os.path.realpath(__file__)) 20chrome_src = os.path.abspath(os.path.join(script_dir, os.pardir)) 21SRC_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 22sys.path.insert(0, os.path.join(chrome_src, 'tools', 'gyp', 'pylib')) 23json_data_file = os.path.join(script_dir, 'win_toolchain.json') 24 25 26# Use MSVS2017 as the default toolchain. 27CURRENT_DEFAULT_TOOLCHAIN_VERSION = '2017' 28 29 30def SetEnvironmentAndGetRuntimeDllDirs(): 31 """Sets up os.environ to use the depot_tools VS toolchain with gyp, and 32 returns the location of the VS runtime DLLs so they can be copied into 33 the output directory after gyp generation. 34 35 Return value is [x64path, x86path] or None 36 """ 37 vs_runtime_dll_dirs = None 38 depot_tools_win_toolchain = \ 39 bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1'))) 40 # When running on a non-Windows host, only do this if the SDK has explicitly 41 # been downloaded before (in which case json_data_file will exist). 42 if ((sys.platform in ('win32', 'cygwin') or os.path.exists(json_data_file)) 43 and depot_tools_win_toolchain): 44 if ShouldUpdateToolchain(): 45 update_result = Update() 46 if update_result != 0: 47 raise Exception('Failed to update, error code %d.' % update_result) 48 with open(json_data_file, 'r') as tempf: 49 toolchain_data = json.load(tempf) 50 51 toolchain = toolchain_data['path'] 52 version = toolchain_data['version'] 53 win_sdk = toolchain_data.get('win_sdk') 54 if not win_sdk: 55 win_sdk = toolchain_data['win8sdk'] 56 wdk = toolchain_data['wdk'] 57 # TODO(scottmg): The order unfortunately matters in these. They should be 58 # split into separate keys for x86 and x64. (See CopyDlls call below). 59 # http://crbug.com/345992 60 vs_runtime_dll_dirs = toolchain_data['runtime_dirs'] 61 62 os.environ['GYP_MSVS_OVERRIDE_PATH'] = toolchain 63 os.environ['GYP_MSVS_VERSION'] = version 64 65 os.environ['WINDOWSSDKDIR'] = win_sdk 66 os.environ['WDK_DIR'] = wdk 67 # Include the VS runtime in the PATH in case it's not machine-installed. 68 runtime_path = os.path.pathsep.join(vs_runtime_dll_dirs) 69 os.environ['PATH'] = runtime_path + os.path.pathsep + os.environ['PATH'] 70 elif sys.platform == 'win32' and not depot_tools_win_toolchain: 71 if not 'GYP_MSVS_OVERRIDE_PATH' in os.environ: 72 os.environ['GYP_MSVS_OVERRIDE_PATH'] = DetectVisualStudioPath() 73 if not 'GYP_MSVS_VERSION' in os.environ: 74 os.environ['GYP_MSVS_VERSION'] = GetVisualStudioVersion() 75 76 # When using an installed toolchain these files aren't needed in the output 77 # directory in order to run binaries locally, but they are needed in order 78 # to create isolates or the mini_installer. Copying them to the output 79 # directory ensures that they are available when needed. 80 bitness = platform.architecture()[0] 81 # When running 64-bit python the x64 DLLs will be in System32 82 x64_path = 'System32' if bitness == '64bit' else 'Sysnative' 83 x64_path = os.path.join(os.path.expandvars('%windir%'), x64_path) 84 vs_runtime_dll_dirs = [x64_path, os.path.expandvars('%windir%/SysWOW64')] 85 86 return vs_runtime_dll_dirs 87 88 89def _RegistryGetValueUsingWinReg(key, value): 90 """Use the _winreg module to obtain the value of a registry key. 91 92 Args: 93 key: The registry key. 94 value: The particular registry value to read. 95 Return: 96 contents of the registry key's value, or None on failure. Throws 97 ImportError if _winreg is unavailable. 98 """ 99 import _winreg 100 try: 101 root, subkey = key.split('\\', 1) 102 assert root == 'HKLM' # Only need HKLM for now. 103 with _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, subkey) as hkey: 104 return _winreg.QueryValueEx(hkey, value)[0] 105 except WindowsError: 106 return None 107 108 109def _RegistryGetValue(key, value): 110 try: 111 return _RegistryGetValueUsingWinReg(key, value) 112 except ImportError: 113 raise Exception('The python library _winreg not found.') 114 115 116def GetVisualStudioVersion(): 117 """Return GYP_MSVS_VERSION of Visual Studio. 118 """ 119 return os.environ.get('GYP_MSVS_VERSION', CURRENT_DEFAULT_TOOLCHAIN_VERSION) 120 121 122def DetectVisualStudioPath(): 123 """Return path to the GYP_MSVS_VERSION of Visual Studio. 124 """ 125 126 # Note that this code is used from 127 # build/toolchain/win/setup_toolchain.py as well. 128 version_as_year = GetVisualStudioVersion() 129 year_to_version = { 130 '2017': '15.0', 131 } 132 if version_as_year not in year_to_version: 133 raise Exception(('Visual Studio version %s (from GYP_MSVS_VERSION)' 134 ' not supported. Supported versions are: %s') % ( 135 version_as_year, ', '.join(year_to_version.keys()))) 136 version = year_to_version[version_as_year] 137 if version_as_year == '2017': 138 # The VC++ 2017 install location needs to be located using COM instead of 139 # the registry. For details see: 140 # https://blogs.msdn.microsoft.com/heaths/2016/09/15/changes-to-visual-studio-15-setup/ 141 # For now we use a hardcoded default with an environment variable override. 142 for path in ( 143 os.environ.get('vs2017_install'), 144 os.path.expandvars('%ProgramFiles(x86)%' 145 '/Microsoft Visual Studio/2017/Enterprise'), 146 os.path.expandvars('%ProgramFiles(x86)%' 147 '/Microsoft Visual Studio/2017/Professional'), 148 os.path.expandvars('%ProgramFiles(x86)%' 149 '/Microsoft Visual Studio/2017/Community')): 150 if path and os.path.exists(path): 151 return path 152 153 raise Exception(('Visual Studio Version %s (from GYP_MSVS_VERSION)' 154 ' not found.') % (version_as_year)) 155 156 157def _CopyRuntimeImpl(target, source, verbose=True): 158 """Copy |source| to |target| if it doesn't already exist or if it needs to be 159 updated (comparing last modified time as an approximate float match as for 160 some reason the values tend to differ by ~1e-07 despite being copies of the 161 same file... https://crbug.com/603603). 162 """ 163 if (os.path.isdir(os.path.dirname(target)) and 164 (not os.path.isfile(target) or 165 abs(os.stat(target).st_mtime - os.stat(source).st_mtime) >= 0.01)): 166 if verbose: 167 print 'Copying %s to %s...' % (source, target) 168 if os.path.exists(target): 169 # Make the file writable so that we can delete it now, and keep it 170 # readable. 171 os.chmod(target, stat.S_IWRITE | stat.S_IREAD) 172 os.unlink(target) 173 shutil.copy2(source, target) 174 # Make the file writable so that we can overwrite or delete it later, 175 # keep it readable. 176 os.chmod(target, stat.S_IWRITE | stat.S_IREAD) 177 178 179def _CopyUCRTRuntime(target_dir, source_dir, target_cpu, dll_pattern, suffix): 180 """Copy both the msvcp and vccorlib runtime DLLs, only if the target doesn't 181 exist, but the target directory does exist.""" 182 for file_part in ('msvcp', 'vccorlib', 'vcruntime'): 183 dll = dll_pattern % file_part 184 target = os.path.join(target_dir, dll) 185 source = os.path.join(source_dir, dll) 186 _CopyRuntimeImpl(target, source) 187 # Copy the UCRT files from the Windows SDK. This location includes the 188 # api-ms-win-crt-*.dll files that are not found in the Windows directory. 189 # These files are needed for component builds. If WINDOWSSDKDIR is not set 190 # use the default SDK path. This will be the case when 191 # DEPOT_TOOLS_WIN_TOOLCHAIN=0 and vcvarsall.bat has not been run. 192 win_sdk_dir = os.path.normpath( 193 os.environ.get('WINDOWSSDKDIR', 194 os.path.expandvars('%ProgramFiles(x86)%' 195 '\\Windows Kits\\10'))) 196 ucrt_dll_dirs = os.path.join(win_sdk_dir, 'Redist', 'ucrt', 'DLLs', 197 target_cpu) 198 ucrt_files = glob.glob(os.path.join(ucrt_dll_dirs, 'api-ms-win-*.dll')) 199 assert len(ucrt_files) > 0 200 for ucrt_src_file in ucrt_files: 201 file_part = os.path.basename(ucrt_src_file) 202 ucrt_dst_file = os.path.join(target_dir, file_part) 203 _CopyRuntimeImpl(ucrt_dst_file, ucrt_src_file, False) 204 _CopyRuntimeImpl(os.path.join(target_dir, 'ucrtbase' + suffix), 205 os.path.join(source_dir, 'ucrtbase' + suffix)) 206 207 208def FindVCToolsRoot(): 209 """In VS2017 the PGO runtime dependencies are located in 210 {toolchain_root}/VC/Tools/MSVC/{x.y.z}/bin/Host{target_cpu}/{target_cpu}/, the 211 {version_number} part is likely to change in case of a minor update of the 212 toolchain so we don't hardcode this value here (except for the major number). 213 214 This returns the '{toolchain_root}/VC/Tools/MSVC/{x.y.z}/bin/' path. 215 216 This function should only be called when using VS2017. 217 """ 218 assert GetVisualStudioVersion() == '2017' 219 SetEnvironmentAndGetRuntimeDllDirs() 220 assert ('GYP_MSVS_OVERRIDE_PATH' in os.environ) 221 vc_tools_msvc_root = os.path.join(os.environ['GYP_MSVS_OVERRIDE_PATH'], 222 'VC', 'Tools', 'MSVC') 223 for directory in os.listdir(vc_tools_msvc_root): 224 if not os.path.isdir(os.path.join(vc_tools_msvc_root, directory)): 225 continue 226 if re.match('14\.\d+\.\d+', directory): 227 return os.path.join(vc_tools_msvc_root, directory, 'bin') 228 raise Exception('Unable to find the VC tools directory.') 229 230 231def _CopyPGORuntime(target_dir, target_cpu): 232 """Copy the runtime dependencies required during a PGO build. 233 """ 234 env_version = GetVisualStudioVersion() 235 # These dependencies will be in a different location depending on the version 236 # of the toolchain. 237 if env_version == '2017': 238 pgo_runtime_root = FindVCToolsRoot() 239 assert pgo_runtime_root 240 # There's no version of pgosweep.exe in HostX64/x86, so we use the copy 241 # from HostX86/x86. 242 pgo_x86_runtime_dir = os.path.join(pgo_runtime_root, 'HostX86', 'x86') 243 pgo_x64_runtime_dir = os.path.join(pgo_runtime_root, 'HostX64', 'x64') 244 else: 245 raise Exception('Unexpected toolchain version: %s.' % env_version) 246 247 # We need to copy 2 runtime dependencies used during the profiling step: 248 # - pgort140.dll: runtime library required to run the instrumented image. 249 # - pgosweep.exe: executable used to collect the profiling data 250 pgo_runtimes = ['pgort140.dll', 'pgosweep.exe'] 251 for runtime in pgo_runtimes: 252 if target_cpu == 'x86': 253 source = os.path.join(pgo_x86_runtime_dir, runtime) 254 elif target_cpu == 'x64': 255 source = os.path.join(pgo_x64_runtime_dir, runtime) 256 else: 257 raise NotImplementedError("Unexpected target_cpu value: " + target_cpu) 258 if not os.path.exists(source): 259 raise Exception('Unable to find %s.' % source) 260 _CopyRuntimeImpl(os.path.join(target_dir, runtime), source) 261 262 263def _CopyRuntime(target_dir, source_dir, target_cpu, debug): 264 """Copy the VS runtime DLLs, only if the target doesn't exist, but the target 265 directory does exist. Handles VS 2015 and VS 2017.""" 266 suffix = "d.dll" if debug else ".dll" 267 # VS 2017 uses the same CRT DLLs as VS 2015. 268 _CopyUCRTRuntime(target_dir, source_dir, target_cpu, '%s140' + suffix, 269 suffix) 270 271 272def CopyDlls(target_dir, configuration, target_cpu): 273 """Copy the VS runtime DLLs into the requested directory as needed. 274 275 configuration is one of 'Debug' or 'Release'. 276 target_cpu is one of 'x86' or 'x64'. 277 278 The debug configuration gets both the debug and release DLLs; the 279 release config only the latter. 280 """ 281 vs_runtime_dll_dirs = SetEnvironmentAndGetRuntimeDllDirs() 282 if not vs_runtime_dll_dirs: 283 return 284 285 x64_runtime, x86_runtime = vs_runtime_dll_dirs 286 runtime_dir = x64_runtime if target_cpu == 'x64' else x86_runtime 287 _CopyRuntime(target_dir, runtime_dir, target_cpu, debug=False) 288 if configuration == 'Debug': 289 _CopyRuntime(target_dir, runtime_dir, target_cpu, debug=True) 290 else: 291 _CopyPGORuntime(target_dir, target_cpu) 292 293 _CopyDebugger(target_dir, target_cpu) 294 295 296def _CopyDebugger(target_dir, target_cpu): 297 """Copy dbghelp.dll and dbgcore.dll into the requested directory as needed. 298 299 target_cpu is one of 'x86' or 'x64'. 300 301 dbghelp.dll is used when Chrome needs to symbolize stacks. Copying this file 302 from the SDK directory avoids using the system copy of dbghelp.dll which then 303 ensures compatibility with recent debug information formats, such as VS 304 2017 /debug:fastlink PDBs. 305 306 dbgcore.dll is needed when using some functions from dbghelp.dll (like 307 MinidumpWriteDump). 308 """ 309 win_sdk_dir = SetEnvironmentAndGetSDKDir() 310 if not win_sdk_dir: 311 return 312 313 # List of debug files that should be copied, the first element of the tuple is 314 # the name of the file and the second indicates if it's optional. 315 debug_files = [('dbghelp.dll', False), ('dbgcore.dll', True)] 316 for debug_file, is_optional in debug_files: 317 full_path = os.path.join(win_sdk_dir, 'Debuggers', target_cpu, debug_file) 318 if not os.path.exists(full_path): 319 if is_optional: 320 continue 321 else: 322 # TODO(crbug.com/773476): remove version requirement. 323 raise Exception('%s not found in "%s"\r\nYou must install the ' 324 '"Debugging Tools for Windows" feature from the Windows' 325 ' 10 SDK. You must use v10.0.17134.0. of the SDK' 326 % (debug_file, full_path)) 327 target_path = os.path.join(target_dir, debug_file) 328 _CopyRuntimeImpl(target_path, full_path) 329 330 331def _GetDesiredVsToolchainHashes(): 332 """Load a list of SHA1s corresponding to the toolchains that we want installed 333 to build with.""" 334 env_version = GetVisualStudioVersion() 335 if env_version == '2017': 336 # VS 2017 Update 7.1 (15.7.1) with 10.0.17134.12 SDK, rebuilt with 337 # dbghelp.dll fix. 338 toolchain_hash = '3bc0ec615cf20ee342f3bc29bc991b5ad66d8d2c' 339 # Third parties that do not have access to the canonical toolchain can map 340 # canonical toolchain version to their own toolchain versions. 341 toolchain_hash_mapping_key = 'GYP_MSVS_HASH_%s' % toolchain_hash 342 return [os.environ.get(toolchain_hash_mapping_key, toolchain_hash)] 343 raise Exception('Unsupported VS version %s' % env_version) 344 345 346def ShouldUpdateToolchain(): 347 """Check if the toolchain should be upgraded.""" 348 if not os.path.exists(json_data_file): 349 return True 350 with open(json_data_file, 'r') as tempf: 351 toolchain_data = json.load(tempf) 352 version = toolchain_data['version'] 353 env_version = GetVisualStudioVersion() 354 # If there's a mismatch between the version set in the environment and the one 355 # in the json file then the toolchain should be updated. 356 return version != env_version 357 358 359def Update(force=False): 360 """Requests an update of the toolchain to the specific hashes we have at 361 this revision. The update outputs a .json of the various configuration 362 information required to pass to gyp which we use in |GetToolchainDir()|. 363 """ 364 if force != False and force != '--force': 365 print >>sys.stderr, 'Unknown parameter "%s"' % force 366 return 1 367 if force == '--force' or os.path.exists(json_data_file): 368 force = True 369 370 depot_tools_win_toolchain = \ 371 bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1'))) 372 if ((sys.platform in ('win32', 'cygwin') or force) and 373 depot_tools_win_toolchain): 374 import find_depot_tools 375 depot_tools_path = find_depot_tools.add_depot_tools_to_path() 376 377 # On Linux, the file system is usually case-sensitive while the Windows 378 # SDK only works on case-insensitive file systems. If it doesn't already 379 # exist, set up a ciopfs fuse mount to put the SDK in a case-insensitive 380 # part of the file system. 381 toolchain_dir = os.path.join(depot_tools_path, 'win_toolchain', 'vs_files') 382 # For testing this block, unmount existing mounts with 383 # fusermount -u third_party/depot_tools/win_toolchain/vs_files 384 if sys.platform.startswith('linux') and not os.path.ismount(toolchain_dir): 385 import distutils.spawn 386 ciopfs = distutils.spawn.find_executable('ciopfs') 387 if not ciopfs: 388 # ciopfs not found in PATH; try the one downloaded from the DEPS hook. 389 ciopfs = os.path.join(script_dir, 'ciopfs') 390 if not os.path.isdir(toolchain_dir): 391 os.mkdir(toolchain_dir) 392 if not os.path.isdir(toolchain_dir + '.ciopfs'): 393 os.mkdir(toolchain_dir + '.ciopfs') 394 # Without use_ino, clang's #pragma once and Wnonportable-include-path 395 # both don't work right, see https://llvm.org/PR34931 396 # use_ino doesn't slow down builds, so it seems there's no drawback to 397 # just using it always. 398 subprocess.check_call([ 399 ciopfs, '-o', 'use_ino', toolchain_dir + '.ciopfs', toolchain_dir]) 400 401 # Necessary so that get_toolchain_if_necessary.py will put the VS toolkit 402 # in the correct directory. 403 os.environ['GYP_MSVS_VERSION'] = GetVisualStudioVersion() 404 get_toolchain_args = [ 405 sys.executable, 406 os.path.join(depot_tools_path, 407 'win_toolchain', 408 'get_toolchain_if_necessary.py'), 409 '--output-json', json_data_file, 410 ] + _GetDesiredVsToolchainHashes() 411 if force: 412 get_toolchain_args.append('--force') 413 subprocess.check_call(get_toolchain_args) 414 415 return 0 416 417 418def NormalizePath(path): 419 while path.endswith("\\"): 420 path = path[:-1] 421 return path 422 423 424def SetEnvironmentAndGetSDKDir(): 425 """Gets location information about the current sdk (must have been 426 previously updated by 'update'). This is used for the GN build.""" 427 SetEnvironmentAndGetRuntimeDllDirs() 428 429 # If WINDOWSSDKDIR is not set, search the default SDK path and set it. 430 if not 'WINDOWSSDKDIR' in os.environ: 431 default_sdk_path = os.path.expandvars('%ProgramFiles(x86)%' 432 '\\Windows Kits\\10') 433 if os.path.isdir(default_sdk_path): 434 os.environ['WINDOWSSDKDIR'] = default_sdk_path 435 436 return NormalizePath(os.environ['WINDOWSSDKDIR']) 437 438 439def GetToolchainDir(): 440 """Gets location information about the current toolchain (must have been 441 previously updated by 'update'). This is used for the GN build.""" 442 runtime_dll_dirs = SetEnvironmentAndGetRuntimeDllDirs() 443 win_sdk_dir = SetEnvironmentAndGetSDKDir() 444 445 print '''vs_path = %s 446sdk_path = %s 447vs_version = %s 448wdk_dir = %s 449runtime_dirs = %s 450''' % ( 451 ToGNString(NormalizePath(os.environ['GYP_MSVS_OVERRIDE_PATH'])), 452 ToGNString(win_sdk_dir), 453 ToGNString(GetVisualStudioVersion()), 454 ToGNString(NormalizePath(os.environ.get('WDK_DIR', ''))), 455 ToGNString(os.path.pathsep.join(runtime_dll_dirs or ['None']))) 456 457 458def main(): 459 commands = { 460 'update': Update, 461 'get_toolchain_dir': GetToolchainDir, 462 'copy_dlls': CopyDlls, 463 } 464 if len(sys.argv) < 2 or sys.argv[1] not in commands: 465 print >>sys.stderr, 'Expected one of: %s' % ', '.join(commands) 466 return 1 467 return commands[sys.argv[1]](*sys.argv[2:]) 468 469 470if __name__ == '__main__': 471 sys.exit(main()) 472