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