1#!/usr/bin/env python
2# Copyright 2015 the V8 project authors. All rights reserved.
3# Copyright 2014 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 glob
8import json
9import os
10import pipes
11import shutil
12import subprocess
13import sys
14
15
16script_dir = os.path.dirname(os.path.realpath(__file__))
17chrome_src = os.path.abspath(os.path.join(script_dir, os.pardir))
18SRC_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
19sys.path.insert(1, os.path.join(chrome_src, 'tools'))
20sys.path.insert(0, os.path.join(chrome_src, 'tools', 'gyp', 'pylib'))
21json_data_file = os.path.join(script_dir, 'win_toolchain.json')
22
23
24import gyp
25
26
27# Use MSVS2013 as the default toolchain.
28CURRENT_DEFAULT_TOOLCHAIN_VERSION = '2013'
29
30
31def SetEnvironmentAndGetRuntimeDllDirs():
32  """Sets up os.environ to use the depot_tools VS toolchain with gyp, and
33  returns the location of the VS runtime DLLs so they can be copied into
34  the output directory after gyp generation.
35  """
36  vs_runtime_dll_dirs = None
37  depot_tools_win_toolchain = \
38      bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1')))
39  # When running on a non-Windows host, only do this if the SDK has explicitly
40  # been downloaded before (in which case json_data_file will exist).
41  if ((sys.platform in ('win32', 'cygwin') or os.path.exists(json_data_file))
42      and depot_tools_win_toolchain):
43    if ShouldUpdateToolchain():
44      Update()
45    with open(json_data_file, 'r') as tempf:
46      toolchain_data = json.load(tempf)
47
48    toolchain = toolchain_data['path']
49    version = toolchain_data['version']
50    win_sdk = toolchain_data.get('win_sdk')
51    if not win_sdk:
52      win_sdk = toolchain_data['win8sdk']
53    wdk = toolchain_data['wdk']
54    # TODO(scottmg): The order unfortunately matters in these. They should be
55    # split into separate keys for x86 and x64. (See CopyVsRuntimeDlls call
56    # below). http://crbug.com/345992
57    vs_runtime_dll_dirs = toolchain_data['runtime_dirs']
58
59    os.environ['GYP_MSVS_OVERRIDE_PATH'] = toolchain
60    os.environ['GYP_MSVS_VERSION'] = version
61    # We need to make sure windows_sdk_path is set to the automated
62    # toolchain values in GYP_DEFINES, but don't want to override any
63    # otheroptions.express
64    # values there.
65    gyp_defines_dict = gyp.NameValueListToDict(gyp.ShlexEnv('GYP_DEFINES'))
66    gyp_defines_dict['windows_sdk_path'] = win_sdk
67    os.environ['GYP_DEFINES'] = ' '.join('%s=%s' % (k, pipes.quote(str(v)))
68        for k, v in gyp_defines_dict.iteritems())
69    os.environ['WINDOWSSDKDIR'] = win_sdk
70    os.environ['WDK_DIR'] = wdk
71    # Include the VS runtime in the PATH in case it's not machine-installed.
72    runtime_path = os.path.pathsep.join(vs_runtime_dll_dirs)
73    os.environ['PATH'] = runtime_path + os.path.pathsep + os.environ['PATH']
74  elif sys.platform == 'win32' and not depot_tools_win_toolchain:
75    if not 'GYP_MSVS_OVERRIDE_PATH' in os.environ:
76      os.environ['GYP_MSVS_OVERRIDE_PATH'] = DetectVisualStudioPath()
77    if not 'GYP_MSVS_VERSION' in os.environ:
78      os.environ['GYP_MSVS_VERSION'] = GetVisualStudioVersion()
79
80  return vs_runtime_dll_dirs
81
82
83def _RegistryGetValueUsingWinReg(key, value):
84  """Use the _winreg module to obtain the value of a registry key.
85
86  Args:
87    key: The registry key.
88    value: The particular registry value to read.
89  Return:
90    contents of the registry key's value, or None on failure.  Throws
91    ImportError if _winreg is unavailable.
92  """
93  import _winreg
94  try:
95    root, subkey = key.split('\\', 1)
96    assert root == 'HKLM'  # Only need HKLM for now.
97    with _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, subkey) as hkey:
98      return _winreg.QueryValueEx(hkey, value)[0]
99  except WindowsError:
100    return None
101
102
103def _RegistryGetValue(key, value):
104  try:
105    return _RegistryGetValueUsingWinReg(key, value)
106  except ImportError:
107    raise Exception('The python library _winreg not found.')
108
109
110def GetVisualStudioVersion():
111  """Return GYP_MSVS_VERSION of Visual Studio.
112  """
113  return os.environ.get('GYP_MSVS_VERSION', CURRENT_DEFAULT_TOOLCHAIN_VERSION)
114
115
116def DetectVisualStudioPath():
117  """Return path to the GYP_MSVS_VERSION of Visual Studio.
118  """
119
120  # Note that this code is used from
121  # build/toolchain/win/setup_toolchain.py as well.
122  version_as_year = GetVisualStudioVersion()
123  year_to_version = {
124      '2013': '12.0',
125      '2015': '14.0',
126  }
127  if version_as_year not in year_to_version:
128    raise Exception(('Visual Studio version %s (from GYP_MSVS_VERSION)'
129                     ' not supported. Supported versions are: %s') % (
130                       version_as_year, ', '.join(year_to_version.keys())))
131  version = year_to_version[version_as_year]
132  keys = [r'HKLM\Software\Microsoft\VisualStudio\%s' % version,
133          r'HKLM\Software\Wow6432Node\Microsoft\VisualStudio\%s' % version]
134  for key in keys:
135    path = _RegistryGetValue(key, 'InstallDir')
136    if not path:
137      continue
138    path = os.path.normpath(os.path.join(path, '..', '..'))
139    return path
140
141  raise Exception(('Visual Studio Version %s (from GYP_MSVS_VERSION)'
142                   ' not found.') % (version_as_year))
143
144
145def _VersionNumber():
146  """Gets the standard version number ('120', '140', etc.) based on
147  GYP_MSVS_VERSION."""
148  vs_version = GetVisualStudioVersion()
149  if vs_version == '2013':
150    return '120'
151  elif vs_version == '2015':
152    return '140'
153  else:
154    raise ValueError('Unexpected GYP_MSVS_VERSION')
155
156
157def _CopyRuntimeImpl(target, source, verbose=True):
158  """Copy |source| to |target| if it doesn't already exist or if it
159  needs to be updated.
160  """
161  if (os.path.isdir(os.path.dirname(target)) and
162      (not os.path.isfile(target) or
163      os.stat(target).st_mtime != os.stat(source).st_mtime)):
164    if verbose:
165      print 'Copying %s to %s...' % (source, target)
166    if os.path.exists(target):
167      os.unlink(target)
168    shutil.copy2(source, target)
169
170
171def _CopyRuntime2013(target_dir, source_dir, dll_pattern):
172  """Copy both the msvcr and msvcp runtime DLLs, only if the target doesn't
173  exist, but the target directory does exist."""
174  for file_part in ('p', 'r'):
175    dll = dll_pattern % file_part
176    target = os.path.join(target_dir, dll)
177    source = os.path.join(source_dir, dll)
178    _CopyRuntimeImpl(target, source)
179
180
181def _CopyRuntime2015(target_dir, source_dir, dll_pattern, suffix):
182  """Copy both the msvcp and vccorlib runtime DLLs, only if the target doesn't
183  exist, but the target directory does exist."""
184  for file_part in ('msvcp', 'vccorlib', 'vcruntime'):
185    dll = dll_pattern % file_part
186    target = os.path.join(target_dir, dll)
187    source = os.path.join(source_dir, dll)
188    _CopyRuntimeImpl(target, source)
189  ucrt_src_dir = os.path.join(source_dir, 'api-ms-win-*.dll')
190  print 'Copying %s to %s...' % (ucrt_src_dir, target_dir)
191  for ucrt_src_file in glob.glob(ucrt_src_dir):
192    file_part = os.path.basename(ucrt_src_file)
193    ucrt_dst_file = os.path.join(target_dir, file_part)
194    _CopyRuntimeImpl(ucrt_dst_file, ucrt_src_file, False)
195  _CopyRuntimeImpl(os.path.join(target_dir, 'ucrtbase' + suffix),
196                    os.path.join(source_dir, 'ucrtbase' + suffix))
197
198
199def _CopyRuntime(target_dir, source_dir, target_cpu, debug):
200  """Copy the VS runtime DLLs, only if the target doesn't exist, but the target
201  directory does exist. Handles VS 2013 and VS 2015."""
202  suffix = "d.dll" if debug else ".dll"
203  if GetVisualStudioVersion() == '2015':
204    _CopyRuntime2015(target_dir, source_dir, '%s140' + suffix, suffix)
205  else:
206    _CopyRuntime2013(target_dir, source_dir, 'msvc%s120' + suffix)
207
208  # Copy the PGO runtime library to the release directories.
209  if not debug and os.environ.get('GYP_MSVS_OVERRIDE_PATH'):
210    pgo_x86_runtime_dir = os.path.join(os.environ.get('GYP_MSVS_OVERRIDE_PATH'),
211                                        'VC', 'bin')
212    pgo_x64_runtime_dir = os.path.join(pgo_x86_runtime_dir, 'amd64')
213    pgo_runtime_dll = 'pgort' + _VersionNumber() + '.dll'
214    if target_cpu == "x86":
215      source_x86 = os.path.join(pgo_x86_runtime_dir, pgo_runtime_dll)
216      if os.path.exists(source_x86):
217        _CopyRuntimeImpl(os.path.join(target_dir, pgo_runtime_dll), source_x86)
218    elif target_cpu == "x64":
219      source_x64 = os.path.join(pgo_x64_runtime_dir, pgo_runtime_dll)
220      if os.path.exists(source_x64):
221        _CopyRuntimeImpl(os.path.join(target_dir, pgo_runtime_dll),
222                          source_x64)
223    else:
224      raise NotImplementedError("Unexpected target_cpu value:" + target_cpu)
225
226
227def CopyVsRuntimeDlls(output_dir, runtime_dirs):
228  """Copies the VS runtime DLLs from the given |runtime_dirs| to the output
229  directory so that even if not system-installed, built binaries are likely to
230  be able to run.
231
232  This needs to be run after gyp has been run so that the expected target
233  output directories are already created.
234
235  This is used for the GYP build and gclient runhooks.
236  """
237  x86, x64 = runtime_dirs
238  out_debug = os.path.join(output_dir, 'Debug')
239  out_release = os.path.join(output_dir, 'Release')
240  out_debug_x64 = os.path.join(output_dir, 'Debug_x64')
241  out_release_x64 = os.path.join(output_dir, 'Release_x64')
242
243  _CopyRuntime(out_debug,          x86, "x86", debug=True)
244  _CopyRuntime(out_release,        x86, "x86", debug=False)
245  _CopyRuntime(out_debug_x64,      x64, "x64", debug=True)
246  _CopyRuntime(out_release_x64,    x64, "x64", debug=False)
247
248
249def CopyDlls(target_dir, configuration, target_cpu):
250  """Copy the VS runtime DLLs into the requested directory as needed.
251
252  configuration is one of 'Debug' or 'Release'.
253  target_cpu is one of 'x86' or 'x64'.
254
255  The debug configuration gets both the debug and release DLLs; the
256  release config only the latter.
257
258  This is used for the GN build.
259  """
260  vs_runtime_dll_dirs = SetEnvironmentAndGetRuntimeDllDirs()
261  if not vs_runtime_dll_dirs:
262    return
263
264  x64_runtime, x86_runtime = vs_runtime_dll_dirs
265  runtime_dir = x64_runtime if target_cpu == 'x64' else x86_runtime
266  _CopyRuntime(target_dir, runtime_dir, target_cpu, debug=False)
267  if configuration == 'Debug':
268    _CopyRuntime(target_dir, runtime_dir, target_cpu, debug=True)
269
270
271def _GetDesiredVsToolchainHashes():
272  """Load a list of SHA1s corresponding to the toolchains that we want installed
273  to build with."""
274  if GetVisualStudioVersion() == '2015':
275    # Update 2.
276    return ['95ddda401ec5678f15eeed01d2bee08fcbc5ee97']
277  else:
278    return ['03a4e939cd325d6bc5216af41b92d02dda1366a6']
279
280
281def ShouldUpdateToolchain():
282  """Check if the toolchain should be upgraded."""
283  if not os.path.exists(json_data_file):
284    return True
285  with open(json_data_file, 'r') as tempf:
286    toolchain_data = json.load(tempf)
287  version = toolchain_data['version']
288  env_version = GetVisualStudioVersion()
289  # If there's a mismatch between the version set in the environment and the one
290  # in the json file then the toolchain should be updated.
291  return version != env_version
292
293
294def Update(force=False):
295  """Requests an update of the toolchain to the specific hashes we have at
296  this revision. The update outputs a .json of the various configuration
297  information required to pass to gyp which we use in |GetToolchainDir()|.
298  """
299  if force != False and force != '--force':
300    print >>sys.stderr, 'Unknown parameter "%s"' % force
301    return 1
302  if force == '--force' or os.path.exists(json_data_file):
303    force = True
304
305  depot_tools_win_toolchain = \
306      bool(int(os.environ.get('DEPOT_TOOLS_WIN_TOOLCHAIN', '1')))
307  if ((sys.platform in ('win32', 'cygwin') or force) and
308        depot_tools_win_toolchain):
309    import find_depot_tools
310    depot_tools_path = find_depot_tools.add_depot_tools_to_path()
311    # Necessary so that get_toolchain_if_necessary.py will put the VS toolkit
312    # in the correct directory.
313    os.environ['GYP_MSVS_VERSION'] = GetVisualStudioVersion()
314    get_toolchain_args = [
315        sys.executable,
316        os.path.join(depot_tools_path,
317                    'win_toolchain',
318                    'get_toolchain_if_necessary.py'),
319        '--output-json', json_data_file,
320      ] + _GetDesiredVsToolchainHashes()
321    if force:
322      get_toolchain_args.append('--force')
323    subprocess.check_call(get_toolchain_args)
324
325  return 0
326
327
328def NormalizePath(path):
329  while path.endswith("\\"):
330    path = path[:-1]
331  return path
332
333
334def GetToolchainDir():
335  """Gets location information about the current toolchain (must have been
336  previously updated by 'update'). This is used for the GN build."""
337  runtime_dll_dirs = SetEnvironmentAndGetRuntimeDllDirs()
338
339  # If WINDOWSSDKDIR is not set, search the default SDK path and set it.
340  if not 'WINDOWSSDKDIR' in os.environ:
341    default_sdk_path = 'C:\\Program Files (x86)\\Windows Kits\\10'
342    if os.path.isdir(default_sdk_path):
343      os.environ['WINDOWSSDKDIR'] = default_sdk_path
344
345  print '''vs_path = "%s"
346sdk_path = "%s"
347vs_version = "%s"
348wdk_dir = "%s"
349runtime_dirs = "%s"
350''' % (
351      NormalizePath(os.environ['GYP_MSVS_OVERRIDE_PATH']),
352      NormalizePath(os.environ['WINDOWSSDKDIR']),
353      GetVisualStudioVersion(),
354      NormalizePath(os.environ.get('WDK_DIR', '')),
355      os.path.pathsep.join(runtime_dll_dirs or ['None']))
356
357
358def main():
359  commands = {
360      'update': Update,
361      'get_toolchain_dir': GetToolchainDir,
362      'copy_dlls': CopyDlls,
363  }
364  if len(sys.argv) < 2 or sys.argv[1] not in commands:
365    print >>sys.stderr, 'Expected one of: %s' % ', '.join(commands)
366    return 1
367  return commands[sys.argv[1]](*sys.argv[2:])
368
369
370if __name__ == '__main__':
371  sys.exit(main())
372