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