1# Copyright (c) 2013 Google Inc. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Handle version information related to Visual Stuio."""
6
7import errno
8import os
9import re
10import subprocess
11import sys
12import gyp
13import glob
14
15
16def JoinPath(*args):
17  return os.path.normpath(os.path.join(*args))
18
19
20class VisualStudioVersion(object):
21  """Information regarding a version of Visual Studio."""
22
23  def __init__(self, short_name, description,
24               solution_version, project_version, flat_sln, uses_vcxproj,
25               path, sdk_based, default_toolset=None, compatible_sdks=None):
26    self.short_name = short_name
27    self.description = description
28    self.solution_version = solution_version
29    self.project_version = project_version
30    self.flat_sln = flat_sln
31    self.uses_vcxproj = uses_vcxproj
32    self.path = path
33    self.sdk_based = sdk_based
34    self.default_toolset = default_toolset
35    compatible_sdks = compatible_sdks or []
36    compatible_sdks.sort(key=lambda v: float(v.replace('v', '')), reverse=True)
37    self.compatible_sdks = compatible_sdks
38
39  def ShortName(self):
40    return self.short_name
41
42  def Description(self):
43    """Get the full description of the version."""
44    return self.description
45
46  def SolutionVersion(self):
47    """Get the version number of the sln files."""
48    return self.solution_version
49
50  def ProjectVersion(self):
51    """Get the version number of the vcproj or vcxproj files."""
52    return self.project_version
53
54  def FlatSolution(self):
55    return self.flat_sln
56
57  def UsesVcxproj(self):
58    """Returns true if this version uses a vcxproj file."""
59    return self.uses_vcxproj
60
61  def ProjectExtension(self):
62    """Returns the file extension for the project."""
63    return self.uses_vcxproj and '.vcxproj' or '.vcproj'
64
65  def Path(self):
66    """Returns the path to Visual Studio installation."""
67    return self.path
68
69  def ToolPath(self, tool):
70    """Returns the path to a given compiler tool. """
71    return os.path.normpath(os.path.join(self.path, "VC/bin", tool))
72
73  def DefaultToolset(self):
74    """Returns the msbuild toolset version that will be used in the absence
75    of a user override."""
76    return self.default_toolset
77
78
79  def _SetupScriptInternal(self, target_arch):
80    """Returns a command (with arguments) to be used to set up the
81    environment."""
82    assert target_arch in ('x86', 'x64'), "target_arch not supported"
83    # If WindowsSDKDir is set and SetEnv.Cmd exists then we are using the
84    # depot_tools build tools and should run SetEnv.Cmd to set up the
85    # environment. The check for WindowsSDKDir alone is not sufficient because
86    # this is set by running vcvarsall.bat.
87    sdk_dir = os.environ.get('WindowsSDKDir', '')
88    setup_path = JoinPath(sdk_dir, 'Bin', 'SetEnv.Cmd')
89    if self.sdk_based and sdk_dir and os.path.exists(setup_path):
90      return [setup_path, '/' + target_arch]
91
92    is_host_arch_x64 = (
93      os.environ.get('PROCESSOR_ARCHITECTURE') == 'AMD64' or
94      os.environ.get('PROCESSOR_ARCHITEW6432') == 'AMD64'
95    )
96
97    # For VS2017 (and newer) it's fairly easy
98    if self.short_name >= '2017':
99      script_path = JoinPath(self.path,
100                             'VC', 'Auxiliary', 'Build', 'vcvarsall.bat')
101
102      # Always use a native executable, cross-compiling if necessary.
103      host_arch = 'amd64' if is_host_arch_x64 else 'x86'
104      msvc_target_arch = 'amd64' if target_arch == 'x64' else 'x86'
105      arg = host_arch
106      if host_arch != msvc_target_arch:
107        arg += '_' + msvc_target_arch
108
109      return [script_path, arg]
110
111    # We try to find the best version of the env setup batch.
112    vcvarsall = JoinPath(self.path, 'VC', 'vcvarsall.bat')
113    if target_arch == 'x86':
114      if self.short_name >= '2013' and self.short_name[-1] != 'e' and \
115         is_host_arch_x64:
116        # VS2013 and later, non-Express have a x64-x86 cross that we want
117        # to prefer.
118        return [vcvarsall, 'amd64_x86']
119      else:
120        # Otherwise, the standard x86 compiler. We don't use VC/vcvarsall.bat
121        # for x86 because vcvarsall calls vcvars32, which it can only find if
122        # VS??COMNTOOLS is set, which isn't guaranteed.
123        return [JoinPath(self.path, 'Common7', 'Tools', 'vsvars32.bat')]
124    elif target_arch == 'x64':
125      arg = 'x86_amd64'
126      # Use the 64-on-64 compiler if we're not using an express edition and
127      # we're running on a 64bit OS.
128      if self.short_name[-1] != 'e' and is_host_arch_x64:
129        arg = 'amd64'
130      return [vcvarsall, arg]
131
132  def SetupScript(self, target_arch):
133    script_data = self._SetupScriptInternal(target_arch)
134    script_path = script_data[0]
135    if not os.path.exists(script_path):
136      raise Exception('%s is missing - make sure VC++ tools are installed.' %
137                      script_path)
138    return script_data
139
140
141def _RegistryQueryBase(sysdir, key, value):
142  """Use reg.exe to read a particular key.
143
144  While ideally we might use the win32 module, we would like gyp to be
145  python neutral, so for instance cygwin python lacks this module.
146
147  Arguments:
148    sysdir: The system subdirectory to attempt to launch reg.exe from.
149    key: The registry key to read from.
150    value: The particular value to read.
151  Return:
152    stdout from reg.exe, or None for failure.
153  """
154  # Skip if not on Windows or Python Win32 setup issue
155  if sys.platform not in ('win32', 'cygwin'):
156    return None
157  # Setup params to pass to and attempt to launch reg.exe
158  cmd = [os.path.join(os.environ.get('WINDIR', ''), sysdir, 'reg.exe'),
159         'query', key]
160  if value:
161    cmd.extend(['/v', value])
162  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
163  # Obtain the stdout from reg.exe, reading to the end so p.returncode is valid
164  # Note that the error text may be in [1] in some cases
165  text = p.communicate()[0]
166  # Check return code from reg.exe; officially 0==success and 1==error
167  if p.returncode:
168    return None
169  return text
170
171
172def _RegistryQuery(key, value=None):
173  r"""Use reg.exe to read a particular key through _RegistryQueryBase.
174
175  First tries to launch from %WinDir%\Sysnative to avoid WoW64 redirection. If
176  that fails, it falls back to System32.  Sysnative is available on Vista and
177  up and available on Windows Server 2003 and XP through KB patch 942589. Note
178  that Sysnative will always fail if using 64-bit python due to it being a
179  virtual directory and System32 will work correctly in the first place.
180
181  KB 942589 - http://support.microsoft.com/kb/942589/en-us.
182
183  Arguments:
184    key: The registry key.
185    value: The particular registry value to read (optional).
186  Return:
187    stdout from reg.exe, or None for failure.
188  """
189  text = None
190  try:
191    text = _RegistryQueryBase('Sysnative', key, value)
192  except OSError as e:
193    if e.errno == errno.ENOENT:
194      text = _RegistryQueryBase('System32', key, value)
195    else:
196      raise
197  return text
198
199
200def _RegistryGetValueUsingWinReg(key, value):
201  """Use the _winreg module to obtain the value of a registry key.
202
203  Args:
204    key: The registry key.
205    value: The particular registry value to read.
206  Return:
207    contents of the registry key's value, or None on failure.  Throws
208    ImportError if _winreg is unavailable.
209  """
210  try:
211    import _winreg as winreg
212  except ImportError:
213    import winreg
214  try:
215    root, subkey = key.split('\\', 1)
216    assert root == 'HKLM'  # Only need HKLM for now.
217    with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, subkey) as hkey:
218      return winreg.QueryValueEx(hkey, value)[0]
219  except WindowsError:
220    return None
221
222
223def _RegistryGetValue(key, value):
224  """Use _winreg or reg.exe to obtain the value of a registry key.
225
226  Using _winreg is preferable because it solves an issue on some corporate
227  environments where access to reg.exe is locked down. However, we still need
228  to fallback to reg.exe for the case where the _winreg module is not available
229  (for example in cygwin python).
230
231  Args:
232    key: The registry key.
233    value: The particular registry value to read.
234  Return:
235    contents of the registry key's value, or None on failure.
236  """
237  try:
238    return _RegistryGetValueUsingWinReg(key, value)
239  except ImportError:
240    pass
241
242  # Fallback to reg.exe if we fail to import _winreg.
243  text = _RegistryQuery(key, value)
244  if not text:
245    return None
246  # Extract value.
247  match = re.search(r'REG_\w+\s+([^\r]+)\r\n', text)
248  if not match:
249    return None
250  return match.group(1)
251
252
253def _CreateVersion(name, path, sdk_based=False):
254  """Sets up MSVS project generation.
255
256  Setup is based off the GYP_MSVS_VERSION environment variable or whatever is
257  autodetected if GYP_MSVS_VERSION is not explicitly specified. If a version is
258  passed in that doesn't match a value in versions python will throw a error.
259  """
260  if path:
261    path = os.path.normpath(path)
262  versions = {
263      '2019': VisualStudioVersion('2019',
264                                  'Visual Studio 2019',
265                                  solution_version='12.00',
266                                  project_version='15.0',
267                                  flat_sln=False,
268                                  uses_vcxproj=True,
269                                  path=path,
270                                  sdk_based=sdk_based,
271                                  default_toolset='v141',
272                                  compatible_sdks=['v8.1', 'v10.0']),
273      '2017': VisualStudioVersion('2017',
274                                  'Visual Studio 2017',
275                                  solution_version='12.00',
276                                  project_version='15.0',
277                                  flat_sln=False,
278                                  uses_vcxproj=True,
279                                  path=path,
280                                  sdk_based=sdk_based,
281                                  default_toolset='v141',
282                                  compatible_sdks=['v8.1', 'v10.0']),
283      '2015': VisualStudioVersion('2015',
284                                  'Visual Studio 2015',
285                                  solution_version='12.00',
286                                  project_version='14.0',
287                                  flat_sln=False,
288                                  uses_vcxproj=True,
289                                  path=path,
290                                  sdk_based=sdk_based,
291                                  default_toolset='v140'),
292      '2013': VisualStudioVersion('2013',
293                                  'Visual Studio 2013',
294                                  solution_version='13.00',
295                                  project_version='12.0',
296                                  flat_sln=False,
297                                  uses_vcxproj=True,
298                                  path=path,
299                                  sdk_based=sdk_based,
300                                  default_toolset='v120'),
301      '2013e': VisualStudioVersion('2013e',
302                                   'Visual Studio 2013',
303                                   solution_version='13.00',
304                                   project_version='12.0',
305                                   flat_sln=True,
306                                   uses_vcxproj=True,
307                                   path=path,
308                                   sdk_based=sdk_based,
309                                   default_toolset='v120'),
310      '2012': VisualStudioVersion('2012',
311                                  'Visual Studio 2012',
312                                  solution_version='12.00',
313                                  project_version='4.0',
314                                  flat_sln=False,
315                                  uses_vcxproj=True,
316                                  path=path,
317                                  sdk_based=sdk_based,
318                                  default_toolset='v110'),
319      '2012e': VisualStudioVersion('2012e',
320                                   'Visual Studio 2012',
321                                   solution_version='12.00',
322                                   project_version='4.0',
323                                   flat_sln=True,
324                                   uses_vcxproj=True,
325                                   path=path,
326                                   sdk_based=sdk_based,
327                                   default_toolset='v110'),
328      '2010': VisualStudioVersion('2010',
329                                  'Visual Studio 2010',
330                                  solution_version='11.00',
331                                  project_version='4.0',
332                                  flat_sln=False,
333                                  uses_vcxproj=True,
334                                  path=path,
335                                  sdk_based=sdk_based),
336      '2010e': VisualStudioVersion('2010e',
337                                   'Visual C++ Express 2010',
338                                   solution_version='11.00',
339                                   project_version='4.0',
340                                   flat_sln=True,
341                                   uses_vcxproj=True,
342                                   path=path,
343                                   sdk_based=sdk_based),
344      '2008': VisualStudioVersion('2008',
345                                  'Visual Studio 2008',
346                                  solution_version='10.00',
347                                  project_version='9.00',
348                                  flat_sln=False,
349                                  uses_vcxproj=False,
350                                  path=path,
351                                  sdk_based=sdk_based),
352      '2008e': VisualStudioVersion('2008e',
353                                   'Visual Studio 2008',
354                                   solution_version='10.00',
355                                   project_version='9.00',
356                                   flat_sln=True,
357                                   uses_vcxproj=False,
358                                   path=path,
359                                   sdk_based=sdk_based),
360      '2005': VisualStudioVersion('2005',
361                                  'Visual Studio 2005',
362                                  solution_version='9.00',
363                                  project_version='8.00',
364                                  flat_sln=False,
365                                  uses_vcxproj=False,
366                                  path=path,
367                                  sdk_based=sdk_based),
368      '2005e': VisualStudioVersion('2005e',
369                                   'Visual Studio 2005',
370                                   solution_version='9.00',
371                                   project_version='8.00',
372                                   flat_sln=True,
373                                   uses_vcxproj=False,
374                                   path=path,
375                                   sdk_based=sdk_based),
376  }
377  return versions[str(name)]
378
379
380def _ConvertToCygpath(path):
381  """Convert to cygwin path if we are using cygwin."""
382  if sys.platform == 'cygwin':
383    p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE)
384    path = p.communicate()[0].strip()
385  return path
386
387
388def _DetectVisualStudioVersions(versions_to_check, force_express):
389  """Collect the list of installed visual studio versions.
390
391  Returns:
392    A list of visual studio versions installed in descending order of
393    usage preference.
394    Base this on the registry and a quick check if devenv.exe exists.
395    Possibilities are:
396      2005(e) - Visual Studio 2005 (8)
397      2008(e) - Visual Studio 2008 (9)
398      2010(e) - Visual Studio 2010 (10)
399      2012(e) - Visual Studio 2012 (11)
400      2013(e) - Visual Studio 2013 (12)
401      2015    - Visual Studio 2015 (14)
402      2017    - Visual Studio 2017 (15)
403    Where (e) is e for express editions of MSVS and blank otherwise.
404  """
405  version_to_year = {
406      '8.0': '2005',
407      '9.0': '2008',
408      '10.0': '2010',
409      '11.0': '2012',
410      '12.0': '2013',
411      '14.0': '2015',
412      '15.0': '2017'
413  }
414  versions = []
415  for version in versions_to_check:
416    # Old method of searching for which VS version is installed
417    # We don't use the 2010-encouraged-way because we also want to get the
418    # path to the binaries, which it doesn't offer.
419    keys = [r'HKLM\Software\Microsoft\VisualStudio\%s' % version,
420            r'HKLM\Software\Wow6432Node\Microsoft\VisualStudio\%s' % version,
421            r'HKLM\Software\Microsoft\VCExpress\%s' % version,
422            r'HKLM\Software\Wow6432Node\Microsoft\VCExpress\%s' % version]
423    for index in range(len(keys)):
424      path = _RegistryGetValue(keys[index], 'InstallDir')
425      if not path:
426        continue
427      path = _ConvertToCygpath(path)
428      # Check for full.
429      full_path = os.path.join(path, 'devenv.exe')
430      express_path = os.path.join(path, '*express.exe')
431      if not force_express and os.path.exists(full_path):
432        # Add this one.
433        versions.append(_CreateVersion(version_to_year[version],
434            os.path.join(path, '..', '..')))
435      # Check for express.
436      elif glob.glob(express_path):
437        # Add this one.
438        versions.append(_CreateVersion(version_to_year[version] + 'e',
439            os.path.join(path, '..', '..')))
440
441    # The old method above does not work when only SDK is installed.
442    keys = [r'HKLM\Software\Microsoft\VisualStudio\SxS\VC7',
443            r'HKLM\Software\Wow6432Node\Microsoft\VisualStudio\SxS\VC7',
444            r'HKLM\Software\Microsoft\VisualStudio\SxS\VS7',
445            r'HKLM\Software\Wow6432Node\Microsoft\VisualStudio\SxS\VS7']
446    for index in range(len(keys)):
447      path = _RegistryGetValue(keys[index], version)
448      if not path:
449        continue
450      path = _ConvertToCygpath(path)
451      if version == '15.0':
452          if os.path.exists(path):
453              versions.append(_CreateVersion('2017', path))
454      elif version != '14.0':  # There is no Express edition for 2015.
455        versions.append(_CreateVersion(version_to_year[version] + 'e',
456            os.path.join(path, '..'), sdk_based=True))
457
458  return versions
459
460
461def SelectVisualStudioVersion(version='auto', allow_fallback=True):
462  """Select which version of Visual Studio projects to generate.
463
464  Arguments:
465    version: Hook to allow caller to force a particular version (vs auto).
466  Returns:
467    An object representing a visual studio project format version.
468  """
469  # In auto mode, check environment variable for override.
470  if version == 'auto':
471    version = os.environ.get('GYP_MSVS_VERSION', 'auto')
472  version_map = {
473    'auto': ('15.0', '14.0', '12.0', '10.0', '9.0', '8.0', '11.0'),
474    '2005': ('8.0',),
475    '2005e': ('8.0',),
476    '2008': ('9.0',),
477    '2008e': ('9.0',),
478    '2010': ('10.0',),
479    '2010e': ('10.0',),
480    '2012': ('11.0',),
481    '2012e': ('11.0',),
482    '2013': ('12.0',),
483    '2013e': ('12.0',),
484    '2015': ('14.0',),
485    '2017': ('15.0',),
486  }
487  override_path = os.environ.get('GYP_MSVS_OVERRIDE_PATH')
488  if override_path:
489    msvs_version = os.environ.get('GYP_MSVS_VERSION')
490    if not msvs_version:
491      raise ValueError('GYP_MSVS_OVERRIDE_PATH requires GYP_MSVS_VERSION to be '
492                       'set to a particular version (e.g. 2010e).')
493    return _CreateVersion(msvs_version, override_path, sdk_based=True)
494  version = str(version)
495  versions = _DetectVisualStudioVersions(version_map[version], 'e' in version)
496  if not versions:
497    if not allow_fallback:
498      raise ValueError('Could not locate Visual Studio installation.')
499    if version == 'auto':
500      # Default to 2005 if we couldn't find anything
501      return _CreateVersion('2005', None)
502    else:
503      return _CreateVersion(version, None)
504  return versions[0]
505