common.py (SCons-4.3.0) | : | common.py (SCons-4.4.0) | ||
---|---|---|---|---|
""" | # MIT License | |||
Common helper functions for working with the Microsoft tool chain. | ||||
""" | ||||
# | # | |||
# __COPYRIGHT__ | # Copyright The SCons Foundation | |||
# | # | |||
# Permission is hereby granted, free of charge, to any person obtaining | # Permission is hereby granted, free of charge, to any person obtaining | |||
# a copy of this software and associated documentation files (the | # a copy of this software and associated documentation files (the | |||
# "Software"), to deal in the Software without restriction, including | # "Software"), to deal in the Software without restriction, including | |||
# without limitation the rights to use, copy, modify, merge, publish, | # without limitation the rights to use, copy, modify, merge, publish, | |||
# distribute, sublicense, and/or sell copies of the Software, and to | # distribute, sublicense, and/or sell copies of the Software, and to | |||
# permit persons to whom the Software is furnished to do so, subject to | # permit persons to whom the Software is furnished to do so, subject to | |||
# the following conditions: | # the following conditions: | |||
# | # | |||
# The above copyright notice and this permission notice shall be included | # The above copyright notice and this permission notice shall be included | |||
# in all copies or substantial portions of the Software. | # in all copies or substantial portions of the Software. | |||
# | # | |||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY | |||
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | |||
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |||
# | ||||
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" | """ | |||
Common helper functions for working with the Microsoft tool chain. | ||||
""" | ||||
import copy | import copy | |||
import json | import json | |||
import os | import os | |||
import re | import re | |||
import subprocess | import subprocess | |||
import sys | import sys | |||
from contextlib import suppress | ||||
from pathlib import Path | ||||
import SCons.Util | import SCons.Util | |||
import SCons.Warnings | ||||
class MSVCCacheInvalidWarning(SCons.Warnings.WarningOnByDefault): | ||||
pass | ||||
# SCONS_MSCOMMON_DEBUG is internal-use so undocumented: | # SCONS_MSCOMMON_DEBUG is internal-use so undocumented: | |||
# set to '-' to print to console, else set to filename to log to | # set to '-' to print to console, else set to filename to log to | |||
LOGFILE = os.environ.get('SCONS_MSCOMMON_DEBUG') | LOGFILE = os.environ.get('SCONS_MSCOMMON_DEBUG') | |||
if LOGFILE == '-': | if LOGFILE: | |||
def debug(message): | ||||
print(message) | ||||
elif LOGFILE: | ||||
import logging | import logging | |||
modulelist = ( | modulelist = ( | |||
# root module and parent/root module | # root module and parent/root module | |||
'MSCommon', 'Tool', | 'MSCommon', 'Tool', | |||
# python library and below: correct iff scons does not have a lib folder | # python library and below: correct iff scons does not have a lib folder | |||
'lib', | 'lib', | |||
# scons modules | # scons modules | |||
'SCons', 'test', 'scons' | 'SCons', 'test', 'scons' | |||
) | ) | |||
def get_relative_filename(filename, module_list): | def get_relative_filename(filename, module_list): | |||
if not filename: | if not filename: | |||
return filename | return filename | |||
for module in module_list: | for module in module_list: | |||
try: | try: | |||
ind = filename.rindex(module) | ind = filename.rindex(module) | |||
return filename[ind:] | return filename[ind:] | |||
except ValueError: | except ValueError: | |||
pass | pass | |||
return filename | return filename | |||
class _Debug_Filter(logging.Filter): | class _Debug_Filter(logging.Filter): | |||
# custom filter for module relative filename | # custom filter for module relative filename | |||
def filter(self, record): | def filter(self, record): | |||
relfilename = get_relative_filename(record.pathname, modulelist) | relfilename = get_relative_filename(record.pathname, modulelist) | |||
relfilename = relfilename.replace('\\', '/') | relfilename = relfilename.replace('\\', '/') | |||
record.relfilename = relfilename | record.relfilename = relfilename | |||
return True | return True | |||
logging.basicConfig( | ||||
# This looks like: | # Log format looks like: | |||
# 00109ms:MSCommon/vc.py:find_vc_pdir#447: | # 00109ms:MSCommon/vc.py:find_vc_pdir#447: VC found '14.3' [file] | |||
format=( | # debug: 00109ms:MSCommon/vc.py:find_vc_pdir#447: VC found '14.3' [stdout] | |||
'%(relativeCreated)05dms' | log_format=( | |||
':%(relfilename)s' | '%(relativeCreated)05dms' | |||
':%(funcName)s' | ':%(relfilename)s' | |||
'#%(lineno)s' | ':%(funcName)s' | |||
':%(message)s: ' | '#%(lineno)s' | |||
), | ': %(message)s' | |||
filename=LOGFILE, | ) | |||
level=logging.DEBUG) | if LOGFILE == '-': | |||
log_format = 'debug: ' + log_format | ||||
log_handler = logging.StreamHandler(sys.stdout) | ||||
else: | ||||
log_handler = logging.FileHandler(filename=LOGFILE) | ||||
log_formatter = logging.Formatter(log_format) | ||||
log_handler.setFormatter(log_formatter) | ||||
logger = logging.getLogger(name=__name__) | logger = logging.getLogger(name=__name__) | |||
logger.setLevel(level=logging.DEBUG) | ||||
logger.addHandler(log_handler) | ||||
logger.addFilter(_Debug_Filter()) | logger.addFilter(_Debug_Filter()) | |||
debug = logger.debug | debug = logger.debug | |||
else: | else: | |||
def debug(x): return None | def debug(x, *args): | |||
return None | ||||
# SCONS_CACHE_MSVC_CONFIG is public, and is documented. | # SCONS_CACHE_MSVC_CONFIG is public, and is documented. | |||
CONFIG_CACHE = os.environ.get('SCONS_CACHE_MSVC_CONFIG') | CONFIG_CACHE = os.environ.get('SCONS_CACHE_MSVC_CONFIG') | |||
if CONFIG_CACHE in ('1', 'true', 'True'): | if CONFIG_CACHE in ('1', 'true', 'True'): | |||
CONFIG_CACHE = os.path.join(os.path.expanduser('~'), '.scons_msvc_cache') | CONFIG_CACHE = os.path.join(os.path.expanduser('~'), 'scons_msvc_cache.json' | |||
) | ||||
# SCONS_CACHE_MSVC_FORCE_DEFAULTS is internal-use so undocumented. | ||||
CONFIG_CACHE_FORCE_DEFAULT_ARGUMENTS = False | ||||
if CONFIG_CACHE: | ||||
if os.environ.get('SCONS_CACHE_MSVC_FORCE_DEFAULTS') in ('1', 'true', 'True' | ||||
): | ||||
CONFIG_CACHE_FORCE_DEFAULT_ARGUMENTS = True | ||||
def read_script_env_cache(): | def read_script_env_cache(): | |||
""" fetch cached msvc env vars if requested, else return empty dict """ | """ fetch cached msvc env vars if requested, else return empty dict """ | |||
envcache = {} | envcache = {} | |||
if CONFIG_CACHE: | if CONFIG_CACHE: | |||
try: | try: | |||
with open(CONFIG_CACHE, 'r') as f: | p = Path(CONFIG_CACHE) | |||
envcache = json.load(f) | with p.open('r') as f: | |||
# Convert the list of cache entry dictionaries read from | ||||
# json to the cache dictionary. Reconstruct the cache key | ||||
# tuple from the key list written to json. | ||||
envcache_list = json.load(f) | ||||
if isinstance(envcache_list, list): | ||||
envcache = {tuple(d['key']): d['data'] for d in envcache_lis | ||||
t} | ||||
else: | ||||
# don't fail if incompatible format, just proceed without it | ||||
warn_msg = "Incompatible format for msvc cache file {}: file | ||||
may be overwritten.".format( | ||||
repr(CONFIG_CACHE) | ||||
) | ||||
SCons.Warnings.warn(MSVCCacheInvalidWarning, warn_msg) | ||||
debug(warn_msg) | ||||
except FileNotFoundError: | except FileNotFoundError: | |||
# don't fail if no cache file, just proceed without it | # don't fail if no cache file, just proceed without it | |||
pass | pass | |||
return envcache | return envcache | |||
def write_script_env_cache(cache): | def write_script_env_cache(cache): | |||
""" write out cache of msvc env vars if requested """ | """ write out cache of msvc env vars if requested """ | |||
if CONFIG_CACHE: | if CONFIG_CACHE: | |||
try: | try: | |||
with open(CONFIG_CACHE, 'w') as f: | p = Path(CONFIG_CACHE) | |||
json.dump(cache, f, indent=2) | with p.open('w') as f: | |||
# Convert the cache dictionary to a list of cache entry | ||||
# dictionaries. The cache key is converted from a tuple to | ||||
# a list for compatibility with json. | ||||
envcache_list = [{'key': list(key), 'data': data} for key, data | ||||
in cache.items()] | ||||
json.dump(envcache_list, f, indent=2) | ||||
except TypeError: | except TypeError: | |||
# data can't serialize to json, don't leave partial file | # data can't serialize to json, don't leave partial file | |||
os.remove(CONFIG_CACHE) | with suppress(FileNotFoundError): | |||
p.unlink() | ||||
except IOError: | except IOError: | |||
# can't write the file, just skip | # can't write the file, just skip | |||
pass | pass | |||
_is_win64 = None | _is_win64 = None | |||
def is_win64(): | def is_win64(): | |||
"""Return true if running on windows 64 bits. | """Return true if running on windows 64 bits. | |||
Works whether python itself runs in 64 bits or 32 bits.""" | Works whether python itself runs in 64 bits or 32 bits.""" | |||
skipping to change at line 191 | skipping to change at line 231 | |||
for k in keys: | for k in keys: | |||
if k in os.environ and (force or k not in normenv): | if k in os.environ and (force or k not in normenv): | |||
normenv[k] = os.environ[k] | normenv[k] = os.environ[k] | |||
# add some things to PATH to prevent problems: | # add some things to PATH to prevent problems: | |||
# Shouldn't be necessary to add system32, since the default environment | # Shouldn't be necessary to add system32, since the default environment | |||
# should include it, but keep this here to be safe (needed for reg.exe) | # should include it, but keep this here to be safe (needed for reg.exe) | |||
sys32_dir = os.path.join( | sys32_dir = os.path.join( | |||
os.environ.get("SystemRoot", os.environ.get("windir", r"C:\Windows")), " System32" | os.environ.get("SystemRoot", os.environ.get("windir", r"C:\Windows")), " System32" | |||
) | ) | |||
if sys32_dir not in normenv["PATH"]: | if sys32_dir not in normenv["PATH"]: | |||
normenv["PATH"] = normenv["PATH"] + os.pathsep + sys32_dir | normenv["PATH"] = normenv["PATH"] + os.pathsep + sys32_dir | |||
# Without Wbem in PATH, vcvarsall.bat has a "'wmic' is not recognized" | # Without Wbem in PATH, vcvarsall.bat has a "'wmic' is not recognized" | |||
# error starting with Visual Studio 2017, although the script still | # error starting with Visual Studio 2017, although the script still | |||
# seems to work anyway. | # seems to work anyway. | |||
sys32_wbem_dir = os.path.join(sys32_dir, 'Wbem') | sys32_wbem_dir = os.path.join(sys32_dir, 'Wbem') | |||
if sys32_wbem_dir not in normenv['PATH']: | if sys32_wbem_dir not in normenv['PATH']: | |||
normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_wbem_dir | normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_wbem_dir | |||
# Without Powershell in PATH, an internal call to a telemetry | # Without Powershell in PATH, an internal call to a telemetry | |||
# function (starting with a VS2019 update) can fail | # function (starting with a VS2019 update) can fail | |||
# Note can also set VSCMD_SKIP_SENDTELEMETRY to avoid this. | # Note can also set VSCMD_SKIP_SENDTELEMETRY to avoid this. | |||
sys32_ps_dir = os.path.join(sys32_dir, r'WindowsPowerShell\v1.0') | sys32_ps_dir = os.path.join(sys32_dir, r'WindowsPowerShell\v1.0') | |||
if sys32_ps_dir not in normenv['PATH']: | if sys32_ps_dir not in normenv['PATH']: | |||
normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_ps_dir | normenv['PATH'] = normenv['PATH'] + os.pathsep + sys32_ps_dir | |||
debug("PATH: %s" % normenv['PATH']) | debug("PATH: %s", normenv['PATH']) | |||
return normenv | return normenv | |||
def get_output(vcbat, args=None, env=None): | def get_output(vcbat, args=None, env=None): | |||
"""Parse the output of given bat file, with given args.""" | """Parse the output of given bat file, with given args.""" | |||
if env is None: | if env is None: | |||
# Create a blank environment, for use in launching the tools | # Create a blank environment, for use in launching the tools | |||
env = SCons.Environment.Environment(tools=[]) | env = SCons.Environment.Environment(tools=[]) | |||
# TODO: Hard-coded list of the variables that (may) need to be | # TODO: Hard-coded list of the variables that (may) need to be | |||
skipping to change at line 232 | skipping to change at line 272 | |||
# end up running a dozen or more scripts, changes not only with | # end up running a dozen or more scripts, changes not only with | |||
# each release but with what is installed at the time. We think | # each release but with what is installed at the time. We think | |||
# in modern installations most are set along the way and don't | # in modern installations most are set along the way and don't | |||
# need to be picked from the env, but include these for safety's sake. | # need to be picked from the env, but include these for safety's sake. | |||
# Any VSCMD variables definitely are picked from the env and | # Any VSCMD variables definitely are picked from the env and | |||
# control execution in interesting ways. | # control execution in interesting ways. | |||
# Note these really should be unified - either controlled by vs.py, | # Note these really should be unified - either controlled by vs.py, | |||
# or synced with the the common_tools_var # settings in vs.py. | # or synced with the the common_tools_var # settings in vs.py. | |||
vs_vc_vars = [ | vs_vc_vars = [ | |||
'COMSPEC', # path to "shell" | 'COMSPEC', # path to "shell" | |||
'OS', # name of OS family: Windows_NT or undefined (95/98/ME) | ||||
'VS170COMNTOOLS', # path to common tools for given version | 'VS170COMNTOOLS', # path to common tools for given version | |||
'VS160COMNTOOLS', | 'VS160COMNTOOLS', | |||
'VS150COMNTOOLS', | 'VS150COMNTOOLS', | |||
'VS140COMNTOOLS', | 'VS140COMNTOOLS', | |||
'VS120COMNTOOLS', | 'VS120COMNTOOLS', | |||
'VS110COMNTOOLS', | 'VS110COMNTOOLS', | |||
'VS100COMNTOOLS', | 'VS100COMNTOOLS', | |||
'VS90COMNTOOLS', | 'VS90COMNTOOLS', | |||
'VS80COMNTOOLS', | 'VS80COMNTOOLS', | |||
'VS71COMNTOOLS', | 'VS71COMNTOOLS', | |||
'VS70COMNTOOLS', | 'VSCOMNTOOLS', | |||
'VS60COMNTOOLS', | 'MSDevDir', | |||
'VSCMD_DEBUG', # enable logging and other debug aids | 'VSCMD_DEBUG', # enable logging and other debug aids | |||
'VSCMD_SKIP_SENDTELEMETRY', | 'VSCMD_SKIP_SENDTELEMETRY', | |||
'windir', # windows directory (SystemRoot not available in 95/98/ME) | ||||
] | ] | |||
env['ENV'] = normalize_env(env['ENV'], vs_vc_vars, force=False) | env['ENV'] = normalize_env(env['ENV'], vs_vc_vars, force=False) | |||
if args: | if args: | |||
debug("Calling '%s %s'" % (vcbat, args)) | debug("Calling '%s %s'", vcbat, args) | |||
popen = SCons.Action._subproc(env, | popen = SCons.Action._subproc(env, | |||
'"%s" %s & set' % (vcbat, args), | '"%s" %s & set' % (vcbat, args), | |||
stdin='devnull', | stdin='devnull', | |||
stdout=subprocess.PIPE, | stdout=subprocess.PIPE, | |||
stderr=subprocess.PIPE) | stderr=subprocess.PIPE) | |||
else: | else: | |||
debug("Calling '%s'" % vcbat) | debug("Calling '%s'", vcbat) | |||
popen = SCons.Action._subproc(env, | popen = SCons.Action._subproc(env, | |||
'"%s" & set' % vcbat, | '"%s" & set' % vcbat, | |||
stdin='devnull', | stdin='devnull', | |||
stdout=subprocess.PIPE, | stdout=subprocess.PIPE, | |||
stderr=subprocess.PIPE) | stderr=subprocess.PIPE) | |||
# Use the .stdout and .stderr attributes directly because the | # Use the .stdout and .stderr attributes directly because the | |||
# .communicate() method uses the threading module on Windows | # .communicate() method uses the threading module on Windows | |||
# and won't work under Pythons not built with threading. | # and won't work under Pythons not built with threading. | |||
with popen.stdout: | with popen.stdout: | |||
stdout = popen.stdout.read() | stdout = popen.stdout.read() | |||
with popen.stderr: | with popen.stderr: | |||
stderr = popen.stderr.read() | stderr = popen.stderr.read() | |||
# Extra debug logic, uncomment if necessary | # Extra debug logic, uncomment if necessary | |||
# debug('stdout:%s' % stdout) | # debug('stdout:%s', stdout) | |||
# debug('stderr:%s' % stderr) | # debug('stderr:%s', stderr) | |||
# Ongoing problems getting non-corrupted text led to this | # Ongoing problems getting non-corrupted text led to this | |||
# changing to "oem" from "mbcs" - the scripts run presumably | # changing to "oem" from "mbcs" - the scripts run presumably | |||
# attached to a console, so some particular rules apply. | # attached to a console, so some particular rules apply. | |||
# Unfortunately, "oem" not defined in Python 3.5, so get another way | # Unfortunately, "oem" not defined in Python 3.5, so get another way | |||
if sys.version_info.major == 3 and sys.version_info.minor < 6: | if sys.version_info.major == 3 and sys.version_info.minor < 6: | |||
from ctypes import windll | from ctypes import windll | |||
OEM = "cp{}".format(windll.kernel32.GetConsoleOutputCP()) | OEM = "cp{}".format(windll.kernel32.GetConsoleOutputCP()) | |||
else: | else: | |||
skipping to change at line 313 | skipping to change at line 355 | |||
) | ) | |||
def parse_output(output, keep=KEEPLIST): | def parse_output(output, keep=KEEPLIST): | |||
""" | """ | |||
Parse output from running visual c++/studios vcvarsall.bat and running set | Parse output from running visual c++/studios vcvarsall.bat and running set | |||
To capture the values listed in keep | To capture the values listed in keep | |||
""" | """ | |||
# dkeep is a dict associating key: path_list, where key is one item from | # dkeep is a dict associating key: path_list, where key is one item from | |||
# keep, and path_list the associated list of paths | # keep, and path_list the associated list of paths | |||
dkeep = dict([(i, []) for i in keep]) | dkeep = {i: [] for i in keep} | |||
# rdk will keep the regex to match the .bat file output line starts | # rdk will keep the regex to match the .bat file output line starts | |||
rdk = {} | rdk = {} | |||
for i in keep: | for i in keep: | |||
rdk[i] = re.compile('%s=(.*)' % i, re.I) | rdk[i] = re.compile('%s=(.*)' % i, re.I) | |||
def add_env(rmatch, key, dkeep=dkeep): | def add_env(rmatch, key, dkeep=dkeep): | |||
path_list = rmatch.group(1).split(os.pathsep) | path_list = rmatch.group(1).split(os.pathsep) | |||
for path in path_list: | for path in path_list: | |||
# Do not add empty paths (when a var ends with ;) | # Do not add empty paths (when a var ends with ;) | |||
End of changes. 25 change blocks. | ||||
38 lines changed or deleted | 85 lines changed or added |