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