Upgrade SCons to v4.8.0 [skip ci]

This commit is contained in:
Artem Pavlenko 2024-08-21 13:52:56 +01:00
parent 7a1663f0a2
commit dc97085b31
1539 changed files with 3884 additions and 1577 deletions

View file

@ -32,15 +32,15 @@ The files are split into directories named by the first few
digits of the signature. The prefix length used for directory digits of the signature. The prefix length used for directory
names can be changed by this script. names can be changed by this script.
""" """
__revision__ = "scripts/scons-configure-cache.py 265be6883fadbb5a545612265acc919595158366 Sun, 17 Mar 2024 17:33:54 -0700 bdbaddog" __revision__ = "scripts/scons-configure-cache.py 7c688f694c644b61342670ce92977bf4a396c0d4 Sun, 07 Jul 2024 16:52:07 -0700 bdbaddog"
__version__ = "4.7.0" __version__ = "4.8.0"
__build__ = "265be6883fadbb5a545612265acc919595158366" __build__ = "7c688f694c644b61342670ce92977bf4a396c0d4"
__buildsys__ = "M1Dog2021" __buildsys__ = "M1Dog2021"
__date__ = "Sun, 17 Mar 2024 17:33:54 -0700" __date__ = "Sun, 07 Jul 2024 16:52:07 -0700"
__developer__ = "bdbaddog" __developer__ = "bdbaddog"
@ -51,7 +51,7 @@ import sys
# python compatibility check # python compatibility check
if sys.version_info < (3, 6, 0): if sys.version_info < (3, 6, 0):
msg = "scons: *** SCons version %s does not run under Python version %s.\n\ msg = "scons: *** SCons version %s does not run under Python version %s.\n\
Python >= 3.5 is required.\n" Python >= 3.6.0 is required.\n"
sys.stderr.write(msg % (__version__, sys.version.split()[0])) sys.stderr.write(msg % (__version__, sys.version.split()[0]))
sys.exit(1) sys.exit(1)

View file

@ -1,110 +0,0 @@
# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Variable type for enumeration Variables.
Enumeration variables allow selection of one from a specified set of values.
Usage example::
opts = Variables()
opts.Add(
EnumVariable(
'debug',
help='debug output and symbols',
default='no',
allowed_values=('yes', 'no', 'full'),
map={},
ignorecase=2,
)
)
...
if env['debug'] == 'full':
...
"""
from typing import Tuple, Callable
import SCons.Errors
__all__ = ['EnumVariable',]
def _validator(key, val, env, vals) -> None:
if val not in vals:
raise SCons.Errors.UserError(
'Invalid value for option %s: %s. Valid values are: %s' % (key, val, vals))
def EnumVariable(key, help, default, allowed_values, map={}, ignorecase: int=0) -> Tuple[str, str, str, Callable, Callable]:
"""Return a tuple describing an enumaration SCons Variable.
The input parameters describe an option with only certain values
allowed. Returns A tuple including an appropriate converter and
validator. The result is usable as input to :meth:`Add`.
*key* and *default* are passed directly on to :meth:`Add`.
*help* is the descriptive part of the help text,
and will have the allowed values automatically appended.
*allowed_values* is a list of strings, which are the allowed values
for this option.
The *map*-dictionary may be used for converting the input value
into canonical values (e.g. for aliases).
The value of *ignorecase* defines the behaviour of the validator:
* 0: the validator/converter are case-sensitive.
* 1: the validator/converter are case-insensitive.
* 2: the validator/converter is case-insensitive and the
converted value will always be lower-case.
The *validator* tests whether the value is in the list of allowed values.
The *converter* converts input values according to the given
*map*-dictionary (unmapped input values are returned unchanged).
"""
help = '%s (%s)' % (help, '|'.join(allowed_values))
# define validator
if ignorecase:
validator = lambda key, val, env: \
_validator(key, val.lower(), env, allowed_values)
else:
validator = lambda key, val, env: \
_validator(key, val, env, allowed_values)
# define converter
if ignorecase == 2:
converter = lambda val: map.get(val.lower(), val).lower()
elif ignorecase == 1:
converter = lambda val: map.get(val.lower(), val)
else:
converter = lambda val: map.get(val, val)
return (key, help, default, validator, converter)
# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:

View file

@ -1,152 +0,0 @@
# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Variable type for list Variables.
A 'list' option may either be 'all', 'none' or a list of names
separated by comma. After the option has been processed, the option
value holds either the named list elements, all list elements or no
list elements at all.
Usage example::
list_of_libs = Split('x11 gl qt ical')
opts = Variables()
opts.Add(
ListVariable(
'shared',
help='libraries to build as shared libraries',
default='all',
elems=list_of_libs,
)
)
...
for lib in list_of_libs:
if lib in env['shared']:
env.SharedObject(...)
else:
env.Object(...)
"""
# Known Bug: This should behave like a Set-Type, but does not really,
# since elements can occur twice.
import collections
from typing import Tuple, Callable
import SCons.Util
__all__ = ['ListVariable',]
class _ListVariable(collections.UserList):
def __init__(self, initlist=None, allowedElems=None) -> None:
if initlist is None:
initlist = []
if allowedElems is None:
allowedElems = []
super().__init__([_f for _f in initlist if _f])
self.allowedElems = sorted(allowedElems)
def __cmp__(self, other):
return NotImplemented
def __eq__(self, other):
return NotImplemented
def __ge__(self, other):
return NotImplemented
def __gt__(self, other):
return NotImplemented
def __le__(self, other):
return NotImplemented
def __lt__(self, other):
return NotImplemented
def __str__(self) -> str:
if not len(self):
return 'none'
self.data.sort()
if self.data == self.allowedElems:
return 'all'
else:
return ','.join(self)
def prepare_to_store(self):
return self.__str__()
def _converter(val, allowedElems, mapdict) -> _ListVariable:
""" """
if val == 'none':
val = []
elif val == 'all':
val = allowedElems
else:
val = [_f for _f in val.split(',') if _f]
val = [mapdict.get(v, v) for v in val]
notAllowed = [v for v in val if v not in allowedElems]
if notAllowed:
raise ValueError(
"Invalid value(s) for option: %s" % ','.join(notAllowed)
)
return _ListVariable(val, allowedElems)
# def _validator(key, val, env) -> None:
# """ """
# # TODO: write validator for pgk list
# pass
def ListVariable(key, help, default, names, map={}) -> Tuple[str, str, str, None, Callable]:
"""Return a tuple describing a list SCons Variable.
The input parameters describe a 'list' option. Returns
a tuple including the correct converter and validator.
The result is usable for input to :meth:`Add`.
*help* will have text appended indicating the legal values
(not including any extra names from *map*).
*map* can be used to map alternative names to the ones in *names* -
that is, a form of alias.
A 'list' option may either be 'all', 'none' or a list of
names (separated by commas).
"""
names_str = 'allowed names: %s' % ' '.join(names)
if SCons.Util.is_List(default):
default = ','.join(default)
help = '\n '.join(
(help, '(all|none|comma-separated list of names)', names_str))
return (key, help, default, None, lambda val: _converter(val, names, map))
# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:

View file

@ -1,336 +0,0 @@
# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""Adds user-friendly customizable variables to an SCons build. """
import os.path
import sys
from functools import cmp_to_key
import SCons.Environment
import SCons.Errors
import SCons.Util
import SCons.Warnings
from .BoolVariable import BoolVariable # okay
from .EnumVariable import EnumVariable # okay
from .ListVariable import ListVariable # naja
from .PackageVariable import PackageVariable # naja
from .PathVariable import PathVariable # okay
class Variables:
"""
Holds all the options, updates the environment with the variables,
and renders the help text.
If *is_global* is true, this is a singleton, create only once.
Args:
files (optional): List of option configuration files to load
(backward compatibility). If a single string is passed it is
automatically placed in a file list (Default value = None)
args (optional): dictionary to override values set from *files*.
(Default value = None)
is_global (optional): global instance? (Default value = True)
"""
instance = None
def __init__(self, files=None, args=None, is_global: bool=True) -> None:
if args is None:
args = {}
self.options = []
self.args = args
if not SCons.Util.is_List(files):
if files:
files = [files,]
else:
files = []
self.files = files
self.unknown = {}
# create the singleton instance
if is_global:
self = Variables.instance
if not Variables.instance:
Variables.instance=self
def _do_add(self, key, help: str="", default=None, validator=None, converter=None, **kwargs) -> None:
class Variable:
pass
option = Variable()
# If we get a list or a tuple, we take the first element as the
# option key and store the remaining in aliases.
if SCons.Util.is_List(key) or SCons.Util.is_Tuple(key):
option.key = key[0]
option.aliases = list(key[1:])
else:
option.key = key
# TODO: normalize to not include key in aliases. Currently breaks tests.
option.aliases = [key,]
if not SCons.Environment.is_valid_construction_var(option.key):
raise SCons.Errors.UserError("Illegal Variables key `%s'" % str(option.key))
option.help = help
option.default = default
option.validator = validator
option.converter = converter
self.options.append(option)
# options might be added after the 'unknown' dict has been set up,
# so we remove the key and all its aliases from that dict
for alias in option.aliases + [option.key,]:
if alias in self.unknown:
del self.unknown[alias]
def keys(self) -> list:
"""Returns the keywords for the options."""
return [o.key for o in self.options]
def Add(self, key, *args, **kwargs) -> None:
r""" Adds an option.
Arguments:
key: the name of the variable, or a 5-tuple (or list).
If a tuple, and there are no additional arguments,
the tuple is unpacked into the four named kwargs from below.
If a tuple and there are additional arguments, the first word
of the tuple is taken as the key, and the remainder as aliases.
*args: optional positional arguments, corresponding to the four
named kwargs below.
Keyword Args:
help: help text for the options (Default value = "")
default: default value for option (Default value = None)
validator: function called to validate the option's value
(Default value = None)
converter: function to be called to convert the option's
value before putting it in the environment. (Default value = None)
**kwargs: arbitrary keyword arguments used by the variable itself.
"""
if SCons.Util.is_List(key) or SCons.Util.is_Tuple(key):
if not (len(args) or len(kwargs)):
return self._do_add(*key)
return self._do_add(key, *args, **kwargs)
def AddVariables(self, *optlist) -> None:
""" Adds a list of options.
Each list element is a tuple/list of arguments to be passed on
to the underlying method for adding options.
Example::
opt.AddVariables(
('debug', '', 0),
('CC', 'The C compiler'),
('VALIDATE', 'An option for testing validation', 'notset', validator, None),
)
"""
for o in optlist:
self._do_add(*o)
def Update(self, env, args=None) -> None:
""" Updates an environment with the option variables.
Args:
env: the environment to update.
args (optional): a dictionary of keys and values to update
in *env*. If omitted, uses the variables from the commandline.
"""
values = {}
# first set the defaults:
for option in self.options:
if option.default is not None:
values[option.key] = option.default
# next set the value specified in the options file
for filename in self.files:
if os.path.exists(filename):
dir = os.path.split(os.path.abspath(filename))[0]
if dir:
sys.path.insert(0, dir)
try:
values['__name__'] = filename
with open(filename) as f:
contents = f.read()
exec(contents, {}, values)
finally:
if dir:
del sys.path[0]
del values['__name__']
# set the values specified on the command line
if args is None:
args = self.args
for arg, value in args.items():
added = False
for option in self.options:
if arg in option.aliases + [option.key,]:
values[option.key] = value
added = True
if not added:
self.unknown[arg] = value
# put the variables in the environment:
# (don't copy over variables that are not declared as options)
for option in self.options:
try:
env[option.key] = values[option.key]
except KeyError:
pass
# apply converters
for option in self.options:
if option.converter and option.key in values:
value = env.subst('${%s}'%option.key)
try:
try:
env[option.key] = option.converter(value)
except TypeError:
env[option.key] = option.converter(value, env)
except ValueError as x:
raise SCons.Errors.UserError('Error converting option: %s\n%s'%(option.key, x))
# apply validators
for option in self.options:
if option.validator and option.key in values:
option.validator(option.key, env.subst('${%s}'%option.key), env)
def UnknownVariables(self) -> dict:
""" Returns unknown variables.
Identifies options that were not known, declared options in this object.
"""
return self.unknown
def Save(self, filename, env) -> None:
""" Save the options to a file.
Saves all the options which have non-default settings
to the given file as Python expressions. This file can
then be used to load the options for a subsequent run.
This can be used to create an option cache file.
Args:
filename: Name of the file to save into
env: the environment get the option values from
"""
# Create the file and write out the header
try:
with open(filename, 'w') as fh:
# Make an assignment in the file for each option
# within the environment that was assigned a value
# other than the default. We don't want to save the
# ones set to default: in case the SConscript settings
# change you would then pick up old defaults.
for option in self.options:
try:
value = env[option.key]
try:
prepare = value.prepare_to_store
except AttributeError:
try:
eval(repr(value))
except KeyboardInterrupt:
raise
except:
# Convert stuff that has a repr() that
# cannot be evaluated into a string
value = SCons.Util.to_String(value)
else:
value = prepare()
defaultVal = env.subst(SCons.Util.to_String(option.default))
if option.converter:
try:
defaultVal = option.converter(defaultVal)
except TypeError:
defaultVal = option.converter(defaultVal, env)
if str(env.subst('${%s}' % option.key)) != str(defaultVal):
fh.write('%s = %s\n' % (option.key, repr(value)))
except KeyError:
pass
except OSError as x:
raise SCons.Errors.UserError('Error writing options to file: %s\n%s' % (filename, x))
def GenerateHelpText(self, env, sort=None) -> str:
""" Generates the help text for the options.
Args:
env: an environment that is used to get the current values
of the options.
sort: Either a comparison function used for sorting
(must take two arguments and return -1, 0 or 1)
or a boolean to indicate if it should be sorted.
"""
if callable(sort):
options = sorted(self.options, key=cmp_to_key(lambda x, y: sort(x.key, y.key)))
elif sort is True:
options = sorted(self.options, key=lambda x: x.key)
else:
options = self.options
def format_opt(opt, self=self, env=env) -> str:
if opt.key in env:
actual = env.subst('${%s}' % opt.key)
else:
actual = None
return self.FormatVariableHelpText(env, opt.key, opt.help, opt.default, actual, opt.aliases)
lines = [_f for _f in map(format_opt, options) if _f]
return ''.join(lines)
fmt = '\n%s: %s\n default: %s\n actual: %s\n'
aliasfmt = '\n%s: %s\n default: %s\n actual: %s\n aliases: %s\n'
def FormatVariableHelpText(self, env, key, help, default, actual, aliases=None) -> str:
if aliases is None:
aliases = []
# Don't display the key name itself as an alias.
aliases = [a for a in aliases if a != key]
if aliases:
return self.aliasfmt % (key, help, default, actual, aliases)
else:
return self.fmt % (key, help, default, actual)
# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4:

View file

@ -892,10 +892,10 @@ def scons_subproc_run(scons_env, *args, **kwargs) -> subprocess.CompletedProcess
kwargs['check'] = check kwargs['check'] = check
# TODO: Python version-compat stuff: remap/remove too-new args if needed # TODO: Python version-compat stuff: remap/remove too-new args if needed
if 'text' in kwargs and sys.version_info[:3] < (3, 7): if 'text' in kwargs and sys.version_info < (3, 7):
kwargs['universal_newlines'] = kwargs.pop('text') kwargs['universal_newlines'] = kwargs.pop('text')
if 'capture_output' in kwargs and sys.version_info[:3] < (3, 7): if 'capture_output' in kwargs and sys.version_info < (3, 7):
capture_output = kwargs.pop('capture_output') capture_output = kwargs.pop('capture_output')
if capture_output: if capture_output:
kwargs['stdout'] = kwargs['stderr'] = PIPE kwargs['stdout'] = kwargs['stderr'] = PIPE
@ -1454,16 +1454,6 @@ class FunctionAction(_ActionAction):
except TypeError: except TypeError:
result.command=self.strfunction(target, source, env) result.command=self.strfunction(target, source, env)
# FIXME: This maintains backward compatibility with respect to
# which type of exceptions were returned by raising an
# exception and which ones were returned by value. It would
# probably be best to always return them by value here, but
# some codes do not check the return value of Actions and I do
# not have the time to modify them at this point.
if (exc_info[1] and
not isinstance(exc_info[1], EnvironmentError)):
raise result
return result return result
finally: finally:
# Break the cycle between the traceback object and this # Break the cycle between the traceback object and this

View file

@ -387,7 +387,7 @@ class BuilderBase:
target_scanner = None, target_scanner = None,
source_scanner = None, source_scanner = None,
emitter = None, emitter = None,
multi: int = 0, multi: bool = False,
env = None, env = None,
single_source: bool = False, single_source: bool = False,
name = None, name = None,

View file

@ -23,10 +23,10 @@
"""Code for debugging SCons internal things. """Code for debugging SCons internal things.
Shouldn't be needed by most users. Quick shortcuts: Shouldn't be needed by most users. Quick shortcuts::
from SCons.Debug import caller_trace from SCons.Debug import caller_trace
caller_trace() caller_trace()
""" """
import atexit import atexit

View file

@ -291,7 +291,15 @@ def copy_func(dest, src, symlinks: bool=True) -> int:
elif os.path.islink(src): elif os.path.islink(src):
if symlinks: if symlinks:
os.symlink(os.readlink(src), dest) try:
os.symlink(os.readlink(src), dest)
except FileExistsError:
raise SCons.Errors.BuildError(
errstr=(
f'Error: Copy() called to create symlink at "{dest}",'
' but a file already exists at that location.'
)
)
return 0 return 0
return copy_func(dest, os.path.realpath(src)) return copy_func(dest, os.path.realpath(src))

View file

@ -35,9 +35,9 @@ import os
import sys import sys
import re import re
import shlex import shlex
from collections import UserDict, deque from collections import UserDict, UserList, deque
from subprocess import PIPE, DEVNULL from subprocess import PIPE, DEVNULL
from typing import Optional from typing import Callable, Collection, Optional, Sequence, Union
import SCons.Action import SCons.Action
import SCons.Builder import SCons.Builder
@ -510,13 +510,6 @@ class BuilderDict(UserDict):
self.__setitem__(i, v) self.__setitem__(i, v)
_is_valid_var = re.compile(r'[_a-zA-Z]\w*$')
def is_valid_construction_var(varstr) -> bool:
"""Return True if *varstr* is a legitimate construction variable."""
return _is_valid_var.match(varstr)
class SubstitutionEnvironment: class SubstitutionEnvironment:
"""Base class for different flavors of construction environments. """Base class for different flavors of construction environments.
@ -587,26 +580,20 @@ class SubstitutionEnvironment:
return self._dict[key] return self._dict[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
# This is heavily used. This implementation is the best we have
# according to the timings in bench/env.__setitem__.py.
#
# The "key in self._special_set_keys" test here seems to perform
# pretty well for the number of keys we have. A hard-coded
# list worked a little better in Python 2.5, but that has the
# disadvantage of maybe getting out of sync if we ever add more
# variable names.
# So right now it seems like a good trade-off, but feel free to
# revisit this with bench/env.__setitem__.py as needed (and
# as newer versions of Python come out).
if key in self._special_set_keys: if key in self._special_set_keys:
self._special_set[key](self, key, value) self._special_set[key](self, key, value)
else: else:
# If we already have the entry, then it's obviously a valid # Performance: since this is heavily used, try to avoid checking
# key and we don't need to check. If we do check, using a # if the variable is valid unless necessary. bench/__setitem__.py
# global, pre-compiled regular expression directly is more # times a bunch of different approaches. Based the most recent
# efficient than calling another function or a method. # run, against Python 3.6-3.13(beta), the best we can do across
if key not in self._dict and not _is_valid_var.match(key): # different combinations of actions is to use a membership test
raise UserError("Illegal construction variable `%s'" % key) # to see if we already have the variable, if so it must already
# have been checked, so skip; if we do check, "isidentifier()"
# (new in Python 3 so wasn't in benchmark until recently)
# on the key is the best.
if key not in self._dict and not key.isidentifier():
raise UserError(f"Illegal construction variable {key!r}")
self._dict[key] = value self._dict[key] = value
def get(self, key, default=None): def get(self, key, default=None):
@ -881,11 +868,11 @@ class SubstitutionEnvironment:
'RPATH' : [], 'RPATH' : [],
} }
def do_parse(arg) -> None: def do_parse(arg: Union[str, Sequence]) -> None:
# if arg is a sequence, recurse with each element
if not arg: if not arg:
return return
# if arg is a sequence, recurse with each element
if not is_String(arg): if not is_String(arg):
for t in arg: do_parse(t) for t in arg: do_parse(t)
return return
@ -902,7 +889,7 @@ class SubstitutionEnvironment:
else: else:
mapping['CPPDEFINES'].append([t[0], '='.join(t[1:])]) mapping['CPPDEFINES'].append([t[0], '='.join(t[1:])])
# Loop through the flags and add them to the appropriate option. # Loop through the flags and add them to the appropriate variable.
# This tries to strike a balance between checking for all possible # This tries to strike a balance between checking for all possible
# flags and keeping the logic to a finite size, so it doesn't # flags and keeping the logic to a finite size, so it doesn't
# check for some that don't occur often. It particular, if the # check for some that don't occur often. It particular, if the
@ -926,6 +913,8 @@ class SubstitutionEnvironment:
append_next_arg_to = None # for multi-word args append_next_arg_to = None # for multi-word args
for arg in params: for arg in params:
if append_next_arg_to: if append_next_arg_to:
# these are the second pass for options where the
# option-argument follows as a second word.
if append_next_arg_to == 'CPPDEFINES': if append_next_arg_to == 'CPPDEFINES':
append_define(arg) append_define(arg)
elif append_next_arg_to == '-include': elif append_next_arg_to == '-include':
@ -1022,6 +1011,8 @@ class SubstitutionEnvironment:
else: else:
key = 'CFLAGS' key = 'CFLAGS'
mapping[key].append(arg) mapping[key].append(arg)
elif arg.startswith('-stdlib='):
mapping['CXXFLAGS'].append(arg)
elif arg[0] == '+': elif arg[0] == '+':
mapping['CCFLAGS'].append(arg) mapping['CCFLAGS'].append(arg)
mapping['LINKFLAGS'].append(arg) mapping['LINKFLAGS'].append(arg)
@ -1250,9 +1241,11 @@ class Base(SubstitutionEnvironment):
SCons.Tool.Initializers(self) SCons.Tool.Initializers(self)
if tools is None: if tools is None:
tools = self._dict.get('TOOLS', None) tools = self._dict.get('TOOLS', ['default'])
if tools is None: else:
tools = ['default'] # for a new env, if we didn't use TOOLS, make sure it starts empty
# so it only shows tools actually initialized.
self._dict['TOOLS'] = []
apply_tools(self, tools, toolpath) apply_tools(self, tools, toolpath)
# Now restore the passed-in and customized variables # Now restore the passed-in and customized variables
@ -1562,16 +1555,28 @@ class Base(SubstitutionEnvironment):
self._dict[key] = dk + val self._dict[key] = dk + val
self.scanner_map_delete(kw) self.scanner_map_delete(kw)
def Clone(self, tools=[], toolpath=None, parse_flags = None, **kw): def Clone(self, tools=[], toolpath=None, variables=None, parse_flags=None, **kw):
"""Return a copy of a construction Environment. """Return a copy of a construction Environment.
The copy is like a Python "deep copy"--that is, independent The copy is like a Python "deep copy": independent copies are made
copies are made recursively of each objects--except that recursively of each object, except that a reference is copied when
a reference is copied when an object is not deep-copyable an object is not deep-copyable (like a function). There are no
(like a function). There are no references to any mutable references to any mutable objects in the original environment.
objects in the original Environment.
"""
Unrecognized keyword arguments are taken as construction variable
assignments.
Arguments:
tools: list of tools to initialize.
toolpath: list of paths to search for tools.
variables: a :class:`~SCons.Variables.Variables` object to
use to populate construction variables from command-line
variables.
parse_flags: option strings to parse into construction variables.
.. versionadded:: 4.8.0
The optional *variables* parameter was added.
"""
builders = self._dict.get('BUILDERS', {}) builders = self._dict.get('BUILDERS', {})
clone = copy.copy(self) clone = copy.copy(self)
@ -1597,6 +1602,8 @@ class Base(SubstitutionEnvironment):
for key, value in kw.items(): for key, value in kw.items():
new[key] = SCons.Subst.scons_subst_once(value, self, key) new[key] = SCons.Subst.scons_subst_once(value, self, key)
clone.Replace(**new) clone.Replace(**new)
if variables:
variables.Update(clone)
apply_tools(clone, tools, toolpath) apply_tools(clone, tools, toolpath)
@ -1687,17 +1694,23 @@ class Base(SubstitutionEnvironment):
return dlist return dlist
def Dump(self, key=None, format: str='pretty'): def Dump(self, key: Optional[str] = None, format: str = 'pretty') -> str:
""" Return construction variables serialized to a string. """ Returns a dump of serialized construction variables.
The display formats are intended for humaan readers when
debugging - none of the supported formats produce a result that
SCons itself can directly make use of. Objects that cannot
directly be represented get a placeholder like
``<function foo at 0x123456>`` or ``<<non-serializable: function>>``.
Args: Args:
key (optional): if None, format the whole dict of variables. key: if ``None``, format the whole dict of variables,
Else format the value of `key` (Default value = None) else format just the value of *key*.
format (str, optional): specify the format to serialize to. format: specify the format to serialize to. ``"pretty"`` generates
`"pretty"` generates a pretty-printed string, a pretty-printed string, ``"json"`` a JSON-formatted string.
`"json"` a JSON-formatted string.
(Default value = `"pretty"`)
Raises:
ValueError: *format* is not a recognized serialization format.
""" """
if key: if key:
cvars = self.Dictionary(key) cvars = self.Dictionary(key)
@ -1707,9 +1720,9 @@ class Base(SubstitutionEnvironment):
fmt = format.lower() fmt = format.lower()
if fmt == 'pretty': if fmt == 'pretty':
import pprint import pprint # pylint: disable=import-outside-toplevel
pp = pprint.PrettyPrinter(indent=2)
pp = pprint.PrettyPrinter(indent=2)
# TODO: pprint doesn't do a nice job on path-style values # TODO: pprint doesn't do a nice job on path-style values
# if the paths contain spaces (i.e. Windows), because the # if the paths contain spaces (i.e. Windows), because the
# algorithm tries to break lines on spaces, while breaking # algorithm tries to break lines on spaces, while breaking
@ -1718,26 +1731,33 @@ class Base(SubstitutionEnvironment):
return pp.pformat(cvars) return pp.pformat(cvars)
elif fmt == 'json': elif fmt == 'json':
import json import json # pylint: disable=import-outside-toplevel
def non_serializable(obj):
return '<<non-serializable: %s>>' % type(obj).__qualname__ class DumpEncoder(json.JSONEncoder):
return json.dumps(cvars, indent=4, default=non_serializable) """SCons special json Dump formatter."""
def default(self, obj):
if isinstance(obj, (UserList, UserDict)):
return obj.data
return f'<<non-serializable: {type(obj).__qualname__}>>'
return json.dumps(cvars, indent=4, cls=DumpEncoder, sort_keys=True)
else: else:
raise ValueError("Unsupported serialization format: %s." % fmt) raise ValueError("Unsupported serialization format: %s." % fmt)
def FindIxes(self, paths, prefix, suffix): def FindIxes(self, paths: Sequence[str], prefix: str, suffix: str) -> Optional[str]:
"""Search a list of paths for something that matches the prefix and suffix. """Search *paths* for a path that has *prefix* and *suffix*.
Args: Returns on first match.
paths: the list of paths or nodes.
prefix: construction variable for the prefix.
suffix: construction variable for the suffix.
Returns: the matched path or None Arguments:
paths: the list of paths or nodes.
prefix: construction variable for the prefix.
suffix: construction variable for the suffix.
Returns:
The matched path or ``None``
""" """
suffix = self.subst('$'+suffix) suffix = self.subst('$'+suffix)
prefix = self.subst('$'+prefix) prefix = self.subst('$'+prefix)
@ -2001,11 +2021,20 @@ class Base(SubstitutionEnvironment):
def _find_toolpath_dir(self, tp): def _find_toolpath_dir(self, tp):
return self.fs.Dir(self.subst(tp)).srcnode().get_abspath() return self.fs.Dir(self.subst(tp)).srcnode().get_abspath()
def Tool(self, tool, toolpath=None, **kwargs) -> SCons.Tool.Tool: def Tool(
self, tool: Union[str, Callable], toolpath: Optional[Collection[str]] = None, **kwargs
) -> Callable:
"""Find and run tool module *tool*. """Find and run tool module *tool*.
*tool* is generally a string, but can also be a callable object,
in which case it is just called, without any of the setup.
The skipped setup includes storing *kwargs* into the created
:class:`~SCons.Tool.Tool` instance, which is extracted and used
when the instance is called, so in the skip case, the called
object will not get the *kwargs*.
.. versionchanged:: 4.2 .. versionchanged:: 4.2
returns the tool module rather than ``None``. returns the tool object rather than ``None``.
""" """
if is_String(tool): if is_String(tool):
tool = self.subst(tool) tool = self.subst(tool)
@ -2180,52 +2209,44 @@ class Base(SubstitutionEnvironment):
return SCons.SConf.SConf(*nargs, **nkw) return SCons.SConf.SConf(*nargs, **nkw)
def Command(self, target, source, action, **kw): def Command(self, target, source, action, **kw):
"""Builds the supplied target files from the supplied """Set up a one-off build command.
source files using the supplied action. Action may
be any type that the Builder constructor will accept Builds *target* from *source* using *action*, which may be
for an action.""" be any type that the Builder factory will accept for an action.
Generates an anonymous builder and calls it, to add the details
to the build graph. The builder is not named, added to ``BUILDERS``,
or otherwise saved.
Recognizes the :func:`~SCons.Builder.Builder` keywords
``source_scanner``, ``target_scanner``, ``source_factory`` and
``target_factory``. All other arguments from *kw* are passed on
to the builder when it is called.
"""
# Build a kwarg dict for the builder construction
bkw = { bkw = {
'action': action, 'action': action,
'target_factory': self.fs.Entry, 'target_factory': self.fs.Entry,
'source_factory': self.fs.Entry, 'source_factory': self.fs.Entry,
} }
# source scanner
try:
bkw['source_scanner'] = kw['source_scanner']
except KeyError:
pass
else:
del kw['source_scanner']
# target scanner # Recognize these kwargs for the builder construction and take
try: # them out of the args for the subsequent builder call.
bkw['target_scanner'] = kw['target_scanner'] for arg in [
except KeyError: 'source_scanner',
pass 'target_scanner',
else: 'source_factory',
del kw['target_scanner'] 'target_factory',
]:
# source factory try:
try: bkw[arg] = kw.pop(arg)
bkw['source_factory'] = kw['source_factory'] except KeyError:
except KeyError: pass
pass
else:
del kw['source_factory']
# target factory
try:
bkw['target_factory'] = kw['target_factory']
except KeyError:
pass
else:
del kw['target_factory']
bld = SCons.Builder.Builder(**bkw) bld = SCons.Builder.Builder(**bkw)
return bld(self, target, source, **kw) return bld(self, target, source, **kw)
def Depends(self, target, dependency): def Depends(self, target, dependency):
"""Explicity specify that 'target's depend on 'dependency'.""" """Explicity specify that *target* depends on *dependency*."""
tlist = self.arg2nodes(target, self.fs.Entry) tlist = self.arg2nodes(target, self.fs.Entry)
dlist = self.arg2nodes(dependency, self.fs.Entry) dlist = self.arg2nodes(dependency, self.fs.Entry)
for t in tlist: for t in tlist:
@ -2253,7 +2274,7 @@ class Base(SubstitutionEnvironment):
return self.fs.PyPackageDir(s) return self.fs.PyPackageDir(s)
def NoClean(self, *targets): def NoClean(self, *targets):
"""Tags a target so that it will not be cleaned by -c""" """Tag target(s) so that it will not be cleaned by -c."""
tlist = [] tlist = []
for t in targets: for t in targets:
tlist.extend(self.arg2nodes(t, self.fs.Entry)) tlist.extend(self.arg2nodes(t, self.fs.Entry))
@ -2262,7 +2283,7 @@ class Base(SubstitutionEnvironment):
return tlist return tlist
def NoCache(self, *targets): def NoCache(self, *targets):
"""Tags a target so that it will not be cached""" """Tag target(s) so that it will not be cached."""
tlist = [] tlist = []
for t in targets: for t in targets:
tlist.extend(self.arg2nodes(t, self.fs.Entry)) tlist.extend(self.arg2nodes(t, self.fs.Entry))
@ -2551,8 +2572,12 @@ class OverrideEnvironment(Base):
return self.__dict__['__subject'].__getitem__(key) return self.__dict__['__subject'].__getitem__(key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
if not is_valid_construction_var(key): # This doesn't have the same performance equation as a "real"
raise UserError("Illegal construction variable `%s'" % key) # environment: in an override you're basically just writing
# new stuff; it's not a common case to be changing values already
# set in the override dict, so don't spend time checking for existance.
if not key.isidentifier():
raise UserError(f"Illegal construction variable {key!r}")
self.__dict__['overrides'][key] = value self.__dict__['overrides'][key] = value
def __delitem__(self, key): def __delitem__(self, key):

View file

@ -177,8 +177,12 @@ def convert_to_BuildError(status, exc_info=None):
# (for example, failure to create the directory in which the # (for example, failure to create the directory in which the
# target file will appear). # target file will appear).
filename = getattr(status, 'filename', None) filename = getattr(status, 'filename', None)
strerror = getattr(status, 'strerror', str(status)) strerror = getattr(status, 'strerror', None)
errno = getattr(status, 'errno', 2) if strerror is None:
strerror = str(status)
errno = getattr(status, 'errno', None)
if errno is None:
errno = 2
buildError = BuildError( buildError = BuildError(
errstr=strerror, errstr=strerror,

View file

@ -57,38 +57,6 @@ class AliasNodeInfo(SCons.Node.NodeInfoBase):
def str_to_node(self, s): def str_to_node(self, s):
return default_ans.Alias(s) return default_ans.Alias(s)
def __getstate__(self):
"""
Return all fields that shall be pickled. Walk the slots in the class
hierarchy and add those to the state dictionary. If a '__dict__' slot is
available, copy all entries to the dictionary. Also include the version
id, which is fixed for all instances of a class.
"""
state = getattr(self, '__dict__', {}).copy()
for obj in type(self).mro():
for name in getattr(obj,'__slots__',()):
if hasattr(self, name):
state[name] = getattr(self, name)
state['_version_id'] = self.current_version_id
try:
del state['__weakref__']
except KeyError:
pass
return state
def __setstate__(self, state) -> None:
"""
Restore the attributes from a pickled state.
"""
# TODO check or discard version
del state['_version_id']
for key, value in state.items():
if key not in ('__weakref__',):
setattr(self, key, value)
class AliasBuildInfo(SCons.Node.BuildInfoBase): class AliasBuildInfo(SCons.Node.BuildInfoBase):
__slots__ = () __slots__ = ()
current_version_id = 2 current_version_id = 2

View file

@ -120,30 +120,31 @@ def save_strings(val) -> None:
global Save_Strings global Save_Strings
Save_Strings = val Save_Strings = val
#
# Avoid unnecessary function calls by recording a Boolean value that
# tells us whether or not os.path.splitdrive() actually does anything
# on this system, and therefore whether we need to bother calling it
# when looking up path names in various methods below.
#
do_splitdrive = None do_splitdrive = None
_my_splitdrive =None _my_splitdrive = None
def initialize_do_splitdrive() -> None: def initialize_do_splitdrive() -> None:
global do_splitdrive """Set up splitdrive usage.
global has_unc
drive, path = os.path.splitdrive('X:/foo')
# splitunc is removed from python 3.7 and newer
# so we can also just test if splitdrive works with UNC
has_unc = (hasattr(os.path, 'splitunc')
or os.path.splitdrive(r'\\split\drive\test')[0] == r'\\split\drive')
do_splitdrive = not not drive or has_unc Avoid unnecessary function calls by recording a flag that tells us whether
or not :func:`os.path.splitdrive` actually does anything on this system,
and therefore whether we need to bother calling it when looking up path
names in various methods below.
global _my_splitdrive If :data:`do_splitdrive` is True, :func:`_my_splitdrive` will be a real
if has_unc: function which we can call. As all supported Python versions' ntpath module
def splitdrive(p): now handle UNC paths correctly, we no longer special-case that.
Deferring the setup of ``_my_splitdrive`` also lets unit tests do
their thing and test UNC path handling on a POSIX host.
"""
global do_splitdrive, _my_splitdrive
do_splitdrive = bool(os.path.splitdrive('X:/foo')[0])
if do_splitdrive:
def _my_splitdrive(p):
if p[1:2] == ':': if p[1:2] == ':':
return p[:2], p[2:] return p[:2], p[2:]
if p[0:2] == '//': if p[0:2] == '//':
@ -151,12 +152,9 @@ def initialize_do_splitdrive() -> None:
# because UNC paths are always absolute. # because UNC paths are always absolute.
return '//', p[1:] return '//', p[1:]
return '', p return '', p
else: # TODO: the os routine should work and be better debugged than ours,
def splitdrive(p): # but unit test test_unc_path fails on POSIX platforms. Resolve someday.
if p[1:2] == ':': # _my_splitdrive = os.path.splitdrive
return p[:2], p[2:]
return '', p
_my_splitdrive = splitdrive
# Keep some commonly used values in global variables to skip to # Keep some commonly used values in global variables to skip to
# module look-up costs. # module look-up costs.
@ -328,7 +326,11 @@ LocalCopy = SCons.Action.Action(LinkFunc, LocalString)
def UnlinkFunc(target, source, env) -> int: def UnlinkFunc(target, source, env) -> int:
t = target[0] t = target[0]
t.fs.unlink(t.get_abspath()) file = t.get_abspath()
try:
t.fs.unlink(file)
except FileNotFoundError:
pass
return 0 return 0
Unlink = SCons.Action.Action(UnlinkFunc, None) Unlink = SCons.Action.Action(UnlinkFunc, None)
@ -1238,7 +1240,10 @@ class FS(LocalFS):
self.pathTop = os.getcwd() self.pathTop = os.getcwd()
else: else:
self.pathTop = path self.pathTop = path
self.defaultDrive = _my_normcase(_my_splitdrive(self.pathTop)[0]) if do_splitdrive:
self.defaultDrive = _my_normcase(_my_splitdrive(self.pathTop)[0])
else:
self.defaultDrive = ""
self.Top = self.Dir(self.pathTop) self.Top = self.Dir(self.pathTop)
self.Top._path = '.' self.Top._path = '.'
@ -1554,11 +1559,15 @@ class DirNodeInfo(SCons.Node.NodeInfoBase):
def str_to_node(self, s): def str_to_node(self, s):
top = self.fs.Top top = self.fs.Top
root = top.root root = top.root
# Python 3.13/Win changed isabs() - after you split C:/foo/bar,
# the path part is no longer considerd absolute. Save the passed
# path for the isabs check so we can get the right answer.
path = s
if do_splitdrive: if do_splitdrive:
drive, s = _my_splitdrive(s) drive, s = _my_splitdrive(s)
if drive: if drive:
root = self.fs.get_root(drive) root = self.fs.get_root(drive)
if not os.path.isabs(s): if not os.path.isabs(path):
s = top.get_labspath() + '/' + s s = top.get_labspath() + '/' + s
return root._lookup_abs(s, Entry) return root._lookup_abs(s, Entry)
@ -2380,7 +2389,7 @@ class RootDir(Dir):
# The // entry is necessary because os.path.normpath() # The // entry is necessary because os.path.normpath()
# preserves double slashes at the beginning of a path on Posix # preserves double slashes at the beginning of a path on Posix
# platforms. # platforms.
if not has_unc: if not do_splitdrive:
self._lookupDict['//'] = self self._lookupDict['//'] = self
def _morph(self) -> None: def _morph(self) -> None:
@ -2511,45 +2520,18 @@ class FileNodeInfo(SCons.Node.NodeInfoBase):
def str_to_node(self, s): def str_to_node(self, s):
top = self.fs.Top top = self.fs.Top
root = top.root root = top.root
# Python 3.13/Win changed isabs() - after you split C:/foo/bar,
# the path part is no longer considerd absolute. Save the passed
# path for the isabs check so we can get the right answer.
path = s
if do_splitdrive: if do_splitdrive:
drive, s = _my_splitdrive(s) drive, s = _my_splitdrive(s)
if drive: if drive:
root = self.fs.get_root(drive) root = self.fs.get_root(drive)
if not os.path.isabs(s): if not os.path.isabs(path):
s = top.get_labspath() + '/' + s s = top.get_labspath() + '/' + s
return root._lookup_abs(s, Entry) return root._lookup_abs(s, Entry)
def __getstate__(self):
"""
Return all fields that shall be pickled. Walk the slots in the class
hierarchy and add those to the state dictionary. If a '__dict__' slot is
available, copy all entries to the dictionary. Also include the version
id, which is fixed for all instances of a class.
"""
state = getattr(self, '__dict__', {}).copy()
for obj in type(self).mro():
for name in getattr(obj, '__slots__', ()):
if hasattr(self, name):
state[name] = getattr(self, name)
state['_version_id'] = self.current_version_id
try:
del state['__weakref__']
except KeyError:
pass
return state
def __setstate__(self, state) -> None:
"""
Restore the attributes from a pickled state.
"""
# TODO check or discard version
del state['_version_id']
for key, value in state.items():
if key not in ('__weakref__',):
setattr(self, key, value)
def __eq__(self, other): def __eq__(self, other):
return self.csig == other.csig and self.timestamp == other.timestamp and self.size == other.size return self.csig == other.csig and self.timestamp == other.timestamp and self.size == other.size
@ -2986,7 +2968,7 @@ class File(Base):
# created. # created.
self.dir._create() self.dir._create()
def push_to_cache(self) -> None: def push_to_cache(self) -> bool:
"""Try to push the node into a cache """Try to push the node into a cache
""" """
# This should get called before the Nodes' .built() method is # This should get called before the Nodes' .built() method is
@ -2997,10 +2979,10 @@ class File(Base):
# the node to cache so that the memoization of the self.exists() # the node to cache so that the memoization of the self.exists()
# return value doesn't interfere. # return value doesn't interfere.
if self.nocache: if self.nocache:
return return None
self.clear_memoized_values() self.clear_memoized_values()
if self.exists(): if self.exists():
self.get_build_env().get_CacheDir().push(self) return self.get_build_env().get_CacheDir().push(self)
def retrieve_from_cache(self) -> bool: def retrieve_from_cache(self) -> bool:
"""Try to retrieve the node's content from a cache """Try to retrieve the node's content from a cache
@ -3176,12 +3158,16 @@ class File(Base):
return None return None
def do_duplicate(self, src): def do_duplicate(self, src):
"""Create a duplicate of this file from the specified source."""
self._createDir() self._createDir()
if SCons.Node.print_duplicate: if SCons.Node.print_duplicate:
print(f"dup: relinking variant '{self}' from '{src}'") print(f"dup: relinking variant '{self}' from '{src}'")
Unlink(self, None, None) Unlink(self, None, None)
e = Link(self, src, None) try:
if isinstance(e, SCons.Errors.BuildError): e = Link(self, src, None)
if isinstance(e, SCons.Errors.BuildError):
raise e
except SCons.Errors.BuildError as e:
raise SCons.Errors.StopError(f"Cannot duplicate `{src.get_internal_path()}' in `{self.dir._path}': {e.errstr}.") raise SCons.Errors.StopError(f"Cannot duplicate `{src.get_internal_path()}' in `{self.dir._path}': {e.errstr}.")
self.linked = 1 self.linked = 1
# The Link() action may or may not have actually # The Link() action may or may not have actually
@ -3534,7 +3520,7 @@ class File(Base):
In all cases self is the target we're checking to see if it's up to date In all cases self is the target we're checking to see if it's up to date
""" """
T = 0 T = False
if T: Trace('is_up_to_date(%s):' % self) if T: Trace('is_up_to_date(%s):' % self)
if not self.exists(): if not self.exists():
if T: Trace(' not self.exists():') if T: Trace(' not self.exists():')
@ -3730,7 +3716,10 @@ class FileFinder:
if fd is None: if fd is None:
fd = self.default_filedir fd = self.default_filedir
dir, name = os.path.split(fd) dir, name = os.path.split(fd)
drive, d = _my_splitdrive(dir) if do_splitdrive:
drive, d = _my_splitdrive(dir)
else:
drive, d = "", dir
if not name and d[:1] in ('/', OS_SEP): if not name and d[:1] in ('/', OS_SEP):
#return p.fs.get_root(drive).dir_on_disk(name) #return p.fs.get_root(drive).dir_on_disk(name)
return p.fs.get_root(drive) return p.fs.get_root(drive)

View file

@ -37,37 +37,6 @@ class ValueNodeInfo(SCons.Node.NodeInfoBase):
def str_to_node(self, s): def str_to_node(self, s):
return ValueWithMemo(s) return ValueWithMemo(s)
def __getstate__(self):
"""
Return all fields that shall be pickled. Walk the slots in the class
hierarchy and add those to the state dictionary. If a '__dict__' slot
is available, copy all entries to the dictionary. Also include the
version id, which is fixed for all instances of a class.
"""
state = getattr(self, '__dict__', {}).copy()
for obj in type(self).mro():
for name in getattr(obj, '__slots__', ()):
if hasattr(self, name):
state[name] = getattr(self, name)
state['_version_id'] = self.current_version_id
try:
del state['__weakref__']
except KeyError:
pass
return state
def __setstate__(self, state) -> None:
"""
Restore the attributes from a pickled state.
"""
# TODO check or discard version
del state['_version_id']
for key, value in state.items():
if key not in ('__weakref__',):
setattr(self, key, value)
class ValueBuildInfo(SCons.Node.BuildInfoBase): class ValueBuildInfo(SCons.Node.BuildInfoBase):
__slots__ = () __slots__ = ()

View file

@ -678,7 +678,7 @@ class Node(metaclass=NoSlotsPyPy):
except AttributeError: except AttributeError:
pass pass
def push_to_cache(self) -> None: def push_to_cache(self) -> bool:
"""Try to push a node into a cache """Try to push a node into a cache
""" """
pass pass
@ -1063,6 +1063,7 @@ class Node(metaclass=NoSlotsPyPy):
# Don't bother scanning non-derived files, because we don't # Don't bother scanning non-derived files, because we don't
# care what their dependencies are. # care what their dependencies are.
# Don't scan again, if we already have scanned. # Don't scan again, if we already have scanned.
T = False
if self.implicit is not None: if self.implicit is not None:
return return
self.implicit = [] self.implicit = []
@ -1087,7 +1088,12 @@ class Node(metaclass=NoSlotsPyPy):
# essentially short-circuits an N*M scan of the # essentially short-circuits an N*M scan of the
# sources for each individual target, which is a hell # sources for each individual target, which is a hell
# of a lot more efficient. # of a lot more efficient.
def print_nodelist(n):
tgts = [f"{t.path!r}" for t in n]
return f"[{', '.join(tgts)}]"
for tgt in executor.get_all_targets(): for tgt in executor.get_all_targets():
if T: Trace(f"adding implicit {print_nodelist(implicit)} to {tgt!s}\n")
tgt.add_to_implicit(implicit) tgt.add_to_implicit(implicit)
if implicit_deps_unchanged or self.is_up_to_date(): if implicit_deps_unchanged or self.is_up_to_date():
@ -1472,8 +1478,8 @@ class Node(metaclass=NoSlotsPyPy):
@see: FS.File.changed(), FS.File.release_target_info() @see: FS.File.changed(), FS.File.release_target_info()
""" """
t = 0 T = False
if t: Trace('changed(%s [%s], %s)' % (self, classname(self), node)) if T: Trace('changed(%s [%s], %s)' % (self, classname(self), node))
if node is None: if node is None:
node = self node = self
@ -1491,25 +1497,24 @@ class Node(metaclass=NoSlotsPyPy):
# entries to equal the new dependency list, for the benefit # entries to equal the new dependency list, for the benefit
# of the loop below that updates node information. # of the loop below that updates node information.
then.extend([None] * diff) then.extend([None] * diff)
if t: Trace(': old %s new %s' % (len(then), len(children))) if T: Trace(': old %s new %s' % (len(then), len(children)))
result = True result = True
for child, prev_ni in zip(children, then): for child, prev_ni in zip(children, then):
if _decider_map[child.changed_since_last_build](child, self, prev_ni, node): if _decider_map[child.changed_since_last_build](child, self, prev_ni, node):
if t: Trace(': %s changed' % child) if T: Trace(f": '{child!s}' changed")
result = True result = True
if self.has_builder(): if self.has_builder():
contents = self.get_executor().get_contents() contents = self.get_executor().get_contents()
newsig = hash_signature(contents) newsig = hash_signature(contents)
if bi.bactsig != newsig: if bi.bactsig != newsig:
if t: Trace(': bactsig %s != newsig %s' % (bi.bactsig, newsig)) if T: Trace(': bactsig %s != newsig %s' % (bi.bactsig, newsig))
result = True result = True
if not result: if not result:
if t: Trace(': up to date') if T: Trace(': up to date')
if T: Trace('\n')
if t: Trace('\n')
return result return result

View file

@ -23,7 +23,7 @@
"""Handle lists of directory paths. """Handle lists of directory paths.
These are the path lists that get set as CPPPATH, LIBPATH, These are the path lists that get set as ``CPPPATH``, ``LIBPATH``,
etc.) with as much caching of data and efficiency as we can, while etc.) with as much caching of data and efficiency as we can, while
still keeping the evaluation delayed so that we Do the Right Thing still keeping the evaluation delayed so that we Do the Right Thing
(almost) regardless of how the variable is specified. (almost) regardless of how the variable is specified.
@ -47,10 +47,10 @@ def node_conv(obj):
""" """
This is the "string conversion" routine that we have our substitutions This is the "string conversion" routine that we have our substitutions
use to return Nodes, not strings. This relies on the fact that an use to return Nodes, not strings. This relies on the fact that an
EntryProxy object has a get() method that returns the underlying :class:`~SCons.Node.FS.EntryProxy` object has a ``get()`` method that
Node that it wraps, which is a bit of architectural dependence returns the underlying Node that it wraps, which is a bit of
that we might need to break or modify in the future in response to architectural dependence that we might need to break or modify in the
additional requirements. future in response to additional requirements.
""" """
try: try:
get = obj.get get = obj.get
@ -64,34 +64,35 @@ def node_conv(obj):
return result return result
class _PathList: class _PathList:
"""An actual PathList object.""" """An actual PathList object.
Initializes a :class:`PathList` object, canonicalizing the input and
pre-processing it for quicker substitution later.
The stored representation of the :class:`PathList` is a list of tuples
containing (type, value), where the "type" is one of the ``TYPE_*``
variables defined above. We distinguish between:
* Strings that contain no ``$`` and therefore need no
delayed-evaluation string substitution (we expect that there
will be many of these and that we therefore get a pretty
big win from avoiding string substitution)
* Strings that contain ``$`` and therefore need substitution
(the hard case is things like ``${TARGET.dir}/include``,
which require re-evaluation for every target + source)
* Other objects (which may be something like an
:class:`~SCons.Node.FS.EntryProxy`
that needs a method called to return a Node)
Pre-identifying the type of each element in the :class:`PathList`
up-front and storing the type in the list of tuples is intended to
reduce the amount of calculation when we actually do the substitution
over and over for each target.
"""
def __init__(self, pathlist, split=True) -> None: def __init__(self, pathlist, split=True) -> None:
"""
Initializes a PathList object, canonicalizing the input and
pre-processing it for quicker substitution later.
The stored representation of the PathList is a list of tuples
containing (type, value), where the "type" is one of the TYPE_*
variables defined above. We distinguish between:
strings that contain no '$' and therefore need no
delayed-evaluation string substitution (we expect that there
will be many of these and that we therefore get a pretty
big win from avoiding string substitution)
strings that contain '$' and therefore need substitution
(the hard case is things like '${TARGET.dir}/include',
which require re-evaluation for every target + source)
other objects (which may be something like an EntryProxy
that needs a method called to return a Node)
Pre-identifying the type of each element in the PathList up-front
and storing the type in the list of tuples is intended to reduce
the amount of calculation when we actually do the substitution
over and over for each target.
"""
if SCons.Util.is_String(pathlist): if SCons.Util.is_String(pathlist):
if split: if split:
pathlist = pathlist.split(os.pathsep) pathlist = pathlist.split(os.pathsep)
@ -152,34 +153,33 @@ class PathListCache:
use the same Memoizer pattern that we use elsewhere to count cache use the same Memoizer pattern that we use elsewhere to count cache
hits and misses, which is very valuable. hits and misses, which is very valuable.
Lookup keys in the cache are computed by the _PathList_key() method. Lookup keys in the cache are computed by the :meth:`_PathList_key` method.
Cache lookup should be quick, so we don't spend cycles canonicalizing Cache lookup should be quick, so we don't spend cycles canonicalizing
all forms of the same lookup key. For example, 'x:y' and ['x', all forms of the same lookup key. For example, ``x:y`` and ``['x', 'y']``
'y'] logically represent the same list, but we don't bother to logically represent the same list, but we don't bother to
split string representations and treat those two equivalently. split string representations and treat those two equivalently.
(Note, however, that we do, treat lists and tuples the same.) (Note, however, that we do, treat lists and tuples the same.)
The main type of duplication we're trying to catch will come from The main type of duplication we're trying to catch will come from
looking up the same path list from two different clones of the looking up the same path list from two different clones of the
same construction environment. That is, given same construction environment. That is, given::
env2 = env1.Clone() env2 = env1.Clone()
both env1 and env2 will have the same CPPPATH value, and we can both ``env1`` and ``env2`` will have the same ``CPPPATH`` value, and we can
cheaply avoid re-parsing both values of CPPPATH by using the cheaply avoid re-parsing both values of ``CPPPATH`` by using the
common value from this cache. common value from this cache.
""" """
def __init__(self) -> None: def __init__(self) -> None:
self._memo = {} self._memo = {}
def _PathList_key(self, pathlist): def _PathList_key(self, pathlist):
""" """Returns the key for memoization of PathLists.
Returns the key for memoization of PathLists.
Note that we want this to be pretty quick, so we don't completely Note that we want this to be pretty quick, so we don't completely
canonicalize all forms of the same list. For example, canonicalize all forms of the same list. For example,
'dir1:$ROOT/dir2' and ['$ROOT/dir1', 'dir'] may logically ``dir1:$ROOT/dir2`` and ``['$ROOT/dir1', 'dir']`` may logically
represent the same list if you're executing from $ROOT, but represent the same list if you're executing from ``$ROOT``, but
we're not going to bother splitting strings into path elements, we're not going to bother splitting strings into path elements,
or massaging strings into Nodes, to identify that equivalence. or massaging strings into Nodes, to identify that equivalence.
We just want to eliminate obvious redundancy from the normal We just want to eliminate obvious redundancy from the normal
@ -191,9 +191,10 @@ class PathListCache:
@SCons.Memoize.CountDictCall(_PathList_key) @SCons.Memoize.CountDictCall(_PathList_key)
def PathList(self, pathlist, split=True): def PathList(self, pathlist, split=True):
""" """Entry point for getting PathLists.
Returns the cached _PathList object for the specified pathlist,
creating and caching a new object as necessary. Returns the cached :class:`_PathList` object for the specified
pathlist, creating and caching a new object as necessary.
""" """
pathlist = self._PathList_key(pathlist) pathlist = self._PathList_key(pathlist)
try: try:
@ -215,7 +216,8 @@ class PathListCache:
PathList = PathListCache().PathList PathList = PathListCache().PathList
# TODO: removing the class object here means Sphinx doesn't pick up its
# docstrings: they're fine for reading here, but are not in API Docs.
del PathListCache del PathListCache
# Local Variables: # Local Variables:

View file

@ -41,7 +41,7 @@ import time
import traceback import traceback
import platform import platform
import threading import threading
from typing import Optional, List from typing import Optional, List, TYPE_CHECKING
import SCons.CacheDir import SCons.CacheDir
import SCons.Debug import SCons.Debug
@ -59,15 +59,17 @@ import SCons.Taskmaster
import SCons.Util import SCons.Util
import SCons.Warnings import SCons.Warnings
import SCons.Script.Interactive import SCons.Script.Interactive
if TYPE_CHECKING:
from SCons.Script import SConsOption
from SCons.Util.stats import count_stats, memory_stats, time_stats, ENABLE_JSON, write_scons_stats_file, JSON_OUTPUT_FILE from SCons.Util.stats import count_stats, memory_stats, time_stats, ENABLE_JSON, write_scons_stats_file, JSON_OUTPUT_FILE
from SCons import __version__ as SConsVersion from SCons import __version__ as SConsVersion
# these define the range of versions SCons supports # these define the range of versions SCons supports
minimum_python_version = (3, 6, 0) minimum_python_version = (3, 6, 0)
deprecated_python_version = (3, 6, 0) deprecated_python_version = (3, 7, 0) # the first non-deprecated version
# ordered list of SConsctruct names to look for if there is no -f flag # ordered list of SConstruct names to look for if there is no -f flag
KNOWN_SCONSTRUCT_NAMES = [ KNOWN_SCONSTRUCT_NAMES = [
'SConstruct', 'SConstruct',
'Sconstruct', 'Sconstruct',
@ -84,7 +86,11 @@ KNOWN_SCONSCRIPTS = [
"Sconstruct", "Sconstruct",
"sconstruct", "sconstruct",
"SConscript", "SConscript",
"Sconscript",
"sconscript", "sconscript",
"SCsub", # Uncommon alternative to SConscript
"Scsub",
"scsub",
] ]
# Global variables # Global variables
@ -174,6 +180,7 @@ class Progressor:
ProgressObject = SCons.Util.Null() ProgressObject = SCons.Util.Null()
def Progress(*args, **kw) -> None: def Progress(*args, **kw) -> None:
"""Show progress during building - Public API."""
global ProgressObject global ProgressObject
ProgressObject = Progressor(*args, **kw) ProgressObject = Progressor(*args, **kw)
@ -501,29 +508,47 @@ class FakeOptionParser:
# TODO: to quiet checkers, FakeOptionParser should also define # TODO: to quiet checkers, FakeOptionParser should also define
# raise_exception_on_error, preserve_unknown_options, largs and parse_args # raise_exception_on_error, preserve_unknown_options, largs and parse_args
def add_local_option(self, *args, **kw) -> None: def add_local_option(self, *args, **kw) -> "SConsOption":
pass pass
OptionsParser = FakeOptionParser() OptionsParser = FakeOptionParser()
def AddOption(*args, **kw): def AddOption(*args, settable: bool = False, **kw) -> "SConsOption":
"""Add a local option to the option parser - Public API.
If the *settable* parameter is true, the option will be included in the
list of settable options; all other keyword arguments are passed on to
:meth:`~SCons.Script.SConsOptions.SConsOptionParser.add_local_option`.
.. versionchanged:: 4.8.0
The *settable* parameter added to allow including the new option
to the table of options eligible to use :func:`SetOption`.
"""
if 'default' not in kw: if 'default' not in kw:
kw['default'] = None kw['default'] = None
kw['settable'] = settable
result = OptionsParser.add_local_option(*args, **kw) result = OptionsParser.add_local_option(*args, **kw)
return result return result
def GetOption(name): def GetOption(name: str):
"""Get the value from an option - Public API."""
return getattr(OptionsParser.values, name) return getattr(OptionsParser.values, name)
def SetOption(name, value): def SetOption(name: str, value):
"""Set the value of an option - Public API."""
return OptionsParser.values.set_option(name, value) return OptionsParser.values.set_option(name, value)
def DebugOptions(json=None): def DebugOptions(json: Optional[str] = None) -> None:
""" """Specify options to SCons debug logic - Public API.
API to allow specifying options to SCons debug logic
Currently only json is supported which changes the Currently only *json* is supported, which changes the JSON file
json file written by --debug=json from the default written to if the ``--debug=json`` command-line option is specified
to the value supplied.
.. versionadded:: 4.6.0
""" """
if json is not None: if json is not None:
json_node = SCons.Defaults.DefaultEnvironment().arg2nodes(json) json_node = SCons.Defaults.DefaultEnvironment().arg2nodes(json)
@ -540,7 +565,7 @@ def DebugOptions(json=None):
raise SCons.Errors.UserError(f"Unable to create directory for JSON debug output file: {SCons.Util.stats.JSON_OUTPUT_FILE}") raise SCons.Errors.UserError(f"Unable to create directory for JSON debug output file: {SCons.Util.stats.JSON_OUTPUT_FILE}")
def ValidateOptions(throw_exception: bool=False) -> None: def ValidateOptions(throw_exception: bool = False) -> None:
"""Validate options passed to SCons on the command line. """Validate options passed to SCons on the command line.
Checks that all options given on the command line are known to this Checks that all options given on the command line are known to this
@ -1104,11 +1129,13 @@ def _main(parser):
# warning about deprecated Python versions--delayed until here # warning about deprecated Python versions--delayed until here
# in case they disabled the warning in the SConscript files. # in case they disabled the warning in the SConscript files.
if python_version_deprecated(): if python_version_deprecated():
msg = "Support for pre-%s Python version (%s) is deprecated.\n" + \
" If this will cause hardship, contact scons-dev@scons.org"
deprecated_version_string = ".".join(map(str, deprecated_python_version)) deprecated_version_string = ".".join(map(str, deprecated_python_version))
SCons.Warnings.warn(SCons.Warnings.PythonVersionWarning, msg = (
msg % (deprecated_version_string, python_version_string())) f"Support for Python older than {deprecated_version_string}"
f" is deprecated ({python_version_string()} detected).\n"
" If this will cause hardship, contact scons-dev@scons.org"
)
SCons.Warnings.warn(SCons.Warnings.PythonVersionWarning, msg)
if not options.help: if not options.help:
# [ ] Clarify why we need to create Builder here at all, and # [ ] Clarify why we need to create Builder here at all, and
@ -1396,6 +1423,10 @@ def _exec_main(parser, values) -> None:
sconscript files that don't have the suffix. sconscript files that don't have the suffix.
.. versionadded:: 4.6.0 .. versionadded:: 4.6.0
.. versionchanged:: 4.8.0
The additional name ``SCsub`` (with spelling variants)
is also recognized - Godot uses this name.
""" """
if os.path.isabs(filename) and os.path.exists(filename): if os.path.isabs(filename) and os.path.exists(filename):
return filename return filename

View file

@ -66,17 +66,14 @@ def diskcheck_convert(value):
class SConsValues(optparse.Values): class SConsValues(optparse.Values):
""" """Holder class for uniform access to SCons options.
Holder class for uniform access to SCons options, regardless
of whether they can be set on the command line or in the
SConscript files (using the SetOption() function).
A SCons option value can originate three different ways: A SCons option value can originate three different ways:
1) set on the command line; 1. set on the command line.
2) set in an SConscript file; 2. set in an SConscript file via :func:`~SCons.Script.Main.SetOption`.
3) the default setting (from the the op.add_option() 3. the default setting (from the the ``op.add_option()``
calls in the Parser() function, below). calls in the :func:`Parser` function, below).
The command line always overrides a value set in a SConscript file, The command line always overrides a value set in a SConscript file,
which in turn always overrides default settings. Because we want which in turn always overrides default settings. Because we want
@ -87,15 +84,15 @@ class SConsValues(optparse.Values):
The solution implemented in this class is to keep these different sets The solution implemented in this class is to keep these different sets
of settings separate (command line, SConscript file, and default) of settings separate (command line, SConscript file, and default)
and to override the __getattr__() method to check them in turn. and to override the :meth:`__getattr__` method to check them in turn.
This should allow the rest of the code to just fetch values as This allows the rest of the code to just fetch values as attributes of
attributes of an instance of this class, without having to worry an instance of this class, without having to worry about where they
about where they came from. came from (the scheme is similar to a ``ChainMap``).
Note that not all command line options are settable from SConscript Note that not all command line options are settable from SConscript
files, and the ones that are must be explicitly added to the files, and the ones that are must be explicitly added to the
"settable" list in this class, and optionally validated and coerced :attr:`settable` list in this class, and optionally validated and coerced
in the set_option() method. in the :meth:`set_option` method.
""" """
def __init__(self, defaults) -> None: def __init__(self, defaults) -> None:
@ -103,28 +100,24 @@ class SConsValues(optparse.Values):
self.__SConscript_settings__ = {} self.__SConscript_settings__ = {}
def __getattr__(self, attr): def __getattr__(self, attr):
""" """Fetch an options value, respecting priority rules.
Fetches an options value, checking first for explicit settings
from the command line (which are direct attributes), then the This is a little tricky: since we're answering questions
SConscript file settings, then the default values. about outselves, we have avoid lookups that would send us into
into infinite recursion, thus the ``__dict__`` stuff.
""" """
try: try:
return self.__dict__[attr] return self.__dict__[attr]
except KeyError: except KeyError:
try: try:
return self.__dict__['__SConscript_settings__'][attr] return self.__dict__['__SConscript_settings__'][attr]
except KeyError: except KeyError:
try: try:
return getattr(self.__dict__['__defaults__'], attr) return getattr(self.__dict__['__defaults__'], attr)
except KeyError: except KeyError as exc:
# Added because with py3 this is a new class, # Need to respond with AttributeError because
# not a classic class, and due to the way # deepcopy expects that if __setstate__ is not available.
# In that case it will create an object without raise AttributeError(attr) from exc
# __defaults__, and then query for __setstate__
# which will throw an exception of KeyError
# deepcopy() is expecting AttributeError if __setstate__
# is not available.
raise AttributeError(attr)
# keep this list in sync with the SetOption doc in SCons/Script/Main.xml # keep this list in sync with the SetOption doc in SCons/Script/Main.xml
# search for UPDATE_SETOPTION_DOCS there. # search for UPDATE_SETOPTION_DOCS there.
@ -148,26 +141,26 @@ class SConsValues(optparse.Values):
'silent', 'silent',
'stack_size', 'stack_size',
'warn', 'warn',
# TODO: Remove these once we update the AddOption() API to allow setting
# added flag as settable.
# Requested settable flag in : https://github.com/SCons/scons/issues/3983
# From experimental ninja
'disable_execute_ninja',
'disable_ninja',
'skip_ninja_regen'
] ]
def set_option(self, name, value): def set_option(self, name: str, value) -> None:
"""Sets an option from an SConscript file. """Sets an option *name* from an SConscript file.
Vvalidation steps for known (that is, defined in SCons itself)
options are in-line here. Validation should be along the same
lines as for options processed from the command line -
it's kind of a pain to have to duplicate. Project-defined options
can specify callbacks for the command-line version, but will have
no inbuilt validation here. It's up to the build system maintainer
to make sure :func:`~SCons.Script.Main.SetOption` is being used
correctly, we can't really do any better here.
Raises: Raises:
UserError: invalid or malformed option ("error in your script") UserError: the option is not settable.
""" """
if name not in self.settable: if name not in self.settable:
raise SCons.Errors.UserError( raise SCons.Errors.UserError(
"This option is not settable from a SConscript file: %s" % name f"This option is not settable from an SConscript file: {name!r}"
) )
# the following are for options that need some extra processing # the following are for options that need some extra processing
@ -247,7 +240,6 @@ class SConsOption(optparse.Option):
return tuple([self.check_value(opt, v) for v in value]) return tuple([self.check_value(opt, v) for v in value])
def process(self, opt, value, values, parser): def process(self, opt, value, values, parser):
# First, convert the value(s) to the right type. Howl if any # First, convert the value(s) to the right type. Howl if any
# value(s) are bogus. # value(s) are bogus.
value = self.convert_value(opt, value) value = self.convert_value(opt, value)
@ -313,9 +305,7 @@ class SConsOptionParser(optparse.OptionParser):
raise_exception_on_error = False raise_exception_on_error = False
def error(self, msg): def error(self, msg):
""" """Overridden OptionValueError exception handler."""
overridden OptionValueError exception handler
"""
if self.raise_exception_on_error: if self.raise_exception_on_error:
raise SConsBadOptionError(msg, self) raise SConsBadOptionError(msg, self)
else: else:
@ -397,46 +387,46 @@ class SConsOptionParser(optparse.OptionParser):
option.process(opt, value, values, self) option.process(opt, value, values, self)
def reparse_local_options(self) -> None: def reparse_local_options(self) -> None:
""" Re-parse the leftover command-line options. """Re-parse the leftover command-line options.
Parse options stored in `self.largs`, so that any value Leftover options are stored in ``self.largs``, so that any value
overridden on the command line is immediately available overridden on the command line is immediately available
if the user turns around and does a :func:`GetOption` right away. if the user turns around and does a :func:`~SCons.Script.Main.GetOption`
right away.
We mimic the processing of the single args We mimic the processing of the single args
in the original OptionParser :func:`_process_args`, but here we in the original OptionParser :func:`_process_args`, but here we
allow exact matches for long-opts only (no partial argument names!). allow exact matches for long-opts only (no partial argument names!).
Otherwise there could be problems in :func:`add_local_option` Otherwise there could be problems in :meth:`add_local_option`
below. When called from there, we try to reparse the below. When called from there, we try to reparse the
command-line arguments that command-line arguments that
1. haven't been processed so far (`self.largs`), but 1. haven't been processed so far (`self.largs`), but
2. are possibly not added to the list of options yet. 2. are possibly not added to the list of options yet.
So, when we only have a value for "--myargument" so far, So, when we only have a value for ``--myargument`` so far,
a command-line argument of "--myarg=test" would set it, a command-line argument of ``--myarg=test`` would set it,
per the behaviour of :func:`_match_long_opt`, per the behaviour of :func:`_match_long_opt`,
which allows for partial matches of the option name, which allows for partial matches of the option name,
as long as the common prefix appears to be unique. as long as the common prefix appears to be unique.
This would lead to further confusion, because we might want This would lead to further confusion, because we might want
to add another option "--myarg" later on (see issue #2929). to add another option ``--myarg`` later on (see issue #2929).
""" """
rargs = [] rargs = []
largs_restore = [] largs_restore = []
# Loop over all remaining arguments # Loop over all remaining arguments
skip = False skip = False
for l in self.largs: for larg in self.largs:
if skip: if skip:
# Accept all remaining arguments as they are # Accept all remaining arguments as they are
largs_restore.append(l) largs_restore.append(larg)
else: else:
if len(l) > 2 and l[0:2] == "--": if len(larg) > 2 and larg[0:2] == "--":
# Check long option # Check long option
lopt = (l,) lopt = [larg]
if "=" in l: if "=" in larg:
# Split into option and value # Split into option and value
lopt = l.split("=", 1) lopt = larg.split("=", 1)
if lopt[0] in self._long_opt: if lopt[0] in self._long_opt:
# Argument is already known # Argument is already known
@ -445,26 +435,35 @@ class SConsOptionParser(optparse.OptionParser):
# Not known yet, so reject for now # Not known yet, so reject for now
largs_restore.append('='.join(lopt)) largs_restore.append('='.join(lopt))
else: else:
if l == "--" or l == "-": if larg in("--", "-"):
# Stop normal processing and don't # Stop normal processing and don't
# process the rest of the command-line opts # process the rest of the command-line opts
largs_restore.append(l) largs_restore.append(larg)
skip = True skip = True
else: else:
rargs.append(l) rargs.append(larg)
# Parse the filtered list # Parse the filtered list
self.parse_args(rargs, self.values) self.parse_args(rargs, self.values)
# Restore the list of remaining arguments for the # Restore the list of leftover arguments for the
# next call of AddOption/add_local_option... # next call of AddOption/add_local_option...
self.largs = self.largs + largs_restore self.largs = self.largs + largs_restore
def add_local_option(self, *args, **kw): def add_local_option(self, *args, **kw) -> SConsOption:
""" Adds a local option to the parser. """ Adds a local option to the parser.
This is initiated by an :func:`AddOption` call to add a user-defined This is initiated by an :func:`~SCons.Script.Main.AddOption` call to
command-line option. We add the option to a separate option add a user-defined command-line option. Add the option to a separate
group for the local options, creating the group if necessary. option group for the local options, creating the group if necessary.
The keyword argument *settable* is recognized specially (and
removed from *kw*). If true, the option is marked as modifiable;
by default "local" (project-added) options are not eligible for
for :func:`~SCons.Script.Main.SetOption` calls.
.. versionchanged:: 4.8.0
Added special handling of *settable*.
""" """
try: try:
group = self.local_option_group group = self.local_option_group
@ -473,6 +472,7 @@ class SConsOptionParser(optparse.OptionParser):
group = self.add_option_group(group) group = self.add_option_group(group)
self.local_option_group = group self.local_option_group = group
settable = kw.pop('settable')
result = group.add_option(*args, **kw) result = group.add_option(*args, **kw)
if result: if result:
# The option was added successfully. We now have to add the # The option was added successfully. We now have to add the
@ -485,6 +485,8 @@ class SConsOptionParser(optparse.OptionParser):
# right away. # right away.
setattr(self.values.__defaults__, result.dest, result.default) setattr(self.values.__defaults__, result.dest, result.default)
self.reparse_local_options() self.reparse_local_options()
if settable:
SConsValues.settable.append(result.dest)
return result return result
@ -614,7 +616,8 @@ class SConsIndentedHelpFormatter(optparse.IndentedHelpFormatter):
"""Local-only version of store_option_strings. """Local-only version of store_option_strings.
We need to replicate this so the formatter will be set up We need to replicate this so the formatter will be set up
properly if we didn't go through the "normal" store_option_strings properly if we didn't go through the "normal"
:math:`~optparse.HelpFormatter.store_option_strings`.
.. versionadded:: 4.6.0 .. versionadded:: 4.6.0
""" """
@ -633,15 +636,18 @@ def Parser(version):
"""Returns a parser object initialized with the standard SCons options. """Returns a parser object initialized with the standard SCons options.
Add options in the order we want them to show up in the ``-H`` help Add options in the order we want them to show up in the ``-H`` help
text, basically alphabetical. Each ``op.add_option()`` call text, basically alphabetical. For readability, Each
should have a consistent format:: :meth:`~optparse.OptionContainer.add_option` call should have a
consistent format::
op.add_option("-L", "--long-option-name", op.add_option(
nargs=1, type="string", "-L", "--long-option-name",
dest="long_option_name", default='foo', nargs=1, type="string",
action="callback", callback=opt_long_option, dest="long_option_name", default='foo',
help="help text goes here", action="callback", callback=opt_long_option,
metavar="VAR") help="help text goes here",
metavar="VAR"
)
Even though the :mod:`optparse` module constructs reasonable default Even though the :mod:`optparse` module constructs reasonable default
destination names from the long option names, we're going to be destination names from the long option names, we're going to be

View file

@ -45,6 +45,7 @@ import re
import sys import sys
import traceback import traceback
import time import time
from typing import Tuple
class SConscriptReturn(Exception): class SConscriptReturn(Exception):
pass pass
@ -385,7 +386,7 @@ class SConsEnvironment(SCons.Environment.Base):
# Private methods of an SConsEnvironment. # Private methods of an SConsEnvironment.
# #
@staticmethod @staticmethod
def _get_major_minor_revision(version_string): def _get_major_minor_revision(version_string: str) -> Tuple[int, int, int]:
"""Split a version string into major, minor and (optionally) """Split a version string into major, minor and (optionally)
revision parts. revision parts.
@ -484,15 +485,22 @@ class SConsEnvironment(SCons.Environment.Base):
SCons.Script._Set_Default_Targets(self, targets) SCons.Script._Set_Default_Targets(self, targets)
@staticmethod @staticmethod
def EnsureSConsVersion(major, minor, revision: int=0) -> None: def GetSConsVersion() -> Tuple[int, int, int]:
"""Return the current SCons version.
.. versionadded:: 4.8.0
"""
return SConsEnvironment._get_major_minor_revision(SCons.__version__)
@staticmethod
def EnsureSConsVersion(major: int, minor: int, revision: int = 0) -> None:
"""Exit abnormally if the SCons version is not late enough.""" """Exit abnormally if the SCons version is not late enough."""
# split string to avoid replacement during build process # split string to avoid replacement during build process
if SCons.__version__ == '__' + 'VERSION__': if SCons.__version__ == '__' + 'VERSION__':
SCons.Warnings.warn(SCons.Warnings.DevelopmentVersionWarning, SCons.Warnings.warn(SCons.Warnings.DevelopmentVersionWarning,
"EnsureSConsVersion is ignored for development version") "EnsureSConsVersion is ignored for development version")
return return
scons_ver = SConsEnvironment._get_major_minor_revision(SCons.__version__) if SConsEnvironment.GetSConsVersion() < (major, minor, revision):
if scons_ver < (major, minor, revision):
if revision: if revision:
scons_ver_string = '%d.%d.%d' % (major, minor, revision) scons_ver_string = '%d.%d.%d' % (major, minor, revision)
else: else:

View file

@ -297,6 +297,7 @@ def Variables(files=None, args=ARGUMENTS):
# #
# Static functions that do not trigger initialization of # Static functions that do not trigger initialization of
# DefaultEnvironment() and don't use its state. # DefaultEnvironment() and don't use its state.
GetSConsVersion = _SConscript.SConsEnvironment.GetSConsVersion
EnsureSConsVersion = _SConscript.SConsEnvironment.EnsureSConsVersion EnsureSConsVersion = _SConscript.SConsEnvironment.EnsureSConsVersion
EnsurePythonVersion = _SConscript.SConsEnvironment.EnsurePythonVersion EnsurePythonVersion = _SConscript.SConsEnvironment.EnsurePythonVersion
Exit = _SConscript.SConsEnvironment.Exit Exit = _SConscript.SConsEnvironment.Exit

View file

@ -278,9 +278,8 @@ class ThreadPool:
try: try:
prev_size = threading.stack_size(stack_size * 1024) prev_size = threading.stack_size(stack_size * 1024)
except AttributeError as e: except RuntimeError as e:
# Only print a warning if the stack size has been # Only print a warning if the stack size has been explicitly set.
# explicitly set.
if explicit_stack_size is not None: if explicit_stack_size is not None:
msg = "Setting stack size is unsupported by this version of Python:\n " + \ msg = "Setting stack size is unsupported by this version of Python:\n " + \
e.args[0] e.args[0]

View file

@ -0,0 +1,668 @@
# MIT License
#
# Copyright The SCons Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
Version kind categorization for Microsoft Visual C/C++.
"""
import os
import re
from collections import (
namedtuple,
)
from ..common import (
debug,
)
from . import Registry
from . import Util
from . import Dispatcher
Dispatcher.register_modulename(__name__)
# use express install for non-express msvc_version if no other installations found
USE_EXPRESS_FOR_NONEXPRESS = True
# productdir kind
VCVER_KIND_UNKNOWN = 0 # undefined
VCVER_KIND_DEVELOP = 1 # devenv binary
VCVER_KIND_EXPRESS = 2 # express binary
VCVER_KIND_BTDISPATCH = 3 # no ide binaries (buildtools dispatch folder)
VCVER_KIND_VCFORPYTHON = 4 # no ide binaries (2008/9.0)
VCVER_KIND_EXPRESS_WIN = 5 # express for windows binary (VSWinExpress)
VCVER_KIND_EXPRESS_WEB = 6 # express for web binary (VWDExpress)
VCVER_KIND_SDK = 7 # no ide binaries
VCVER_KIND_CMDLINE = 8 # no ide binaries
VCVER_KIND_STR = {
VCVER_KIND_UNKNOWN: '<Unknown>',
VCVER_KIND_DEVELOP: 'Develop',
VCVER_KIND_EXPRESS: 'Express',
VCVER_KIND_BTDISPATCH: 'BTDispatch',
VCVER_KIND_VCFORPYTHON: 'VCForPython',
VCVER_KIND_EXPRESS_WIN: 'Express-Win',
VCVER_KIND_EXPRESS_WEB: 'Express-Web',
VCVER_KIND_SDK: 'SDK',
VCVER_KIND_CMDLINE: 'CmdLine',
}
BITFIELD_KIND_DEVELOP = 0b_1000
BITFIELD_KIND_EXPRESS = 0b_0100
BITFIELD_KIND_EXPRESS_WIN = 0b_0010
BITFIELD_KIND_EXPRESS_WEB = 0b_0001
VCVER_KIND_PROGRAM = namedtuple("VCVerKindProgram", [
'kind', # relpath from pdir to vsroot
'program', # ide binaries
'bitfield',
])
#
IDE_PROGRAM_DEVENV_COM = VCVER_KIND_PROGRAM(
kind=VCVER_KIND_DEVELOP,
program='devenv.com',
bitfield=BITFIELD_KIND_DEVELOP,
)
IDE_PROGRAM_MSDEV_COM = VCVER_KIND_PROGRAM(
kind=VCVER_KIND_DEVELOP,
program='msdev.com',
bitfield=BITFIELD_KIND_DEVELOP,
)
IDE_PROGRAM_WDEXPRESS_EXE = VCVER_KIND_PROGRAM(
kind=VCVER_KIND_EXPRESS,
program='WDExpress.exe',
bitfield=BITFIELD_KIND_EXPRESS,
)
IDE_PROGRAM_VCEXPRESS_EXE = VCVER_KIND_PROGRAM(
kind=VCVER_KIND_EXPRESS,
program='VCExpress.exe',
bitfield=BITFIELD_KIND_EXPRESS,
)
IDE_PROGRAM_VSWINEXPRESS_EXE = VCVER_KIND_PROGRAM(
kind=VCVER_KIND_EXPRESS_WIN,
program='VSWinExpress.exe',
bitfield=BITFIELD_KIND_EXPRESS_WIN,
)
IDE_PROGRAM_VWDEXPRESS_EXE = VCVER_KIND_PROGRAM(
kind=VCVER_KIND_EXPRESS_WEB,
program='VWDExpress.exe',
bitfield=BITFIELD_KIND_EXPRESS_WEB,
)
# detection configuration
VCVER_KIND_DETECT = namedtuple("VCVerKindDetect", [
'root', # relpath from pdir to vsroot
'path', # vsroot to ide dir
'programs', # ide binaries
])
# detected binaries
VCVER_DETECT_BINARIES = namedtuple("VCVerDetectBinaries", [
'bitfields', # detect values
'have_dev', # develop ide binary
'have_exp', # express ide binary
'have_exp_win', # express windows ide binary
'have_exp_web', # express web ide binary
])
VCVER_DETECT_KIND = namedtuple("VCVerDetectKind", [
'skip', # skip vs root
'save', # save in case no other kind found
'kind', # internal kind
'binaries_t',
'extended',
])
# unknown value
_VCVER_DETECT_KIND_UNKNOWN = VCVER_DETECT_KIND(
skip=True,
save=False,
kind=VCVER_KIND_UNKNOWN,
binaries_t=VCVER_DETECT_BINARIES(
bitfields=0b0,
have_dev=False,
have_exp=False,
have_exp_win=False,
have_exp_web=False,
),
extended={},
)
#
_msvc_pdir_func = None
def register_msvc_version_pdir_func(func):
global _msvc_pdir_func
if func:
_msvc_pdir_func = func
_cache_vcver_kind_map = {}
def msvc_version_register_kind(msvc_version, kind_t) -> None:
global _cache_vcver_kind_map
if kind_t is None:
kind_t = _VCVER_DETECT_KIND_UNKNOWN
debug('msvc_version=%s, kind=%s', repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind]))
_cache_vcver_kind_map[msvc_version] = kind_t
def _msvc_version_kind_lookup(msvc_version, env=None):
global _cache_vcver_kind_map
global _msvc_pdir_func
if msvc_version not in _cache_vcver_kind_map:
_msvc_pdir_func(msvc_version, env)
kind_t = _cache_vcver_kind_map.get(msvc_version, _VCVER_DETECT_KIND_UNKNOWN)
debug(
'kind=%s, dev=%s, exp=%s, msvc_version=%s',
repr(VCVER_KIND_STR[kind_t.kind]),
kind_t.binaries_t.have_dev, kind_t.binaries_t.have_exp,
repr(msvc_version)
)
return kind_t
def msvc_version_is_btdispatch(msvc_version, env=None):
kind_t = _msvc_version_kind_lookup(msvc_version, env)
is_btdispatch = bool(kind_t.kind == VCVER_KIND_BTDISPATCH)
debug(
'is_btdispatch=%s, kind:%s, msvc_version=%s',
repr(is_btdispatch), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version)
)
return is_btdispatch
def msvc_version_is_express(msvc_version, env=None):
kind_t = _msvc_version_kind_lookup(msvc_version, env)
is_express = bool(kind_t.kind == VCVER_KIND_EXPRESS)
debug(
'is_express=%s, kind:%s, msvc_version=%s',
repr(is_express), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version)
)
return is_express
def msvc_version_is_vcforpython(msvc_version, env=None):
kind_t = _msvc_version_kind_lookup(msvc_version, env)
is_vcforpython = bool(kind_t.kind == VCVER_KIND_VCFORPYTHON)
debug(
'is_vcforpython=%s, kind:%s, msvc_version=%s',
repr(is_vcforpython), repr(VCVER_KIND_STR[kind_t.kind]), repr(msvc_version)
)
return is_vcforpython
def msvc_version_skip_uwp_target(env, msvc_version):
vernum = float(Util.get_msvc_version_prefix(msvc_version))
vernum_int = int(vernum * 10)
if vernum_int != 140:
return False
kind_t = _msvc_version_kind_lookup(msvc_version, env)
if kind_t.kind != VCVER_KIND_EXPRESS:
return False
target_arch = env.get('TARGET_ARCH')
uwp_is_supported = kind_t.extended.get('uwp_is_supported', {})
is_supported = uwp_is_supported.get(target_arch, True)
if is_supported:
return False
return True
def _pdir_detect_binaries(pdir, detect):
vs_root = os.path.join(pdir, detect.root)
ide_path = os.path.join(vs_root, detect.path)
bitfields = 0b_0000
for ide_program in detect.programs:
prog = os.path.join(ide_path, ide_program.program)
if not os.path.exists(prog):
continue
bitfields |= ide_program.bitfield
have_dev = bool(bitfields & BITFIELD_KIND_DEVELOP)
have_exp = bool(bitfields & BITFIELD_KIND_EXPRESS)
have_exp_win = bool(bitfields & BITFIELD_KIND_EXPRESS_WIN)
have_exp_web = bool(bitfields & BITFIELD_KIND_EXPRESS_WEB)
binaries_t = VCVER_DETECT_BINARIES(
bitfields=bitfields,
have_dev=have_dev,
have_exp=have_exp,
have_exp_win=have_exp_win,
have_exp_web=have_exp_web,
)
debug(
'vs_root=%s, dev=%s, exp=%s, exp_win=%s, exp_web=%s, pdir=%s',
repr(vs_root),
binaries_t.have_dev, binaries_t.have_exp,
binaries_t.have_exp_win, binaries_t.have_exp_web,
repr(pdir)
)
return vs_root, binaries_t
_cache_pdir_vswhere_kind = {}
def msvc_version_pdir_vswhere_kind(msvc_version, pdir, detect_t):
global _cache_pdir_vswhere_kind
vc_dir = os.path.normcase(os.path.normpath(pdir))
cache_key = (msvc_version, vc_dir)
rval = _cache_pdir_vswhere_kind.get(cache_key)
if rval is not None:
debug('cache=%s', repr(rval))
return rval
extended = {}
prefix, suffix = Util.get_msvc_version_prefix_suffix(msvc_version)
vs_root, binaries_t = _pdir_detect_binaries(pdir, detect_t)
if binaries_t.have_dev:
kind = VCVER_KIND_DEVELOP
elif binaries_t.have_exp:
kind = VCVER_KIND_EXPRESS
else:
kind = VCVER_KIND_CMDLINE
skip = False
save = False
if suffix != 'Exp' and kind == VCVER_KIND_EXPRESS:
skip = True
save = USE_EXPRESS_FOR_NONEXPRESS
elif suffix == 'Exp' and kind != VCVER_KIND_EXPRESS:
skip = True
kind_t = VCVER_DETECT_KIND(
skip=skip,
save=save,
kind=kind,
binaries_t=binaries_t,
extended=extended,
)
debug(
'skip=%s, save=%s, kind=%s, msvc_version=%s, pdir=%s',
kind_t.skip, kind_t.save, repr(VCVER_KIND_STR[kind_t.kind]),
repr(msvc_version), repr(pdir)
)
_cache_pdir_vswhere_kind[cache_key] = kind_t
return kind_t
# VS2015 buildtools batch file call detection
# vs2015 buildtools do not support sdk_version or UWP arguments
_VS2015BT_PATH = r'..\Microsoft Visual C++ Build Tools\vcbuildtools.bat'
_VS2015BT_REGEX_STR = ''.join([
r'^\s*if\s+exist\s+',
re.escape(fr'"%~dp0..\{_VS2015BT_PATH}"'),
r'\s+goto\s+setup_buildsku\s*$',
])
_VS2015BT_VCVARS_BUILDTOOLS = re.compile(_VS2015BT_REGEX_STR, re.IGNORECASE)
_VS2015BT_VCVARS_STOP = re.compile(r'^\s*[:]Setup_VS\s*$', re.IGNORECASE)
def _vs_buildtools_2015_vcvars(vcvars_file):
have_buildtools_vcvars = False
with open(vcvars_file) as fh:
for line in fh:
if _VS2015BT_VCVARS_BUILDTOOLS.match(line):
have_buildtools_vcvars = True
break
if _VS2015BT_VCVARS_STOP.match(line):
break
return have_buildtools_vcvars
def _vs_buildtools_2015(vs_root, vc_dir):
is_btdispatch = False
do_once = True
while do_once:
do_once = False
buildtools_file = os.path.join(vs_root, _VS2015BT_PATH)
have_buildtools = os.path.exists(buildtools_file)
debug('have_buildtools=%s', have_buildtools)
if not have_buildtools:
break
vcvars_file = os.path.join(vc_dir, 'vcvarsall.bat')
have_vcvars = os.path.exists(vcvars_file)
debug('have_vcvars=%s', have_vcvars)
if not have_vcvars:
break
have_buildtools_vcvars = _vs_buildtools_2015_vcvars(vcvars_file)
debug('have_buildtools_vcvars=%s', have_buildtools_vcvars)
if not have_buildtools_vcvars:
break
is_btdispatch = True
debug('is_btdispatch=%s', is_btdispatch)
return is_btdispatch
_VS2015EXP_VCVARS_LIBPATH = re.compile(
''.join([
r'^\s*\@if\s+exist\s+\"\%VCINSTALLDIR\%LIB\\store\\(amd64|arm)"\s+',
r'set (LIB|LIBPATH)=\%VCINSTALLDIR\%LIB\\store\\(amd64|arm);.*\%(LIB|LIBPATH)\%\s*$'
]),
re.IGNORECASE
)
_VS2015EXP_VCVARS_STOP = re.compile(r'^\s*[:]GetVSCommonToolsDir\s*$', re.IGNORECASE)
def _vs_express_2015_vcvars(vcvars_file):
n_libpath = 0
with open(vcvars_file) as fh:
for line in fh:
if _VS2015EXP_VCVARS_LIBPATH.match(line):
n_libpath += 1
elif _VS2015EXP_VCVARS_STOP.match(line):
break
have_uwp_fix = n_libpath >= 2
return have_uwp_fix
def _vs_express_2015(pdir):
have_uwp_amd64 = False
have_uwp_arm = False
vcvars_file = os.path.join(pdir, r'vcvarsall.bat')
if os.path.exists(vcvars_file):
vcvars_file = os.path.join(pdir, r'bin\x86_amd64\vcvarsx86_amd64.bat')
if os.path.exists(vcvars_file):
have_uwp_fix = _vs_express_2015_vcvars(vcvars_file)
if have_uwp_fix:
have_uwp_amd64 = True
vcvars_file = os.path.join(pdir, r'bin\x86_arm\vcvarsx86_arm.bat')
if os.path.exists(vcvars_file):
have_uwp_fix = _vs_express_2015_vcvars(vcvars_file)
if have_uwp_fix:
have_uwp_arm = True
debug('have_uwp_amd64=%s, have_uwp_arm=%s', have_uwp_amd64, have_uwp_arm)
return have_uwp_amd64, have_uwp_arm
# winsdk installed 2010 [7.1], 2008 [7.0, 6.1] folders
_REGISTRY_WINSDK_VERSIONS = {'10.0', '9.0'}
_cache_pdir_registry_winsdk = {}
def _msvc_version_pdir_registry_winsdk(msvc_version, pdir):
global _cache_pdir_registry_winsdk
# detect winsdk-only installations
#
# registry keys:
# [prefix]\VisualStudio\SxS\VS7\10.0 <undefined>
# [prefix]\VisualStudio\SxS\VC7\10.0 product directory
# [prefix]\VisualStudio\SxS\VS7\9.0 <undefined>
# [prefix]\VisualStudio\SxS\VC7\9.0 product directory
#
# winsdk notes:
# - winsdk installs do not define the common tools env var
# - the product dir is detected but the vcvars batch files will fail
# - regular installations populate the VS7 registry keys
#
vc_dir = os.path.normcase(os.path.normpath(pdir))
cache_key = (msvc_version, vc_dir)
rval = _cache_pdir_registry_winsdk.get(cache_key)
if rval is not None:
debug('cache=%s', repr(rval))
return rval
if msvc_version not in _REGISTRY_WINSDK_VERSIONS:
is_sdk = False
debug('is_sdk=%s, msvc_version=%s', is_sdk, repr(msvc_version))
else:
vc_dir = os.path.normcase(os.path.normpath(pdir))
vc_suffix = Registry.vstudio_sxs_vc7(msvc_version)
vc_qresults = [record[0] for record in Registry.microsoft_query_paths(vc_suffix)]
vc_root = os.path.normcase(os.path.normpath(vc_qresults[0])) if vc_qresults else None
if vc_dir != vc_root:
# registry vc path is not the current pdir
is_sdk = False
debug(
'is_sdk=%s, msvc_version=%s, pdir=%s, vc_root=%s',
is_sdk, repr(msvc_version), repr(vc_dir), repr(vc_root)
)
else:
# registry vc path is the current pdir
vs_suffix = Registry.vstudio_sxs_vs7(msvc_version)
vs_qresults = [record[0] for record in Registry.microsoft_query_paths(vs_suffix)]
vs_root = vs_qresults[0] if vs_qresults else None
is_sdk = bool(not vs_root and vc_root)
debug(
'is_sdk=%s, msvc_version=%s, vs_root=%s, vc_root=%s',
is_sdk, repr(msvc_version), repr(vs_root), repr(vc_root)
)
_cache_pdir_registry_winsdk[cache_key] = is_sdk
return is_sdk
_cache_pdir_registry_kind = {}
def msvc_version_pdir_registry_kind(msvc_version, pdir, detect_t, is_vcforpython=False):
global _cache_pdir_registry_kind
vc_dir = os.path.normcase(os.path.normpath(pdir))
cache_key = (msvc_version, vc_dir)
rval = _cache_pdir_registry_kind.get(cache_key)
if rval is not None:
debug('cache=%s', repr(rval))
return rval
extended = {}
prefix, suffix = Util.get_msvc_version_prefix_suffix(msvc_version)
vs_root, binaries_t = _pdir_detect_binaries(pdir, detect_t)
if binaries_t.have_dev:
kind = VCVER_KIND_DEVELOP
elif binaries_t.have_exp:
kind = VCVER_KIND_EXPRESS
elif msvc_version == '14.0' and _vs_buildtools_2015(vs_root, pdir):
kind = VCVER_KIND_BTDISPATCH
elif msvc_version == '9.0' and is_vcforpython:
kind = VCVER_KIND_VCFORPYTHON
elif binaries_t.have_exp_win:
kind = VCVER_KIND_EXPRESS_WIN
elif binaries_t.have_exp_web:
kind = VCVER_KIND_EXPRESS_WEB
elif _msvc_version_pdir_registry_winsdk(msvc_version, pdir):
kind = VCVER_KIND_SDK
else:
kind = VCVER_KIND_CMDLINE
skip = False
save = False
if kind in (VCVER_KIND_EXPRESS_WIN, VCVER_KIND_EXPRESS_WEB, VCVER_KIND_SDK):
skip = True
elif suffix != 'Exp' and kind == VCVER_KIND_EXPRESS:
skip = True
save = USE_EXPRESS_FOR_NONEXPRESS
elif suffix == 'Exp' and kind != VCVER_KIND_EXPRESS:
skip = True
if prefix == '14.0' and kind == VCVER_KIND_EXPRESS:
have_uwp_amd64, have_uwp_arm = _vs_express_2015(pdir)
uwp_is_supported = {
'x86': True,
'amd64': have_uwp_amd64,
'arm': have_uwp_arm,
}
extended['uwp_is_supported'] = uwp_is_supported
kind_t = VCVER_DETECT_KIND(
skip=skip,
save=save,
kind=kind,
binaries_t=binaries_t,
extended=extended,
)
debug(
'skip=%s, save=%s, kind=%s, msvc_version=%s, pdir=%s',
kind_t.skip, kind_t.save, repr(VCVER_KIND_STR[kind_t.kind]),
repr(msvc_version), repr(pdir)
)
_cache_pdir_registry_kind[cache_key] = kind_t
return kind_t
# queries
def get_msvc_version_kind(msvc_version, env=None):
kind_t = _msvc_version_kind_lookup(msvc_version, env)
kind_str = VCVER_KIND_STR[kind_t.kind]
debug(
'kind=%s, kind_str=%s, msvc_version=%s',
repr(kind_t.kind), repr(kind_str), repr(msvc_version)
)
return (kind_t.kind, kind_str)
def msvc_version_sdk_version_is_supported(msvc_version, env=None):
vernum = float(Util.get_msvc_version_prefix(msvc_version))
vernum_int = int(vernum * 10)
kind_t = _msvc_version_kind_lookup(msvc_version, env)
if vernum_int >= 141:
# VS2017 and later
is_supported = True
elif vernum_int == 140:
# VS2015:
# True: Develop, CmdLine
# False: Express, BTDispatch
is_supported = True
if kind_t.kind == VCVER_KIND_EXPRESS:
is_supported = False
elif kind_t.kind == VCVER_KIND_BTDISPATCH:
is_supported = False
else:
# VS2013 and earlier
is_supported = False
debug(
'is_supported=%s, msvc_version=%s, kind=%s',
is_supported, repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind])
)
return is_supported
def msvc_version_uwp_is_supported(msvc_version, target_arch=None, env=None):
vernum = float(Util.get_msvc_version_prefix(msvc_version))
vernum_int = int(vernum * 10)
kind_t = _msvc_version_kind_lookup(msvc_version, env)
is_target = False
if vernum_int >= 141:
# VS2017 and later
is_supported = True
elif vernum_int == 140:
# VS2015:
# True: Develop, CmdLine
# Maybe: Express
# False: BTDispatch
is_supported = True
if kind_t.kind == VCVER_KIND_EXPRESS:
uwp_is_supported = kind_t.extended.get('uwp_is_supported', {})
is_supported = uwp_is_supported.get(target_arch, True)
is_target = True
elif kind_t.kind == VCVER_KIND_BTDISPATCH:
is_supported = False
else:
# VS2013 and earlier
is_supported = False
debug(
'is_supported=%s, is_target=%s, msvc_version=%s, kind=%s, target_arch=%s',
is_supported, is_target, repr(msvc_version), repr(VCVER_KIND_STR[kind_t.kind]), repr(target_arch)
)
return is_supported, is_target
# reset cache
def reset() -> None:
global _cache_vcver_kind_map
global _cache_pdir_vswhere_kind
global _cache_pdir_registry_kind
global _cache_pdir_registry_winsdk
debug('')
_cache_vcver_kind_map = {}
_cache_pdir_vswhere_kind = {}
_cache_pdir_registry_kind = {}
_cache_pdir_registry_winsdk = {}

View file

@ -38,6 +38,10 @@ from collections import (
namedtuple, namedtuple,
) )
from contextlib import (
contextmanager,
)
import SCons.Warnings import SCons.Warnings
from ..common import ( from ..common import (
@ -215,6 +219,18 @@ def msvc_notfound_handler(env, msg):
else: else:
SCons.Warnings.warn(SCons.Warnings.VisualCMissingWarning, msg) SCons.Warnings.warn(SCons.Warnings.VisualCMissingWarning, msg)
@contextmanager
def msvc_notfound_policy_contextmanager(MSVC_NOTFOUND_POLICY=None):
""" Temporarily change the MSVC not found policy within a context.
Args:
MSVC_NOTFOUND_POLICY:
string representing the policy behavior
when MSVC is not found or None
"""
prev_policy = msvc_set_notfound_policy(MSVC_NOTFOUND_POLICY)
yield
msvc_set_notfound_policy(prev_policy)
def _msvc_scripterror_policy_lookup(symbol): def _msvc_scripterror_policy_lookup(symbol):
@ -299,3 +315,16 @@ def msvc_scripterror_handler(env, msg):
else: else:
SCons.Warnings.warn(MSVCScriptExecutionWarning, msg) SCons.Warnings.warn(MSVCScriptExecutionWarning, msg)
@contextmanager
def msvc_scripterror_policy_contextmanager(MSVC_SCRIPTERROR_POLICY=None):
""" Temporarily change the msvc batch execution errors policy within a context.
Args:
MSVC_SCRIPTERROR_POLICY:
string representing the policy behavior
when msvc batch file execution errors are detected or None
"""
prev_policy = msvc_set_scripterror_policy(MSVC_SCRIPTERROR_POLICY)
yield
msvc_set_scripterror_policy(prev_policy)

View file

@ -110,6 +110,9 @@ def windows_kit_query_paths(version):
q = windows_kits(version) q = windows_kits(version)
return microsoft_query_paths(q) return microsoft_query_paths(q)
def vstudio_sxs_vs7(version):
return '\\'.join([r'VisualStudio\SxS\VS7', version])
def vstudio_sxs_vc7(version): def vstudio_sxs_vc7(version):
return '\\'.join([r'VisualStudio\SxS\VC7', version]) return '\\'.join([r'VisualStudio\SxS\VC7', version])

View file

@ -42,6 +42,7 @@ from . import Util
from . import Config from . import Config
from . import Registry from . import Registry
from . import WinSDK from . import WinSDK
from . import Kind
from .Exceptions import ( from .Exceptions import (
MSVCInternalError, MSVCInternalError,
@ -172,6 +173,12 @@ MSVC_VERSION_ARGS_DEFINITION = namedtuple('MSVCVersionArgsDefinition', [
'vs_def', 'vs_def',
]) ])
TOOLSET_VERSION_ARGS_DEFINITION = namedtuple('ToolsetVersionArgsDefinition', [
'version', # full version (e.g., '14.1Exp', '14.32.31326')
'vc_buildtools_def',
'is_user',
])
def _msvc_version(version): def _msvc_version(version):
verstr = Util.get_msvc_version_prefix(version) verstr = Util.get_msvc_version_prefix(version)
@ -184,19 +191,22 @@ def _msvc_version(version):
return version_args return version_args
def _toolset_version(version): def _toolset_version(version, is_user=False):
verstr = Util.get_msvc_version_prefix(version) vc_series = Util.get_msvc_version_prefix(version)
vs_def = Config.MSVC_VERSION_INTERNAL[verstr]
version_args = MSVC_VERSION_ARGS_DEFINITION( vc_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL[vc_series]
vc_buildtools_def = Config.VC_BUILDTOOLS_MAP[vc_buildseries_def.vc_buildseries]
version_args = TOOLSET_VERSION_ARGS_DEFINITION(
version = version, version = version,
vs_def = vs_def, vc_buildtools_def = vc_buildtools_def,
is_user = is_user,
) )
return version_args return version_args
def _msvc_script_argument_uwp(env, msvc, arglist): def _msvc_script_argument_uwp(env, msvc, arglist, target_arch):
uwp_app = env['MSVC_UWP_APP'] uwp_app = env['MSVC_UWP_APP']
debug('MSVC_VERSION=%s, MSVC_UWP_APP=%s', repr(msvc.version), repr(uwp_app)) debug('MSVC_VERSION=%s, MSVC_UWP_APP=%s', repr(msvc.version), repr(uwp_app))
@ -207,17 +217,34 @@ def _msvc_script_argument_uwp(env, msvc, arglist):
if uwp_app not in _ARGUMENT_BOOLEAN_TRUE_LEGACY: if uwp_app not in _ARGUMENT_BOOLEAN_TRUE_LEGACY:
return None return None
if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric: if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric:
debug( debug(
'invalid: msvc version constraint: %s < %s VS2015', 'invalid: msvc version constraint: %s < %s VS2015',
repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
repr(VS2015.vc_buildtools_def.vc_version_numeric) repr(VS2015.vc_buildtools_def.msvc_version_numeric)
) )
err_msg = "MSVC_UWP_APP ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format( err_msg = "MSVC_UWP_APP ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format(
repr(uwp_app), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version) repr(uwp_app), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
) )
raise MSVCArgumentError(err_msg) raise MSVCArgumentError(err_msg)
is_supported, is_target = Kind.msvc_version_uwp_is_supported(msvc.version, target_arch, env)
if not is_supported:
_, kind_str = Kind.get_msvc_version_kind(msvc.version)
debug(
'invalid: msvc_version constraint: %s %s %s',
repr(msvc.version), repr(kind_str), repr(target_arch)
)
if is_target and target_arch:
err_msg = "MSVC_UWP_APP ({}) TARGET_ARCH ({}) is not supported for MSVC_VERSION {} ({})".format(
repr(uwp_app), repr(target_arch), repr(msvc.version), repr(kind_str)
)
else:
err_msg = "MSVC_UWP_APP ({}) is not supported for MSVC_VERSION {} ({})".format(
repr(uwp_app), repr(msvc.version), repr(kind_str)
)
raise MSVCArgumentError(err_msg)
# VS2017+ rewrites uwp => store for 14.0 toolset # VS2017+ rewrites uwp => store for 14.0 toolset
uwp_arg = msvc.vs_def.vc_uwp uwp_arg = msvc.vs_def.vc_uwp
@ -250,16 +277,24 @@ def _user_script_argument_uwp(env, uwp, user_argstr) -> bool:
raise MSVCArgumentError(err_msg) raise MSVCArgumentError(err_msg)
def _msvc_script_argument_sdk_constraints(msvc, sdk_version): def _msvc_script_argument_sdk_constraints(msvc, sdk_version, env):
if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric: if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric:
debug( debug(
'invalid: msvc_version constraint: %s < %s VS2015', 'invalid: msvc_version constraint: %s < %s VS2015',
repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
repr(VS2015.vc_buildtools_def.vc_version_numeric) repr(VS2015.vc_buildtools_def.msvc_version_numeric)
) )
err_msg = "MSVC_SDK_VERSION ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format( err_msg = "MSVC_SDK_VERSION ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format(
repr(sdk_version), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version) repr(sdk_version), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
)
return err_msg
if not Kind.msvc_version_sdk_version_is_supported(msvc.version, env):
_, kind_str = Kind.get_msvc_version_kind(msvc.version)
debug('invalid: msvc_version constraint: %s %s', repr(msvc.version), repr(kind_str))
err_msg = "MSVC_SDK_VERSION ({}) is not supported for MSVC_VERSION {} ({})".format(
repr(sdk_version), repr(msvc.version), repr(kind_str)
) )
return err_msg return err_msg
@ -277,23 +312,23 @@ def _msvc_script_argument_sdk_platform_constraints(msvc, toolset, sdk_version, p
if sdk_version == '8.1' and platform_def.is_uwp: if sdk_version == '8.1' and platform_def.is_uwp:
vs_def = toolset.vs_def if toolset else msvc.vs_def vc_buildtools_def = toolset.vc_buildtools_def if toolset else msvc.vs_def.vc_buildtools_def
if vs_def.vc_buildtools_def.vc_version_numeric > VS2015.vc_buildtools_def.vc_version_numeric: if vc_buildtools_def.msvc_version_numeric > VS2015.vc_buildtools_def.msvc_version_numeric:
debug( debug(
'invalid: uwp/store SDK 8.1 msvc_version constraint: %s > %s VS2015', 'invalid: uwp/store SDK 8.1 msvc_version constraint: %s > %s VS2015',
repr(vs_def.vc_buildtools_def.vc_version_numeric), repr(vc_buildtools_def.msvc_version_numeric),
repr(VS2015.vc_buildtools_def.vc_version_numeric) repr(VS2015.vc_buildtools_def.msvc_version_numeric)
) )
if toolset and toolset.vs_def != msvc.vs_def: if toolset and toolset.is_user:
err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: toolset version {} > {} VS2015".format( err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: toolset {} MSVC_VERSION {} > {} VS2015".format(
repr(sdk_version), repr(platform_def.vc_platform), repr(sdk_version), repr(platform_def.vc_platform),
repr(toolset.version), repr(VS2015.vc_buildtools_def.vc_version) repr(toolset.version), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
) )
else: else:
err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: MSVC_VERSION {} > {} VS2015".format( err_msg = "MSVC_SDK_VERSION ({}) and platform type ({}) constraint violation: MSVC_VERSION {} > {} VS2015".format(
repr(sdk_version), repr(platform_def.vc_platform), repr(sdk_version), repr(platform_def.vc_platform),
repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version) repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
) )
return err_msg return err_msg
@ -310,7 +345,7 @@ def _msvc_script_argument_sdk(env, msvc, toolset, platform_def, arglist):
if not sdk_version: if not sdk_version:
return None return None
err_msg = _msvc_script_argument_sdk_constraints(msvc, sdk_version) err_msg = _msvc_script_argument_sdk_constraints(msvc, sdk_version, env)
if err_msg: if err_msg:
raise MSVCArgumentError(err_msg) raise MSVCArgumentError(err_msg)
@ -333,7 +368,10 @@ def _msvc_script_argument_sdk(env, msvc, toolset, platform_def, arglist):
def _msvc_script_default_sdk(env, msvc, platform_def, arglist, force_sdk: bool=False): def _msvc_script_default_sdk(env, msvc, platform_def, arglist, force_sdk: bool=False):
if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric: if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric:
return None
if not Kind.msvc_version_sdk_version_is_supported(msvc.version, env):
return None return None
sdk_list = WinSDK.get_sdk_version_list(msvc.vs_def, platform_def) sdk_list = WinSDK.get_sdk_version_list(msvc.vs_def, platform_def)
@ -415,10 +453,11 @@ def _msvc_sxs_toolset_folder(msvc, sxs_folder):
if Util.is_toolset_sxs(sxs_folder): if Util.is_toolset_sxs(sxs_folder):
return sxs_folder, sxs_folder return sxs_folder, sxs_folder
key = (msvc.vs_def.vc_buildtools_def.vc_version, sxs_folder) for vc_buildseries_def in msvc.vs_def.vc_buildtools_def.vc_buildseries_list:
if key in _msvc_sxs_bugfix_folder: key = (vc_buildseries_def.vc_version, sxs_folder)
sxs_version = _msvc_sxs_bugfix_folder[key] sxs_version = _msvc_sxs_bugfix_folder.get(key)
return sxs_folder, sxs_version if sxs_version:
return sxs_folder, sxs_version
debug('sxs folder: ignore version=%s', repr(sxs_folder)) debug('sxs folder: ignore version=%s', repr(sxs_folder))
return None, None return None, None
@ -574,49 +613,60 @@ def _msvc_version_toolset_vcvars(msvc, vc_dir, toolset_version):
def _msvc_script_argument_toolset_constraints(msvc, toolset_version): def _msvc_script_argument_toolset_constraints(msvc, toolset_version):
if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric: if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric:
debug( debug(
'invalid: msvc version constraint: %s < %s VS2017', 'invalid: msvc version constraint: %s < %s VS2017',
repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
repr(VS2017.vc_buildtools_def.vc_version_numeric) repr(VS2017.vc_buildtools_def.msvc_version_numeric)
) )
err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: MSVC_VERSION {} < {} VS2017".format( err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: MSVC_VERSION {} < {} VS2017".format(
repr(toolset_version), repr(msvc.version), repr(VS2017.vc_buildtools_def.vc_version) repr(toolset_version), repr(msvc.version), repr(VS2017.vc_buildtools_def.msvc_version)
) )
return err_msg return err_msg
toolset_verstr = Util.get_msvc_version_prefix(toolset_version) toolset_series = Util.get_msvc_version_prefix(toolset_version)
if not toolset_verstr: if not toolset_series:
debug('invalid: msvc version: toolset_version=%s', repr(toolset_version)) debug('invalid: msvc version: toolset_version=%s', repr(toolset_version))
err_msg = 'MSVC_TOOLSET_VERSION {} format is not supported'.format( err_msg = 'MSVC_TOOLSET_VERSION {} format is not supported'.format(
repr(toolset_version) repr(toolset_version)
) )
return err_msg return err_msg
toolset_vernum = float(toolset_verstr) toolset_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL.get(toolset_series)
if not toolset_buildseries_def:
if toolset_vernum < VS2015.vc_buildtools_def.vc_version_numeric: debug('invalid: msvc version: toolset_version=%s', repr(toolset_version))
debug( err_msg = 'MSVC_TOOLSET_VERSION {} build series {} is not supported'.format(
'invalid: toolset version constraint: %s < %s VS2015', repr(toolset_version), repr(toolset_series)
repr(toolset_vernum), repr(VS2015.vc_buildtools_def.vc_version_numeric)
)
err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} < {} VS2015".format(
repr(toolset_version), repr(toolset_verstr), repr(VS2015.vc_buildtools_def.vc_version)
) )
return err_msg return err_msg
if toolset_vernum > msvc.vs_def.vc_buildtools_def.vc_version_numeric: toolset_buildtools_def = Config.VC_BUILDTOOLS_MAP[toolset_buildseries_def.vc_buildseries]
toolset_verstr = toolset_buildtools_def.msvc_version
toolset_vernum = toolset_buildtools_def.msvc_version_numeric
if toolset_vernum < VS2015.vc_buildtools_def.msvc_version_numeric:
debug(
'invalid: toolset version constraint: %s < %s VS2015',
repr(toolset_vernum), repr(VS2015.vc_buildtools_def.msvc_version_numeric)
)
err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} < {} VS2015".format(
repr(toolset_version), repr(toolset_verstr), repr(VS2015.vc_buildtools_def.msvc_version)
)
return err_msg
if toolset_vernum > msvc.vs_def.vc_buildtools_def.msvc_version_numeric:
debug( debug(
'invalid: toolset version constraint: toolset %s > %s msvc', 'invalid: toolset version constraint: toolset %s > %s msvc',
repr(toolset_vernum), repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric) repr(toolset_vernum), repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric)
) )
err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} > {} MSVC_VERSION".format( err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} > {} MSVC_VERSION".format(
repr(toolset_version), repr(toolset_verstr), repr(msvc.version) repr(toolset_version), repr(toolset_verstr), repr(msvc.version)
) )
return err_msg return err_msg
if toolset_vernum == VS2015.vc_buildtools_def.vc_version_numeric: if toolset_vernum == VS2015.vc_buildtools_def.msvc_version_numeric:
# tooset = 14.0 # tooset = 14.0
if Util.is_toolset_full(toolset_version): if Util.is_toolset_full(toolset_version):
if not Util.is_toolset_140(toolset_version): if not Util.is_toolset_140(toolset_version):
@ -624,7 +674,7 @@ def _msvc_script_argument_toolset_constraints(msvc, toolset_version):
'invalid: toolset version 14.0 constraint: %s != 14.0', 'invalid: toolset version 14.0 constraint: %s != 14.0',
repr(toolset_version) repr(toolset_version)
) )
err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset version {} != '14.0'".format( err_msg = "MSVC_TOOLSET_VERSION ({}) constraint violation: toolset msvc version {} != '14.0'".format(
repr(toolset_version), repr(toolset_version) repr(toolset_version), repr(toolset_version)
) )
return err_msg return err_msg
@ -688,7 +738,7 @@ def _msvc_script_argument_toolset(env, msvc, vc_dir, arglist):
def _msvc_script_default_toolset(env, msvc, vc_dir, arglist, force_toolset: bool=False): def _msvc_script_default_toolset(env, msvc, vc_dir, arglist, force_toolset: bool=False):
if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric: if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric:
return None return None
toolset_default = _msvc_default_toolset(msvc, vc_dir) toolset_default = _msvc_default_toolset(msvc, vc_dir)
@ -729,26 +779,26 @@ def _user_script_argument_toolset(env, toolset_version, user_argstr):
def _msvc_script_argument_spectre_constraints(msvc, toolset, spectre_libs, platform_def): def _msvc_script_argument_spectre_constraints(msvc, toolset, spectre_libs, platform_def):
if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric: if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric:
debug( debug(
'invalid: msvc version constraint: %s < %s VS2017', 'invalid: msvc version constraint: %s < %s VS2017',
repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
repr(VS2017.vc_buildtools_def.vc_version_numeric) repr(VS2017.vc_buildtools_def.msvc_version_numeric)
) )
err_msg = "MSVC_SPECTRE_LIBS ({}) constraint violation: MSVC_VERSION {} < {} VS2017".format( err_msg = "MSVC_SPECTRE_LIBS ({}) constraint violation: MSVC_VERSION {} < {} VS2017".format(
repr(spectre_libs), repr(msvc.version), repr(VS2017.vc_buildtools_def.vc_version) repr(spectre_libs), repr(msvc.version), repr(VS2017.vc_buildtools_def.msvc_version)
) )
return err_msg return err_msg
if toolset: if toolset:
if toolset.vs_def.vc_buildtools_def.vc_version_numeric < VS2017.vc_buildtools_def.vc_version_numeric: if toolset.vc_buildtools_def.msvc_version_numeric < VS2017.vc_buildtools_def.msvc_version_numeric:
debug( debug(
'invalid: toolset version constraint: %s < %s VS2017', 'invalid: toolset version constraint: %s < %s VS2017',
repr(toolset.vs_def.vc_buildtools_def.vc_version_numeric), repr(toolset.vc_buildtools_def.msvc_version_numeric),
repr(VS2017.vc_buildtools_def.vc_version_numeric) repr(VS2017.vc_buildtools_def.msvc_version_numeric)
) )
err_msg = "MSVC_SPECTRE_LIBS ({}) constraint violation: toolset version {} < {} VS2017".format( err_msg = "MSVC_SPECTRE_LIBS ({}) constraint violation: toolset version {} < {} VS2017".format(
repr(spectre_libs), repr(toolset.version), repr(VS2017.vc_buildtools_def.vc_version) repr(spectre_libs), repr(toolset.version), repr(VS2017.vc_buildtools_def.msvc_version)
) )
return err_msg return err_msg
@ -836,14 +886,14 @@ def _msvc_script_argument_user(env, msvc, arglist):
if not script_args: if not script_args:
return None return None
if msvc.vs_def.vc_buildtools_def.vc_version_numeric < VS2015.vc_buildtools_def.vc_version_numeric: if msvc.vs_def.vc_buildtools_def.msvc_version_numeric < VS2015.vc_buildtools_def.msvc_version_numeric:
debug( debug(
'invalid: msvc version constraint: %s < %s VS2015', 'invalid: msvc version constraint: %s < %s VS2015',
repr(msvc.vs_def.vc_buildtools_def.vc_version_numeric), repr(msvc.vs_def.vc_buildtools_def.msvc_version_numeric),
repr(VS2015.vc_buildtools_def.vc_version_numeric) repr(VS2015.vc_buildtools_def.msvc_version_numeric)
) )
err_msg = "MSVC_SCRIPT_ARGS ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format( err_msg = "MSVC_SCRIPT_ARGS ({}) constraint violation: MSVC_VERSION {} < {} VS2015".format(
repr(script_args), repr(msvc.version), repr(VS2015.vc_buildtools_def.vc_version) repr(script_args), repr(msvc.version), repr(VS2015.vc_buildtools_def.msvc_version)
) )
raise MSVCArgumentError(err_msg) raise MSVCArgumentError(err_msg)
@ -873,7 +923,18 @@ def _msvc_process_construction_variables(env) -> bool:
return False return False
def msvc_script_arguments(env, version, vc_dir, arg): def msvc_script_arguments_has_uwp(env):
if not _msvc_process_construction_variables(env):
return False
uwp_app = env.get('MSVC_UWP_APP')
is_uwp = bool(uwp_app and uwp_app in _ARGUMENT_BOOLEAN_TRUE_LEGACY)
debug('is_uwp=%s', is_uwp)
return is_uwp
def msvc_script_arguments(env, version, vc_dir, arg=None):
arguments = [arg] if arg else [] arguments = [arg] if arg else []
@ -889,10 +950,12 @@ def msvc_script_arguments(env, version, vc_dir, arg):
if _msvc_process_construction_variables(env): if _msvc_process_construction_variables(env):
target_arch = env.get('TARGET_ARCH')
# MSVC_UWP_APP # MSVC_UWP_APP
if 'MSVC_UWP_APP' in env: if 'MSVC_UWP_APP' in env:
uwp = _msvc_script_argument_uwp(env, msvc, arglist) uwp = _msvc_script_argument_uwp(env, msvc, arglist, target_arch)
else: else:
uwp = None uwp = None
@ -926,7 +989,7 @@ def msvc_script_arguments(env, version, vc_dir, arg):
if user_toolset: if user_toolset:
toolset = None toolset = None
elif toolset_version: elif toolset_version:
toolset = _toolset_version(toolset_version) toolset = _toolset_version(toolset_version, is_user=True)
elif default_toolset: elif default_toolset:
toolset = _toolset_version(default_toolset) toolset = _toolset_version(default_toolset)
else: else:
@ -958,7 +1021,7 @@ def msvc_script_arguments(env, version, vc_dir, arg):
if user_argstr: if user_argstr:
_user_script_argument_spectre(env, spectre, user_argstr) _user_script_argument_spectre(env, spectre, user_argstr)
if msvc.vs_def.vc_buildtools_def.vc_version == '14.0': if msvc.vs_def.vc_buildtools_def.msvc_version == '14.0':
if user_uwp and sdk_version and len(arglist) == 2: if user_uwp and sdk_version and len(arglist) == 2:
# VS2015 toolset argument order issue: SDK store => store SDK # VS2015 toolset argument order issue: SDK store => store SDK
arglist_reverse = True arglist_reverse = True
@ -1003,6 +1066,14 @@ def _msvc_toolset_versions_internal(msvc_version, vc_dir, full: bool=True, sxs:
return toolset_versions return toolset_versions
def _msvc_version_toolsets_internal(msvc_version, vc_dir):
msvc = _msvc_version(msvc_version)
toolsets_sxs, toolsets_full = _msvc_version_toolsets(msvc, vc_dir)
return toolsets_sxs, toolsets_full
def _msvc_toolset_versions_spectre_internal(msvc_version, vc_dir): def _msvc_toolset_versions_spectre_internal(msvc_version, vc_dir):
msvc = _msvc_version(msvc_version) msvc = _msvc_version(msvc_version)

View file

@ -37,6 +37,15 @@ from ..common import debug
from . import Config from . import Config
# call _initialize method upon class definition completion
class AutoInitialize:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if hasattr(cls, '_initialize') and callable(getattr(cls, '_initialize', None)):
cls._initialize()
# path utilities # path utilities
# windows drive specification (e.g., 'C:') # windows drive specification (e.g., 'C:')
@ -234,6 +243,26 @@ def get_msvc_version_prefix(version):
rval = m.group('version') rval = m.group('version')
return rval return rval
def get_msvc_version_prefix_suffix(version):
"""
Get the msvc version number prefix and suffix from a string.
Args:
version: str
version specification
Returns:
(str, str): the msvc version prefix and suffix
"""
prefix = suffix = ''
if version:
m = re_msvc_version.match(version)
if m:
prefix = m.group('msvc_version')
suffix = m.group('suffix') if m.group('suffix') else ''
return prefix, suffix
# toolset version query utilities # toolset version query utilities
def is_toolset_full(toolset_version) -> bool: def is_toolset_full(toolset_version) -> bool:
@ -322,15 +351,21 @@ def msvc_version_components(vcver):
return msvc_version_components_def return msvc_version_components_def
_MSVC_EXTENDED_VERSION_COMPONENTS_DEFINITION = namedtuple('MSVCExtendedVersionComponentsDefinition', [ _MSVC_EXTENDED_VERSION_COMPONENTS_DEFINITION = namedtuple('MSVCExtendedVersionComponentsDefinition', [
'msvc_version', # msvc version (e.g., '14.1Exp') 'msvc_version', # msvc version (e.g., '14.1Exp')
'msvc_verstr', # msvc version numeric string (e.g., '14.1') 'msvc_verstr', # msvc version numeric string (e.g., '14.1')
'msvc_suffix', # msvc version component type (e.g., 'Exp') 'msvc_suffix', # msvc version component type (e.g., 'Exp')
'msvc_vernum', # msvc version floating point number (e.g, 14.1) 'msvc_suffix_rank', # msvc version component rank (0, 1)
'msvc_major', # msvc major version integer number (e.g., 14) 'msvc_vernum', # msvc version floating point number (e.g, 14.1)
'msvc_minor', # msvc minor version integer number (e.g., 1) 'msvc_major', # msvc major version integer number (e.g., 14)
'msvc_comps', # msvc version components tuple (e.g., ('14', '1')) 'msvc_minor', # msvc minor version integer number (e.g., 1)
'msvc_comps', # msvc version components tuple (e.g., ('14', '1'))
'msvc_buildtools', # msvc build tools
'msvc_buildtools_num', # msvc build tools integer number
'msvc_buildseries', # msvc build series
'msvc_buildseries_num', # msvc build series floating point number
'msvc_toolset_version', # msvc toolset version 'msvc_toolset_version', # msvc toolset version
'msvc_toolset_comps', # msvc toolset version components 'msvc_toolset_comps', # msvc toolset version components
'msvc_toolset_is_sxs', # msvc toolset version is sxs
'version', # msvc version or msvc toolset version 'version', # msvc version or msvc toolset version
]) ])
@ -355,11 +390,19 @@ def msvc_extended_version_components(version):
msvc_toolset_version = m.group('version') msvc_toolset_version = m.group('version')
msvc_toolset_comps = tuple(msvc_toolset_version.split('.')) msvc_toolset_comps = tuple(msvc_toolset_version.split('.'))
msvc_toolset_is_sxs = is_toolset_sxs(msvc_toolset_version)
msvc_verstr = get_msvc_version_prefix(msvc_toolset_version) vc_verstr = get_msvc_version_prefix(msvc_toolset_version)
if not msvc_verstr: if not vc_verstr:
return None return None
vc_buildseries_def = Config.MSVC_BUILDSERIES_EXTERNAL.get(vc_verstr)
if not vc_buildseries_def:
return None
vc_buildtools_def = Config.VC_BUILDTOOLS_MAP[vc_buildseries_def.vc_buildseries]
msvc_verstr = vc_buildtools_def.msvc_version
msvc_suffix = m.group('suffix') if m.group('suffix') else '' msvc_suffix = m.group('suffix') if m.group('suffix') else ''
msvc_version = msvc_verstr + msvc_suffix msvc_version = msvc_verstr + msvc_suffix
@ -376,12 +419,18 @@ def msvc_extended_version_components(version):
msvc_version = msvc_version, msvc_version = msvc_version,
msvc_verstr = msvc_verstr, msvc_verstr = msvc_verstr,
msvc_suffix = msvc_suffix, msvc_suffix = msvc_suffix,
msvc_suffix_rank = 0 if not msvc_suffix else 1,
msvc_vernum = msvc_vernum, msvc_vernum = msvc_vernum,
msvc_major = msvc_major, msvc_major = msvc_major,
msvc_minor = msvc_minor, msvc_minor = msvc_minor,
msvc_comps = msvc_comps, msvc_comps = msvc_comps,
msvc_buildtools = vc_buildtools_def.msvc_version,
msvc_buildtools_num = vc_buildtools_def.msvc_version_numeric,
msvc_buildseries = vc_buildseries_def.vc_version,
msvc_buildseries_num = vc_buildseries_def.vc_version_numeric,
msvc_toolset_version = msvc_toolset_version, msvc_toolset_version = msvc_toolset_version,
msvc_toolset_comps = msvc_toolset_comps, msvc_toolset_comps = msvc_toolset_comps,
msvc_toolset_is_sxs = msvc_toolset_is_sxs,
version = version, version = version,
) )

View file

@ -40,6 +40,7 @@ from . import Exceptions # noqa: F401
from . import Config # noqa: F401 from . import Config # noqa: F401
from . import Util # noqa: F401 from . import Util # noqa: F401
from . import Registry # noqa: F401 from . import Registry # noqa: F401
from . import Kind # noqa: F401
from . import SetupEnvDefault # noqa: F401 from . import SetupEnvDefault # noqa: F401
from . import Policy # noqa: F401 from . import Policy # noqa: F401
from . import WinSDK # noqa: F401 from . import WinSDK # noqa: F401

View file

@ -27,16 +27,176 @@ Design Notes
``MSCommon/vc.py`` and are available via the ``SCons.Tool.MSCommon`` namespace. ``MSCommon/vc.py`` and are available via the ``SCons.Tool.MSCommon`` namespace.
MSVC Detection Priority
=======================
For msvc version specifications without an 'Exp' suffix, an express
installation is used only when no other installation is detected.
+---------+---------+----------------------------------------------------------+
| Product | VCVer | Priority |
+=========+=========+==========================================================+
| VS2022 | 14.3 | Enterprise, Professional, Community, BuildTools |
+---------+---------+----------------------------------------------------------+
| VS2019 | 14.2 | Enterprise, Professional, Community, BuildTools |
+---------+---------+----------------------------------------------------------+
| VS2017 | 14.1 | Enterprise, Professional, Community, BuildTools, Express |
+---------+---------+----------------------------------------------------------+
| VS2017 | 14.1Exp | Express |
+---------+---------+----------------------------------------------------------+
| VS2015 | 14.0 | [Develop, BuildTools, CmdLine], Express |
+---------+---------+----------------------------------------------------------+
| VS2015 | 14.0Exp | Express |
+---------+---------+----------------------------------------------------------+
| VS2013 | 12.0 | Develop, Express |
+---------+---------+----------------------------------------------------------+
| VS2013 | 12.0Exp | Express |
+---------+---------+----------------------------------------------------------+
| VS2012 | 11.0 | Develop, Express |
+---------+---------+----------------------------------------------------------+
| VS2012 | 11.0Exp | Express |
+---------+---------+----------------------------------------------------------+
| VS2010 | 10.0 | Develop, Express |
+---------+---------+----------------------------------------------------------+
| VS2010 | 10.0Exp | Express |
+---------+---------+----------------------------------------------------------+
| VS2008 | 9.0 | Develop, VCForPython, Express |
+---------+---------+----------------------------------------------------------+
| VS2008 | 9.0Exp | Express |
+---------+---------+----------------------------------------------------------+
| VS2005 | 8.0 | Develop, Express |
+---------+---------+----------------------------------------------------------+
| VS2005 | 8.0Exp | Express |
+---------+---------+----------------------------------------------------------+
| VS2003 | 7.1 | Develop |
+---------+---------+----------------------------------------------------------+
| VS2002 | 7.0 | Develop |
+---------+---------+----------------------------------------------------------+
| VS6.0 | 6.0 | Develop |
+---------+---------+----------------------------------------------------------+
Legend:
Develop
devenv.com or msdev.com is detected.
Express
WDExpress.exe or VCExpress.exe is detected.
BuildTools [VS2015]
The vcvarsall batch file dispatches to the buildtools batch file.
CmdLine [VS2015]
Neither Develop, Express, or BuildTools.
VS2015 Edition Limitations
==========================
VS2015 BuildTools
-----------------
The VS2015 BuildTools stand-alone batch file does not support the ``sdk version`` argument.
The VS2015 BuildTools stand-alone batch file does not support the ``store`` argument.
These arguments appear to be silently ignored and likely would result in compiler
and/or linker build failures.
The VS2015 BuildTools ``vcvarsall.bat`` batch file dispatches to the stand-alone buildtools
batch file under certain circumstances. A fragment from the vcvarsall batch file is:
::
if exist "%~dp0..\common7\IDE\devenv.exe" goto setup_VS
if exist "%~dp0..\common7\IDE\wdexpress.exe" goto setup_VS
if exist "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" goto setup_buildsku
:setup_VS
...
:setup_buildsku
if not exist "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" goto usage
set CurrentDir=%CD%
call "%~dp0..\..\Microsoft Visual C++ Build Tools\vcbuildtools.bat" %1 %2
cd /d %CurrentDir%
goto :eof
VS2015 Express
--------------
The VS2015 Express batch file does not support the ``sdk version`` argument.
The VS2015 Express batch file does not support the ``store`` argument for the ``amd64`` and
``arm`` target architectures
amd64 Target Architecture
^^^^^^^^^^^^^^^^^^^^^^^^^
As installed, VS2015 Express does not support the ``store`` argument for the ``amd64`` target
architecture. The generated ``store`` library paths include directories that do not exist.
The store library paths appear in two places in the ``vcvarsx86_amd64`` batch file:
::
:setstorelib
@if exist "%VCINSTALLDIR%LIB\amd64\store" set LIB=%VCINSTALLDIR%LIB\amd64\store;%LIB%
...
:setstorelibpath
@if exist "%VCINSTALLDIR%LIB\amd64\store" set LIBPATH=%VCINSTALLDIR%LIB\amd64\store;%LIBPATH%
The correct store library paths would be:
::
:setstorelib
@if exist "%VCINSTALLDIR%LIB\store\amd64" set LIB=%VCINSTALLDIR%LIB\store\amd64;%LIB%
...
:setstorelibpath
@if exist "%VCINSTALLDIR%LIB\store\amd64" set LIBPATH=%VCINSTALLDIR%LIB\store\amd64;%LIBPATH%
arm Target Architecture
^^^^^^^^^^^^^^^^^^^^^^^
As installed, VS2015 Express does not support the ``store`` argument for the ``arm`` target
architecture. The generated ``store`` library paths include directories that do not exist.
The store library paths appear in two places in the ``vcvarsx86_arm`` batch file:
::
:setstorelib
@if exist "%VCINSTALLDIR%LIB\ARM\store" set LIB=%VCINSTALLDIR%LIB\ARM\store;%LIB%
...
:setstorelibpath
@if exist "%VCINSTALLDIR%LIB\ARM\store" set LIBPATH=%VCINSTALLDIR%LIB\ARM\store;%LIBPATH%
The correct store library paths would be file:
::
:setstorelib
@if exist "%VCINSTALLDIR%LIB\store\ARM" set LIB=%VCINSTALLDIR%LIB\store\ARM;%LIB%
...
:setstorelibpath
@if exist "%VCINSTALLDIR%LIB\store\ARM" set LIBPATH=%VCINSTALLDIR%LIB\store\ARM;%LIBPATH%
Known Issues Known Issues
============ ============
The following issues are known to exist: The following issues are known to exist:
* Using ``MSVC_USE_SCRIPT`` and ``MSVC_USE_SCRIPT_ARGS`` to call older Microsoft SDK * Using ``MSVC_USE_SCRIPT`` and ``MSVC_USE_SCRIPT_ARGS`` to call older Microsoft SDK
``SetEnv.cmd`` batch files may result in build failures. Some of these batch files ``SetEnv.cmd`` batch files may result in build failures.
require delayed expansion to be enabled which is not usually the Windows default.
One solution would be to launch the MSVC batch file command in a new command interpreter Typically, the reasons for build failures with SDK batch files are one, or both, of:
instance with delayed expansion enabled via command-line options.
* The batch files require delayed expansion to be enabled which is not usually the Windows default.
* The batch files inspect environment variables that are not defined in the minimal subprocess
environment in which the batch files are invoked.
* The code to suppress the "No versions of the MSVC compiler were found" warning for * The code to suppress the "No versions of the MSVC compiler were found" warning for
the default environment was moved from ``MSCommon/vc.py`` to ``MSCommon/MSVC/SetupEnvDefault.py``. the default environment was moved from ``MSCommon/vc.py`` to ``MSCommon/MSVC/SetupEnvDefault.py``.
@ -47,20 +207,24 @@ The following issues are known to exist:
Experimental Features Experimental Features
===================== =====================
msvc_query_version_toolset(version=None, prefer_newest=True) msvc_query_version_toolset(version=None, prefer_newest=True, vswhere_exe=None)
------------------------------------------------------------ ------------------------------------------------------------------------------
The experimental function ``msvc_query_version_toolset`` was added to ``MSCommon/vc.py`` The experimental function ``msvc_query_version_toolset`` was added to ``MSCommon/vc.py``
and is available via the ``SCons.Tool.MSCommon`` namespace. This function takes a version and is available via the ``SCons.Tool.MSCommon`` namespace.
specification or a toolset version specification and a product preference as arguments and
returns the msvc version and the msvc toolset version for the corresponding version specification. This function takes a version specification or a toolset version specification, an optional product
preference, and an optional vswhere executable location as arguments and returns the msvc version and
the msvc toolset version for the corresponding version specification.
This is a proxy for using the toolset version for selection until that functionality can be added. This is a proxy for using the toolset version for selection until that functionality can be added.
Example usage: Example usage:
:: ::
for version in [ for version in [
'14.4',
'14.3', '14.3',
'14.2', '14.2',
'14.1', '14.1',
@ -90,6 +254,7 @@ Example usage:
print('{}Query: {} version={}, prefer_newest={}'.format(newline, msg, version, prefer_newest)) print('{}Query: {} version={}, prefer_newest={}'.format(newline, msg, version, prefer_newest))
Example output fragment Example output fragment
:: ::
Build: _build003 {'MSVC_VERSION': '14.3', 'MSVC_TOOLSET_VERSION': '14.29.30133'} Build: _build003 {'MSVC_VERSION': '14.3', 'MSVC_TOOLSET_VERSION': '14.29.30133'}
@ -117,6 +282,7 @@ added to the batch file argument list. This is intended to make the cache more
updates that may change the default toolset version and/or the default SDK version. updates that may change the default toolset version and/or the default SDK version.
Example usage: Example usage:
:: ::
@echo Enabling scons cache ... @echo Enabling scons cache ...
@ -139,6 +305,7 @@ Enabling warnings to be produced for detected msvc batch file errors may provide
for build failures. Refer to the documentation for details. for build failures. Refer to the documentation for details.
Change the default policy: Change the default policy:
:: ::
from SCons.Tool.MSCommon import msvc_set_scripterror_policy from SCons.Tool.MSCommon import msvc_set_scripterror_policy
@ -146,6 +313,7 @@ Change the default policy:
msvc_set_scripterror_policy('Warning') msvc_set_scripterror_policy('Warning')
Specify the policy per-environment: Specify the policy per-environment:
:: ::
env = Environment(MSVC_VERSION='14.3', MSVC_SPECTRE_LIBS=True, MSVC_SCRIPTERROR_POLICY='Warning') env = Environment(MSVC_VERSION='14.3', MSVC_SPECTRE_LIBS=True, MSVC_SCRIPTERROR_POLICY='Warning')
@ -171,6 +339,7 @@ On occasion, the raw vswhere output may prove useful especially if there are sus
detection of installed msvc instances. detection of installed msvc instances.
Windows command-line sample invocations: Windows command-line sample invocations:
:: ::
@rem 64-Bit Windows @rem 64-Bit Windows
@ -188,31 +357,37 @@ Batch File Arguments
Supported MSVC batch file arguments by product: Supported MSVC batch file arguments by product:
======= === === ======= ======= +---------+---------+--------+---------+---------+
Product UWP SDK Toolset Spectre | Product | UWP | SDK | Toolset | Spectre |
======= === === ======= ======= +=========+=========+========+=========+=========+
VS2022 X X X X | VS2022 | X | X | X | X |
------- --- --- ------- ------- +---------+---------+--------+---------+---------+
VS2019 X X X X | VS2019 | X | X | X | X |
------- --- --- ------- ------- +---------+---------+--------+---------+---------+
VS2017 X X X X | VS2017 | X | X | X | X |
------- --- --- ------- ------- +---------+---------+--------+---------+---------+
VS2015 X X | VS2015 | X [1] | X [2] | | |
======= === === ======= ======= +---------+---------+--------+---------+---------+
Notes:
1) The BuildTools edition does not support the ``store`` argument. The Express edition
supports the ``store`` argument for the ``x86`` target only.
2) The ``sdk version`` argument is not supported in the BuildTools and Express editions.
Supported MSVC batch file arguments in SCons: Supported MSVC batch file arguments in SCons:
======== ====================================== =================================================== +----------+----------------------------------------+-----------------------------------------------------+
Argument Construction Variable Script Argument Equivalent | Argument | Construction Variable | Script Argument Equivalent |
======== ====================================== =================================================== +==========+========================================+=====================================================+
UWP ``MSVC_UWP_APP=True`` ``MSVC_SCRIPT_ARGS='store'`` | UWP | ``MSVC_UWP_APP=True`` | ``MSVC_SCRIPT_ARGS='store'`` |
-------- -------------------------------------- --------------------------------------------------- +----------+----------------------------------------+-----------------------------------------------------+
SDK ``MSVC_SDK_VERSION='10.0.20348.0'`` ``MSVC_SCRIPT_ARGS='10.0.20348.0'`` | SDK | ``MSVC_SDK_VERSION='10.0.20348.0'`` | ``MSVC_SCRIPT_ARGS='10.0.20348.0'`` |
-------- -------------------------------------- --------------------------------------------------- +----------+----------------------------------------+-----------------------------------------------------+
Toolset ``MSVC_TOOLSET_VERSION='14.31.31103'`` ``MSVC_SCRIPT_ARGS='-vcvars_ver=14.31.31103'`` | Toolset | ``MSVC_TOOLSET_VERSION='14.31.31103'`` | ``MSVC_SCRIPT_ARGS='-vcvars_ver=14.31.31103'`` |
-------- -------------------------------------- --------------------------------------------------- +----------+----------------------------------------+-----------------------------------------------------+
Spectre ``MSVC_SPECTRE_LIBS=True`` ``MSVC_SCRIPT_ARGS='-vcvars_spectre_libs=spectre'`` | Spectre | ``MSVC_SPECTRE_LIBS=True`` | ``MSVC_SCRIPT_ARGS='-vcvars_spectre_libs=spectre'`` |
======== ====================================== =================================================== +----------+----------------------------------------+-----------------------------------------------------+
**MSVC_SCRIPT_ARGS contents are not validated. Utilizing script arguments that have construction **MSVC_SCRIPT_ARGS contents are not validated. Utilizing script arguments that have construction
variable equivalents is discouraged and may lead to difficult to diagnose build errors.** variable equivalents is discouraged and may lead to difficult to diagnose build errors.**
@ -244,6 +419,7 @@ that the msvc batch files would return. When using ``MSVC_SCRIPT_ARGS``, the
toolset specification should be omitted entirely. toolset specification should be omitted entirely.
Local installation and summary test results: Local installation and summary test results:
:: ::
VS2022\VC\Auxiliary\Build\Microsoft.VCToolsVersion.v143.default.txt VS2022\VC\Auxiliary\Build\Microsoft.VCToolsVersion.v143.default.txt
@ -253,6 +429,7 @@ Local installation and summary test results:
14.32.31326 14.32.31326
Toolset version summary: Toolset version summary:
:: ::
14.31.31103 Environment() 14.31.31103 Environment()
@ -268,6 +445,7 @@ Toolset version summary:
14.32.31326 Environment(MSVC_SCRIPT_ARGS=['-vcvars_ver=14.32']) 14.32.31326 Environment(MSVC_SCRIPT_ARGS=['-vcvars_ver=14.32'])
VS2022\\Common7\\Tools\\vsdevcmd\\ext\\vcvars.bat usage fragment: VS2022\\Common7\\Tools\\vsdevcmd\\ext\\vcvars.bat usage fragment:
:: ::
@echo -vcvars_ver=version : Version of VC++ Toolset to select @echo -vcvars_ver=version : Version of VC++ Toolset to select
@ -289,6 +467,7 @@ VS2022\\Common7\\Tools\\vsdevcmd\\ext\\vcvars.bat usage fragment:
@echo SxS toolset to [VSInstallDir]\VC\MSVC\Tools\ directory. @echo SxS toolset to [VSInstallDir]\VC\MSVC\Tools\ directory.
VS2022 batch file fragment to determine the default toolset version: VS2022 batch file fragment to determine the default toolset version:
:: ::
@REM Add MSVC @REM Add MSVC
@ -315,75 +494,114 @@ Visual Studio Version Notes
SDK Versions SDK Versions
------------ ------------
==== ============ +------+-------------------+
SDK Format | SDK | Format |
==== ============ +======+===================+
10.0 10.0.XXXXX.Y | 10.0 | 10.0.XXXXX.Y [1] |
---- ------------ +------+-------------------+
8.1 8.1 | 8.1 | 8.1 |
==== ============ +------+-------------------+
Notes:
1) The Windows 10 SDK version number is 10.0.20348.0 and earlier.
The Windows 11 SDK version number is 10.0.22000.194 and later.
BuildSeries Versions
--------------------
+-------------+-------+-------+
| BuildSeries | VCVER | CLVER |
+=============+=======+=======+
| 14.4 | 14.4X | 19.4 |
+-------------+-------+-------+
| 14.3 | 14.3X | 19.3 |
+-------------+-------+-------+
| 14.2 | 14.2X | 19.2 |
+-------------+-------+-------+
| 14.1 | 14.1X | 19.1 |
+-------------+-------+-------+
| 14.0 | 14.0 | 19.0 |
+-------------+-------+-------+
| 12.0 | 12.0 | 18.0 |
+-------------+-------+-------+
| 11.0 | 11.0 | 17.0 |
+-------------+-------+-------+
| 10.0 | 10.0 | 16.0 |
+-------------+-------+-------+
| 9.0 | 9.0 | 15.0 |
+-------------+-------+-------+
| 8.0 | 8.0 | 14.0 |
+-------------+-------+-------+
| 7.1 | 7.1 | 13.1 |
+-------------+-------+-------+
| 7.0 | 7.0 | 13.0 |
+-------------+-------+-------+
| 6.0 | 6.0 | 12.0 |
+-------------+-------+-------+
BuildTools Versions BuildTools Versions
------------------- -------------------
========== ===== ===== ======== +------------+-------------+----------+
BuildTools VCVER CLVER MSVCRT | BuildTools | BuildSeries | MSVCRT |
========== ===== ===== ======== +============+=============+==========+
v143 14.3 19.3 140/ucrt | v143 | 14.4, 14.3 | 140/ucrt |
---------- ----- ----- -------- +------------+-------------+----------+
v142 14.2 19.2 140/ucrt | v142 | 14.2 | 140/ucrt |
---------- ----- ----- -------- +------------+-------------+----------+
v141 14.1 19.1 140/ucrt | v141 | 14.1 | 140/ucrt |
---------- ----- ----- -------- +------------+-------------+----------+
v140 14.0 19.0 140/ucrt | v140 | 14.0 | 140/ucrt |
---------- ----- ----- -------- +------------+-------------+----------+
v120 12.0 18.0 120 | v120 | 12.0 | 120 |
---------- ----- ----- -------- +------------+-------------+----------+
v110 11.0 17.0 110 | v110 | 11.0 | 110 |
---------- ----- ----- -------- +------------+-------------+----------+
v100 10.0 16.0 100 | v100 | 10.0 | 100 |
---------- ----- ----- -------- +------------+-------------+----------+
v90 9.0 15.0 90 | v90 | 9.0 | 90 |
---------- ----- ----- -------- +------------+-------------+----------+
v80 8.0 14.0 80 | v80 | 8.0 | 80 |
---------- ----- ----- -------- +------------+-------------+----------+
v71 7.1 13.1 71 | v71 | 7.1 | 71 |
---------- ----- ----- -------- +------------+-------------+----------+
v70 7.0 13.0 70 | v70 | 7.0 | 70 |
---------- ----- ----- -------- +------------+-------------+----------+
v60 6.0 12.0 60 | v60 | 6.0 | 60 |
========== ===== ===== ======== +------------+-------------+----------+
Product Versions Product Versions
---------------- ----------------
======== ===== ========= ====================== +----------+-------+-------+-----------+------------------------+
Product VSVER SDK BuildTools | Product | VSVER | SCons | SDK | BuildTools |
======== ===== ========= ====================== +==========+=======+=======+===========+========================+
2022 17.0 10.0, 8.1 v143, v142, v141, v140 | 2022 | 17.0 | 14.3 | 10.0, 8.1 | v143, v142, v141, v140 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2019 16.0 10.0, 8.1 v142, v141, v140 | 2019 | 16.0 | 14.2 | 10.0, 8.1 | v142, v141, v140 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2017 15.0 10.0, 8.1 v141, v140 | 2017 | 15.0 | 14.1 | 10.0, 8.1 | v141, v140 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2015 14.0 10.0, 8.1 v140 | 2015 | 14.0 | 14.0 | 10.0, 8.1 | v140 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2013 12.0 v120 | 2013 | 12.0 | 12.0 | | v120 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2012 11.0 v110 | 2012 | 11.0 | 11.0 | | v110 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2010 10.0 v100 | 2010 | 10.0 | 10.0 | | v100 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2008 9.0 v90 | 2008 | 9.0 | 9.0 | | v90 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2005 8.0 v80 | 2005 | 8.0 | 8.0 | | v80 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2003.NET 7.1 v71 | 2003.NET | 7.1 | 7.1 | | v71 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
2002.NET 7.0 v70 | 2002.NET | 7.0 | 7.0 | | v70 |
-------- ----- --------- ---------------------- +----------+-------+-------+-----------+------------------------+
6.0 6.0 v60 | 6.0 | 6.0 | 6.0 | | v60 |
======== ===== ========= ====================== +----------+-------+-------+-----------+------------------------+
SCons Implementation Notes SCons Implementation Notes

View file

@ -45,6 +45,9 @@ from SCons.Tool.MSCommon.vc import ( # noqa: F401
msvc_toolset_versions, msvc_toolset_versions,
msvc_toolset_versions_spectre, msvc_toolset_versions_spectre,
msvc_query_version_toolset, msvc_query_version_toolset,
vswhere_register_executable,
vswhere_get_executable,
vswhere_freeze_executable,
) )
from SCons.Tool.MSCommon.vs import ( # noqa: F401 from SCons.Tool.MSCommon.vs import ( # noqa: F401
@ -60,6 +63,8 @@ from .MSVC.Policy import ( # noqa: F401
msvc_get_notfound_policy, msvc_get_notfound_policy,
msvc_set_scripterror_policy, msvc_set_scripterror_policy,
msvc_get_scripterror_policy, msvc_get_scripterror_policy,
msvc_notfound_policy_contextmanager,
msvc_scripterror_policy_contextmanager,
) )
from .MSVC.Exceptions import ( # noqa: F401 from .MSVC.Exceptions import ( # noqa: F401
@ -78,7 +83,9 @@ from .vc import ( # noqa: F401
MSVCUnsupportedHostArch, MSVCUnsupportedHostArch,
MSVCUnsupportedTargetArch, MSVCUnsupportedTargetArch,
MSVCScriptNotFound, MSVCScriptNotFound,
MSVCUseScriptError,
MSVCUseSettingsError, MSVCUseSettingsError,
VSWhereUserError,
) )
from .MSVC.Util import ( # noqa: F401 from .MSVC.Util import ( # noqa: F401

View file

@ -372,7 +372,7 @@ def mssdk_setup_env(env):
return return
msvs_version = env.subst(msvs_version) msvs_version = env.subst(msvs_version)
from . import vs from . import vs
msvs = vs.get_vs_by_version(msvs_version) msvs = vs.get_vs_by_version(msvs_version, env)
debug('mssdk_setup_env:msvs is :%s', msvs) debug('mssdk_setup_env:msvs is :%s', msvs)
if not msvs: if not msvs:
debug('mssdk_setup_env: no VS version detected, bailingout:%s', msvs) debug('mssdk_setup_env: no VS version detected, bailingout:%s', msvs)

View file

@ -28,7 +28,6 @@ MS Compilers: detect Visual Studio and/or Visual C/C++
import os import os
import SCons.Errors import SCons.Errors
import SCons.Tool.MSCommon.vc
import SCons.Util import SCons.Util
from .common import ( from .common import (
@ -40,6 +39,19 @@ from .common import (
read_reg, read_reg,
) )
from .vc import (
find_vc_pdir,
get_msvc_version_numeric,
reset_installed_vcs,
vswhere_freeze_env,
)
# Visual Studio express version policy when unqualified version is not installed:
# True: use express version for unqualified version (e.g., use 12.0Exp for 12.0)
# False: do not use express version for unqualified version
_VSEXPRESS_USE_VERSTR = True
class VisualStudio: class VisualStudio:
""" """
@ -48,6 +60,9 @@ class VisualStudio:
""" """
def __init__(self, version, **kw) -> None: def __init__(self, version, **kw) -> None:
self.version = version self.version = version
self.verstr = get_msvc_version_numeric(version)
self.vernum = float(self.verstr)
self.is_express = True if self.verstr != self.version else False
kw['vc_version'] = kw.get('vc_version', version) kw['vc_version'] = kw.get('vc_version', version)
kw['sdk_version'] = kw.get('sdk_version', version) kw['sdk_version'] = kw.get('sdk_version', version)
self.__dict__.update(kw) self.__dict__.update(kw)
@ -66,7 +81,7 @@ class VisualStudio:
return batch_file return batch_file
def find_vs_dir_by_vc(self, env): def find_vs_dir_by_vc(self, env):
dir = SCons.Tool.MSCommon.vc.find_vc_pdir(env, self.vc_version) dir = find_vc_pdir(self.vc_version, env)
if not dir: if not dir:
debug('no installed VC %s', self.vc_version) debug('no installed VC %s', self.vc_version)
return None return None
@ -428,6 +443,7 @@ InstalledVSMap = None
def get_installed_visual_studios(env=None): def get_installed_visual_studios(env=None):
global InstalledVSList global InstalledVSList
global InstalledVSMap global InstalledVSMap
vswhere_freeze_env(env)
if InstalledVSList is None: if InstalledVSList is None:
InstalledVSList = [] InstalledVSList = []
InstalledVSMap = {} InstalledVSMap = {}
@ -436,12 +452,21 @@ def get_installed_visual_studios(env=None):
if vs.get_executable(env): if vs.get_executable(env):
debug('found VS %s', vs.version) debug('found VS %s', vs.version)
InstalledVSList.append(vs) InstalledVSList.append(vs)
if vs.is_express and vs.verstr not in InstalledVSMap:
if _VSEXPRESS_USE_VERSTR:
InstalledVSMap[vs.verstr] = vs
InstalledVSMap[vs.version] = vs InstalledVSMap[vs.version] = vs
return InstalledVSList return InstalledVSList
def _get_installed_vss(env=None):
get_installed_visual_studios(env)
versions = list(InstalledVSMap.keys())
return versions
def reset_installed_visual_studios() -> None: def reset_installed_visual_studios() -> None:
global InstalledVSList global InstalledVSList
global InstalledVSMap global InstalledVSMap
debug('')
InstalledVSList = None InstalledVSList = None
InstalledVSMap = None InstalledVSMap = None
for vs in SupportedVSList: for vs in SupportedVSList:
@ -449,7 +474,7 @@ def reset_installed_visual_studios() -> None:
# Need to clear installed VC's as well as they are used in finding # Need to clear installed VC's as well as they are used in finding
# installed VS's # installed VS's
SCons.Tool.MSCommon.vc.reset_installed_vcs() reset_installed_vcs()
# We may be asked to update multiple construction environments with # We may be asked to update multiple construction environments with
@ -487,7 +512,7 @@ def reset_installed_visual_studios() -> None:
def msvs_exists(env=None) -> bool: def msvs_exists(env=None) -> bool:
return len(get_installed_visual_studios(env)) > 0 return len(get_installed_visual_studios(env)) > 0
def get_vs_by_version(msvs): def get_vs_by_version(msvs, env=None):
global InstalledVSMap global InstalledVSMap
global SupportedVSMap global SupportedVSMap
@ -495,7 +520,7 @@ def get_vs_by_version(msvs):
if msvs not in SupportedVSMap: if msvs not in SupportedVSMap:
msg = "Visual Studio version %s is not supported" % repr(msvs) msg = "Visual Studio version %s is not supported" % repr(msvs)
raise SCons.Errors.UserError(msg) raise SCons.Errors.UserError(msg)
get_installed_visual_studios() get_installed_visual_studios(env)
vs = InstalledVSMap.get(msvs) vs = InstalledVSMap.get(msvs)
debug('InstalledVSMap:%s', InstalledVSMap) debug('InstalledVSMap:%s', InstalledVSMap)
debug('found vs:%s', vs) debug('found vs:%s', vs)
@ -524,7 +549,7 @@ def get_default_version(env):
""" """
if 'MSVS' not in env or not SCons.Util.is_Dict(env['MSVS']): if 'MSVS' not in env or not SCons.Util.is_Dict(env['MSVS']):
# get all versions, and remember them for speed later # get all versions, and remember them for speed later
versions = [vs.version for vs in get_installed_visual_studios()] versions = _get_installed_vss(env)
env['MSVS'] = {'VERSIONS' : versions} env['MSVS'] = {'VERSIONS' : versions}
else: else:
versions = env['MSVS'].get('VERSIONS', []) versions = env['MSVS'].get('VERSIONS', [])
@ -570,7 +595,7 @@ def merge_default_version(env) -> None:
# TODO: refers to versions and arch which aren't defined; called nowhere. Drop? # TODO: refers to versions and arch which aren't defined; called nowhere. Drop?
def msvs_setup_env(env) -> None: def msvs_setup_env(env) -> None:
msvs = get_vs_by_version(version) msvs = get_vs_by_version(version, env)
if msvs is None: if msvs is None:
return return
batfilename = msvs.get_batch_file() batfilename = msvs.get_batch_file()
@ -582,7 +607,7 @@ def msvs_setup_env(env) -> None:
vars = ('LIB', 'LIBPATH', 'PATH', 'INCLUDE') vars = ('LIB', 'LIBPATH', 'PATH', 'INCLUDE')
msvs_list = get_installed_visual_studios() msvs_list = get_installed_visual_studios(env)
vscommonvarnames = [vs.common_tools_var for vs in msvs_list] vscommonvarnames = [vs.common_tools_var for vs in msvs_list]
save_ENV = env['ENV'] save_ENV = env['ENV']
nenv = normalize_env(env['ENV'], nenv = normalize_env(env['ENV'],
@ -597,11 +622,10 @@ def msvs_setup_env(env) -> None:
for k, v in vars.items(): for k, v in vars.items():
env.PrependENVPath(k, v, delete_existing=1) env.PrependENVPath(k, v, delete_existing=1)
def query_versions(): def query_versions(env=None):
"""Query the system to get available versions of VS. A version is """Query the system to get available versions of VS. A version is
considered when a batfile is found.""" considered when a batfile is found."""
msvs_list = get_installed_visual_studios() versions = _get_installed_vss(env)
versions = [msvs.version for msvs in msvs_list]
return versions return versions
# Local Variables: # Local Variables:

View file

@ -251,7 +251,7 @@ class Tool:
kw.update(call_kw) kw.update(call_kw)
else: else:
kw = self.init_kw kw = self.init_kw
env.Append(TOOLS=[self.name]) env.AppendUnique(TOOLS=[self.name])
if hasattr(self, 'options'): if hasattr(self, 'options'):
import SCons.Variables import SCons.Variables
if 'options' not in env: if 'options' not in env:

Some files were not shown because too many files have changed in this diff Show more