1"""
2Common helper functions for working with the Microsoft tool chain.
3"""
4#
5# Copyright (c) 2001 - 2019 The SCons Foundation
6#
7# Permission is hereby granted, free of charge, to any person obtaining
8# a copy of this software and associated documentation files (the
9# "Software"), to deal in the Software without restriction, including
10# without limitation the rights to use, copy, modify, merge, publish,
11# distribute, sublicense, and/or sell copies of the Software, and to
12# permit persons to whom the Software is furnished to do so, subject to
13# the following conditions:
14#
15# The above copyright notice and this permission notice shall be included
16# in all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
19# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
20# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25#
26from __future__ import print_function
27
28__revision__ = "src/engine/SCons/Tool/MSCommon/common.py bee7caf9defd6e108fc2998a2520ddb36a967691 2019-12-17 02:07:09 bdeegan"
29
30import copy
31import json
32import os
33import re
34import subprocess
35import sys
36
37import SCons.Util
38
39# SCONS_MSCOMMON_DEBUG is internal-use so undocumented:
40# set to '-' to print to console, else set to filename to log to
41LOGFILE = os.environ.get('SCONS_MSCOMMON_DEBUG')
42if LOGFILE == '-':
43    def debug(message):
44        print(message)
45elif LOGFILE:
46    import logging
47    logging.basicConfig(
48        format='%(relativeCreated)05dms:pid%(process)05d:MSCommon/%(filename)s:%(message)s',
49        filename=LOGFILE,
50        level=logging.DEBUG)
51    debug = logging.getLogger(name=__name__).debug
52else:
53    debug = lambda x: None
54
55
56# SCONS_CACHE_MSVC_CONFIG is public, and is documented.
57CONFIG_CACHE = os.environ.get('SCONS_CACHE_MSVC_CONFIG')
58if CONFIG_CACHE in ('1', 'true', 'True'):
59    CONFIG_CACHE = os.path.join(os.path.expanduser('~'), '.scons_msvc_cache')
60
61def read_script_env_cache():
62    """ fetch cached msvc env vars if requested, else return empty dict """
63    envcache = {}
64    if CONFIG_CACHE:
65        try:
66            with open(CONFIG_CACHE, 'r') as f:
67                envcache = json.load(f)
68        #TODO can use more specific FileNotFoundError when py2 dropped
69        except IOError:
70            # don't fail if no cache file, just proceed without it
71            pass
72    return envcache
73
74
75def write_script_env_cache(cache):
76    """ write out cache of msvc env vars if requested """
77    if CONFIG_CACHE:
78        try:
79            with open(CONFIG_CACHE, 'w') as f:
80                json.dump(cache, f, indent=2)
81        except TypeError:
82            # data can't serialize to json, don't leave partial file
83            os.remove(CONFIG_CACHE)
84        except IOError:
85            # can't write the file, just skip
86            pass
87
88
89_is_win64 = None
90
91def is_win64():
92    """Return true if running on windows 64 bits.
93
94    Works whether python itself runs in 64 bits or 32 bits."""
95    # Unfortunately, python does not provide a useful way to determine
96    # if the underlying Windows OS is 32-bit or 64-bit.  Worse, whether
97    # the Python itself is 32-bit or 64-bit affects what it returns,
98    # so nothing in sys.* or os.* help.
99
100    # Apparently the best solution is to use env vars that Windows
101    # sets.  If PROCESSOR_ARCHITECTURE is not x86, then the python
102    # process is running in 64 bit mode (on a 64-bit OS, 64-bit
103    # hardware, obviously).
104    # If this python is 32-bit but the OS is 64, Windows will set
105    # ProgramW6432 and PROCESSOR_ARCHITEW6432 to non-null.
106    # (Checking for HKLM\Software\Wow6432Node in the registry doesn't
107    # work, because some 32-bit installers create it.)
108    global _is_win64
109    if _is_win64 is None:
110        # I structured these tests to make it easy to add new ones or
111        # add exceptions in the future, because this is a bit fragile.
112        _is_win64 = False
113        if os.environ.get('PROCESSOR_ARCHITECTURE', 'x86') != 'x86':
114            _is_win64 = True
115        if os.environ.get('PROCESSOR_ARCHITEW6432'):
116            _is_win64 = True
117        if os.environ.get('ProgramW6432'):
118            _is_win64 = True
119    return _is_win64
120
121
122def read_reg(value, hkroot=SCons.Util.HKEY_LOCAL_MACHINE):
123    return SCons.Util.RegGetValue(hkroot, value)[0]
124
125def has_reg(value):
126    """Return True if the given key exists in HKEY_LOCAL_MACHINE, False
127    otherwise."""
128    try:
129        SCons.Util.RegOpenKeyEx(SCons.Util.HKEY_LOCAL_MACHINE, value)
130        ret = True
131    except SCons.Util.WinError:
132        ret = False
133    return ret
134
135# Functions for fetching environment variable settings from batch files.
136
137def normalize_env(env, keys, force=False):
138    """Given a dictionary representing a shell environment, add the variables
139    from os.environ needed for the processing of .bat files; the keys are
140    controlled by the keys argument.
141
142    It also makes sure the environment values are correctly encoded.
143
144    If force=True, then all of the key values that exist are copied
145    into the returned dictionary.  If force=false, values are only
146    copied if the key does not already exist in the copied dictionary.
147
148    Note: the environment is copied."""
149    normenv = {}
150    if env:
151        for k in list(env.keys()):
152            normenv[k] = copy.deepcopy(env[k])
153
154        for k in keys:
155            if k in os.environ and (force or k not in normenv):
156                normenv[k] = os.environ[k]
157
158    # This shouldn't be necessary, since the default environment should include system32,
159    # but keep this here to be safe, since it's needed to find reg.exe which the MSVC
160    # bat scripts use.
161    sys32_dir = os.path.join(os.environ.get("SystemRoot",
162                                            os.environ.get("windir", r"C:\Windows\system32")),
163                             "System32")
164
165    if sys32_dir not in normenv['PATH']:
166        normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_dir
167
168    # Without Wbem in PATH, vcvarsall.bat has a "'wmic' is not recognized"
169    # error starting with Visual Studio 2017, although the script still
170    # seems to work anyway.
171    sys32_wbem_dir = os.path.join(sys32_dir, 'Wbem')
172    if sys32_wbem_dir not in normenv['PATH']:
173        normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_wbem_dir
174
175    debug("PATH: %s"%normenv['PATH'])
176
177    return normenv
178
179def get_output(vcbat, args = None, env = None):
180    """Parse the output of given bat file, with given args."""
181
182    if env is None:
183        # Create a blank environment, for use in launching the tools
184        env = SCons.Environment.Environment(tools=[])
185
186    # TODO:  This is a hard-coded list of the variables that (may) need
187    # to be imported from os.environ[] for v[sc]*vars*.bat file
188    # execution to work.  This list should really be either directly
189    # controlled by vc.py, or else derived from the common_tools_var
190    # settings in vs.py.
191    vs_vc_vars = [
192        'COMSPEC',
193        # VS100 and VS110: Still set, but modern MSVC setup scripts will
194        # discard these if registry has values.  However Intel compiler setup
195        # script still requires these as of 2013/2014.
196        'VS140COMNTOOLS',
197        'VS120COMNTOOLS',
198        'VS110COMNTOOLS',
199        'VS100COMNTOOLS',
200        'VS90COMNTOOLS',
201        'VS80COMNTOOLS',
202        'VS71COMNTOOLS',
203        'VS70COMNTOOLS',
204        'VS60COMNTOOLS',
205    ]
206    env['ENV'] = normalize_env(env['ENV'], vs_vc_vars, force=False)
207
208    if args:
209        debug("Calling '%s %s'" % (vcbat, args))
210        popen = SCons.Action._subproc(env,
211                                      '"%s" %s & set' % (vcbat, args),
212                                      stdin='devnull',
213                                      stdout=subprocess.PIPE,
214                                      stderr=subprocess.PIPE)
215    else:
216        debug("Calling '%s'" % vcbat)
217        popen = SCons.Action._subproc(env,
218                                      '"%s" & set' % vcbat,
219                                      stdin='devnull',
220                                      stdout=subprocess.PIPE,
221                                      stderr=subprocess.PIPE)
222
223    # Use the .stdout and .stderr attributes directly because the
224    # .communicate() method uses the threading module on Windows
225    # and won't work under Pythons not built with threading.
226    with popen.stdout:
227        stdout = popen.stdout.read()
228    with popen.stderr:
229        stderr = popen.stderr.read()
230
231    # Extra debug logic, uncomment if necessary
232#     debug('get_output():stdout:%s'%stdout)
233#     debug('get_output():stderr:%s'%stderr)
234
235    if stderr:
236        # TODO: find something better to do with stderr;
237        # this at least prevents errors from getting swallowed.
238
239        # Nuitka: this is writing bytes to stderr which wants unicode
240        sys.stderr.write(stderr.decode("mbcs"))
241    if popen.wait() != 0:
242        raise IOError(stderr.decode("mbcs"))
243
244    output = stdout.decode("mbcs")
245    return output
246
247KEEPLIST = ("INCLUDE", "LIB", "LIBPATH", "PATH", 'VSCMD_ARG_app_plat')
248# Nuitka: Keep the Windows SDK version too
249KEEPLIST += ("WindowsSDKVersion",)
250
251def parse_output(output, keep=KEEPLIST):
252    """
253    Parse output from running visual c++/studios vcvarsall.bat and running set
254    To capture the values listed in keep
255    """
256
257    # dkeep is a dict associating key: path_list, where key is one item from
258    # keep, and path_list the associated list of paths
259    dkeep = dict([(i, []) for i in keep])
260
261    # rdk will  keep the regex to match the .bat file output line starts
262    rdk = {}
263    for i in keep:
264        rdk[i] = re.compile('%s=(.*)' % i, re.I)
265
266    def add_env(rmatch, key, dkeep=dkeep):
267        path_list = rmatch.group(1).split(os.pathsep)
268        for path in path_list:
269            # Do not add empty paths (when a var ends with ;)
270            if path:
271                # XXX: For some reason, VC98 .bat file adds "" around the PATH
272                # values, and it screws up the environment later, so we strip
273                # it.
274                path = path.strip('"')
275                dkeep[key].append(str(path))
276
277    for line in output.splitlines():
278        for k, value in rdk.items():
279            match = value.match(line)
280            if match:
281                add_env(match, k)
282
283    return dkeep
284
285# Local Variables:
286# tab-width:4
287# indent-tabs-mode:nil
288# End:
289# vim: set expandtab tabstop=4 shiftwidth=4:
290