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