• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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