989 lines
36 KiB
Python
989 lines
36 KiB
Python
|
# 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.
|
||
|
|
||
|
"""SCons string substitution."""
|
||
|
|
||
|
import collections
|
||
|
import re
|
||
|
from inspect import signature, Parameter
|
||
|
|
||
|
import SCons.Errors
|
||
|
from SCons.Util import is_String, is_Sequence
|
||
|
|
||
|
# Indexed by the SUBST_* constants below.
|
||
|
_strconv = [
|
||
|
SCons.Util.to_String_for_subst,
|
||
|
SCons.Util.to_String_for_subst,
|
||
|
SCons.Util.to_String_for_signature,
|
||
|
]
|
||
|
|
||
|
AllowableExceptions = (IndexError, NameError)
|
||
|
|
||
|
|
||
|
def SetAllowableExceptions(*excepts):
|
||
|
global AllowableExceptions
|
||
|
AllowableExceptions = [_f for _f in excepts if _f]
|
||
|
|
||
|
|
||
|
def raise_exception(exception, target, s):
|
||
|
name = exception.__class__.__name__
|
||
|
msg = "%s `%s' trying to evaluate `%s'" % (name, exception, s)
|
||
|
if target:
|
||
|
raise SCons.Errors.BuildError(target[0], msg)
|
||
|
else:
|
||
|
raise SCons.Errors.UserError(msg)
|
||
|
|
||
|
|
||
|
class Literal:
|
||
|
"""A wrapper for a string. If you use this object wrapped
|
||
|
around a string, then it will be interpreted as literal.
|
||
|
When passed to the command interpreter, all special
|
||
|
characters will be escaped."""
|
||
|
def __init__(self, lstr):
|
||
|
self.lstr = lstr
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.lstr
|
||
|
|
||
|
def escape(self, escape_func):
|
||
|
return escape_func(self.lstr)
|
||
|
|
||
|
def for_signature(self):
|
||
|
return self.lstr
|
||
|
|
||
|
def is_literal(self):
|
||
|
return 1
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
if not isinstance(other, Literal):
|
||
|
return False
|
||
|
return self.lstr == other.lstr
|
||
|
|
||
|
def __neq__(self, other):
|
||
|
return not self.__eq__(other)
|
||
|
|
||
|
def __hash__(self):
|
||
|
return hash(self.lstr)
|
||
|
|
||
|
class SpecialAttrWrapper:
|
||
|
"""This is a wrapper for what we call a 'Node special attribute.'
|
||
|
This is any of the attributes of a Node that we can reference from
|
||
|
Environment variable substitution, such as $TARGET.abspath or
|
||
|
$SOURCES[1].filebase. We implement the same methods as Literal
|
||
|
so we can handle special characters, plus a for_signature method,
|
||
|
such that we can return some canonical string during signature
|
||
|
calculation to avoid unnecessary rebuilds."""
|
||
|
|
||
|
def __init__(self, lstr, for_signature=None):
|
||
|
"""The for_signature parameter, if supplied, will be the
|
||
|
canonical string we return from for_signature(). Else
|
||
|
we will simply return lstr."""
|
||
|
self.lstr = lstr
|
||
|
if for_signature:
|
||
|
self.forsig = for_signature
|
||
|
else:
|
||
|
self.forsig = lstr
|
||
|
|
||
|
def __str__(self):
|
||
|
return self.lstr
|
||
|
|
||
|
def escape(self, escape_func):
|
||
|
return escape_func(self.lstr)
|
||
|
|
||
|
def for_signature(self):
|
||
|
return self.forsig
|
||
|
|
||
|
def is_literal(self):
|
||
|
return 1
|
||
|
|
||
|
def quote_spaces(arg):
|
||
|
"""Generic function for putting double quotes around any string that
|
||
|
has white space in it."""
|
||
|
if ' ' in arg or '\t' in arg:
|
||
|
return '"%s"' % arg
|
||
|
else:
|
||
|
return str(arg)
|
||
|
|
||
|
class CmdStringHolder(collections.UserString):
|
||
|
"""This is a special class used to hold strings generated by
|
||
|
scons_subst() and scons_subst_list(). It defines a special method
|
||
|
escape(). When passed a function with an escape algorithm for a
|
||
|
particular platform, it will return the contained string with the
|
||
|
proper escape sequences inserted.
|
||
|
"""
|
||
|
def __init__(self, cmd, literal=None):
|
||
|
super().__init__(cmd)
|
||
|
self.literal = literal
|
||
|
|
||
|
def is_literal(self):
|
||
|
return self.literal
|
||
|
|
||
|
def escape(self, escape_func, quote_func=quote_spaces):
|
||
|
"""Escape the string with the supplied function. The
|
||
|
function is expected to take an arbitrary string, then
|
||
|
return it with all special characters escaped and ready
|
||
|
for passing to the command interpreter.
|
||
|
|
||
|
After calling this function, the next call to str() will
|
||
|
return the escaped string.
|
||
|
"""
|
||
|
|
||
|
if self.is_literal():
|
||
|
return escape_func(self.data)
|
||
|
elif ' ' in self.data or '\t' in self.data:
|
||
|
return quote_func(self.data)
|
||
|
else:
|
||
|
return self.data
|
||
|
|
||
|
def escape_list(mylist, escape_func):
|
||
|
"""Escape a list of arguments by running the specified escape_func
|
||
|
on every object in the list that has an escape() method."""
|
||
|
def escape(obj, escape_func=escape_func):
|
||
|
try:
|
||
|
e = obj.escape
|
||
|
except AttributeError:
|
||
|
return obj
|
||
|
else:
|
||
|
return e(escape_func)
|
||
|
return list(map(escape, mylist))
|
||
|
|
||
|
class NLWrapper:
|
||
|
"""A wrapper class that delays turning a list of sources or targets
|
||
|
into a NodeList until it's needed. The specified function supplied
|
||
|
when the object is initialized is responsible for turning raw nodes
|
||
|
into proxies that implement the special attributes like .abspath,
|
||
|
.source, etc. This way, we avoid creating those proxies just
|
||
|
"in case" someone is going to use $TARGET or the like, and only
|
||
|
go through the trouble if we really have to.
|
||
|
|
||
|
In practice, this might be a wash performance-wise, but it's a little
|
||
|
cleaner conceptually...
|
||
|
"""
|
||
|
|
||
|
def __init__(self, list, func):
|
||
|
self.list = list
|
||
|
self.func = func
|
||
|
def _return_nodelist(self):
|
||
|
return self.nodelist
|
||
|
def _gen_nodelist(self):
|
||
|
mylist = self.list
|
||
|
if mylist is None:
|
||
|
mylist = []
|
||
|
elif not is_Sequence(mylist):
|
||
|
mylist = [mylist]
|
||
|
# The map(self.func) call is what actually turns
|
||
|
# a list into appropriate proxies.
|
||
|
self.nodelist = SCons.Util.NodeList(list(map(self.func, mylist)))
|
||
|
self._create_nodelist = self._return_nodelist
|
||
|
return self.nodelist
|
||
|
_create_nodelist = _gen_nodelist
|
||
|
|
||
|
|
||
|
class Targets_or_Sources(collections.UserList):
|
||
|
"""A class that implements $TARGETS or $SOURCES expansions by in turn
|
||
|
wrapping a NLWrapper. This class handles the different methods used
|
||
|
to access the list, calling the NLWrapper to create proxies on demand.
|
||
|
|
||
|
Note that we subclass collections.UserList purely so that the
|
||
|
is_Sequence() function will identify an object of this class as
|
||
|
a list during variable expansion. We're not really using any
|
||
|
collections.UserList methods in practice.
|
||
|
"""
|
||
|
def __init__(self, nl):
|
||
|
self.nl = nl
|
||
|
def __getattr__(self, attr):
|
||
|
nl = self.nl._create_nodelist()
|
||
|
return getattr(nl, attr)
|
||
|
def __getitem__(self, i):
|
||
|
nl = self.nl._create_nodelist()
|
||
|
return nl[i]
|
||
|
def __str__(self):
|
||
|
nl = self.nl._create_nodelist()
|
||
|
return str(nl)
|
||
|
def __repr__(self):
|
||
|
nl = self.nl._create_nodelist()
|
||
|
return repr(nl)
|
||
|
|
||
|
class Target_or_Source:
|
||
|
"""A class that implements $TARGET or $SOURCE expansions by in turn
|
||
|
wrapping a NLWrapper. This class handles the different methods used
|
||
|
to access an individual proxy Node, calling the NLWrapper to create
|
||
|
a proxy on demand.
|
||
|
"""
|
||
|
def __init__(self, nl):
|
||
|
self.nl = nl
|
||
|
def __getattr__(self, attr):
|
||
|
nl = self.nl._create_nodelist()
|
||
|
try:
|
||
|
nl0 = nl[0]
|
||
|
except IndexError:
|
||
|
# If there is nothing in the list, then we have no attributes to
|
||
|
# pass through, so raise AttributeError for everything.
|
||
|
raise AttributeError("NodeList has no attribute: %s" % attr)
|
||
|
return getattr(nl0, attr)
|
||
|
def __str__(self):
|
||
|
nl = self.nl._create_nodelist()
|
||
|
if nl:
|
||
|
return str(nl[0])
|
||
|
return ''
|
||
|
def __repr__(self):
|
||
|
nl = self.nl._create_nodelist()
|
||
|
if nl:
|
||
|
return repr(nl[0])
|
||
|
return ''
|
||
|
|
||
|
class NullNodeList(SCons.Util.NullSeq):
|
||
|
def __call__(self, *args, **kwargs): return ''
|
||
|
def __str__(self): return ''
|
||
|
|
||
|
NullNodesList = NullNodeList()
|
||
|
|
||
|
def subst_dict(target, source):
|
||
|
"""Create a dictionary for substitution of special
|
||
|
construction variables.
|
||
|
|
||
|
This translates the following special arguments:
|
||
|
|
||
|
target - the target (object or array of objects),
|
||
|
used to generate the TARGET and TARGETS
|
||
|
construction variables
|
||
|
|
||
|
source - the source (object or array of objects),
|
||
|
used to generate the SOURCES and SOURCE
|
||
|
construction variables
|
||
|
"""
|
||
|
dict = {}
|
||
|
|
||
|
if target:
|
||
|
def get_tgt_subst_proxy(thing):
|
||
|
try:
|
||
|
subst_proxy = thing.get_subst_proxy()
|
||
|
except AttributeError:
|
||
|
subst_proxy = thing # probably a string, just return it
|
||
|
return subst_proxy
|
||
|
tnl = NLWrapper(target, get_tgt_subst_proxy)
|
||
|
dict['TARGETS'] = Targets_or_Sources(tnl)
|
||
|
dict['TARGET'] = Target_or_Source(tnl)
|
||
|
|
||
|
# This is a total cheat, but hopefully this dictionary goes
|
||
|
# away soon anyway. We just let these expand to $TARGETS
|
||
|
# because that's "good enough" for the use of ToolSurrogates
|
||
|
# (see test/ToolSurrogate.py) to generate documentation.
|
||
|
dict['CHANGED_TARGETS'] = '$TARGETS'
|
||
|
dict['UNCHANGED_TARGETS'] = '$TARGETS'
|
||
|
else:
|
||
|
dict['TARGETS'] = NullNodesList
|
||
|
dict['TARGET'] = NullNodesList
|
||
|
|
||
|
if source:
|
||
|
def get_src_subst_proxy(node):
|
||
|
try:
|
||
|
rfile = node.rfile
|
||
|
except AttributeError:
|
||
|
pass
|
||
|
else:
|
||
|
node = rfile()
|
||
|
try:
|
||
|
return node.get_subst_proxy()
|
||
|
except AttributeError:
|
||
|
return node # probably a String, just return it
|
||
|
snl = NLWrapper(source, get_src_subst_proxy)
|
||
|
dict['SOURCES'] = Targets_or_Sources(snl)
|
||
|
dict['SOURCE'] = Target_or_Source(snl)
|
||
|
|
||
|
# This is a total cheat, but hopefully this dictionary goes
|
||
|
# away soon anyway. We just let these expand to $TARGETS
|
||
|
# because that's "good enough" for the use of ToolSurrogates
|
||
|
# (see test/ToolSurrogate.py) to generate documentation.
|
||
|
dict['CHANGED_SOURCES'] = '$SOURCES'
|
||
|
dict['UNCHANGED_SOURCES'] = '$SOURCES'
|
||
|
else:
|
||
|
dict['SOURCES'] = NullNodesList
|
||
|
dict['SOURCE'] = NullNodesList
|
||
|
|
||
|
return dict
|
||
|
|
||
|
|
||
|
_callable_args_set = {'target', 'source', 'env', 'for_signature'}
|
||
|
|
||
|
class StringSubber:
|
||
|
"""A class to construct the results of a scons_subst() call.
|
||
|
|
||
|
This binds a specific construction environment, mode, target and
|
||
|
source with two methods (substitute() and expand()) that handle
|
||
|
the expansion.
|
||
|
"""
|
||
|
|
||
|
|
||
|
def __init__(self, env, mode, conv, gvars):
|
||
|
self.env = env
|
||
|
self.mode = mode
|
||
|
self.conv = conv
|
||
|
self.gvars = gvars
|
||
|
|
||
|
def expand(self, s, lvars):
|
||
|
"""Expand a single "token" as necessary, returning an
|
||
|
appropriate string containing the expansion.
|
||
|
|
||
|
This handles expanding different types of things (strings,
|
||
|
lists, callables) appropriately. It calls the wrapper
|
||
|
substitute() method to re-expand things as necessary, so that
|
||
|
the results of expansions of side-by-side strings still get
|
||
|
re-evaluated separately, not smushed together.
|
||
|
"""
|
||
|
if is_String(s):
|
||
|
try:
|
||
|
s0, s1 = s[:2]
|
||
|
except (IndexError, ValueError):
|
||
|
return s
|
||
|
if s0 != '$':
|
||
|
return s
|
||
|
if s1 == '$':
|
||
|
# In this case keep the double $'s which we'll later
|
||
|
# swap for a single dollar sign as we need to retain
|
||
|
# this information to properly avoid matching "$("" when
|
||
|
# the actual text was "$$("" (or "$)"" when "$$)"" )
|
||
|
return '$$'
|
||
|
elif s1 in '()':
|
||
|
return s
|
||
|
else:
|
||
|
key = s[1:]
|
||
|
if key[0] == '{' or '.' in key:
|
||
|
if key[0] == '{':
|
||
|
key = key[1:-1]
|
||
|
|
||
|
# Store for error messages if we fail to expand the
|
||
|
# value
|
||
|
old_s = s
|
||
|
s = None
|
||
|
if key in lvars:
|
||
|
s = lvars[key]
|
||
|
elif key in self.gvars:
|
||
|
s = self.gvars[key]
|
||
|
else:
|
||
|
try:
|
||
|
s = eval(key, self.gvars, lvars)
|
||
|
except KeyboardInterrupt:
|
||
|
raise
|
||
|
except Exception as e:
|
||
|
if e.__class__ in AllowableExceptions:
|
||
|
return ''
|
||
|
raise_exception(e, lvars['TARGETS'], old_s)
|
||
|
|
||
|
if s is None and NameError not in AllowableExceptions:
|
||
|
raise_exception(NameError(key), lvars['TARGETS'], old_s)
|
||
|
elif s is None:
|
||
|
return ''
|
||
|
|
||
|
# Before re-expanding the result, handle
|
||
|
# recursive expansion by copying the local
|
||
|
# variable dictionary and overwriting a null
|
||
|
# string for the value of the variable name
|
||
|
# we just expanded.
|
||
|
#
|
||
|
# This could potentially be optimized by only
|
||
|
# copying lvars when s contains more expansions,
|
||
|
# but lvars is usually supposed to be pretty
|
||
|
# small, and deeply nested variable expansions
|
||
|
# are probably more the exception than the norm,
|
||
|
# so it should be tolerable for now.
|
||
|
lv = lvars.copy()
|
||
|
var = key.split('.')[0]
|
||
|
lv[var] = ''
|
||
|
return self.substitute(s, lv)
|
||
|
elif is_Sequence(s):
|
||
|
def func(l, conv=self.conv, substitute=self.substitute, lvars=lvars):
|
||
|
return conv(substitute(l, lvars))
|
||
|
return list(map(func, s))
|
||
|
elif callable(s):
|
||
|
|
||
|
# SCons has the unusual Null class where any __getattr__ call returns it's self,
|
||
|
# which does not work the signature module, and the Null class returns an empty
|
||
|
# string if called on, so we make an exception in this condition for Null class
|
||
|
# Also allow callables where the only non default valued args match the expected defaults
|
||
|
# this should also allow functools.partial's to work.
|
||
|
if isinstance(s, SCons.Util.Null) or {k for k, v in signature(s).parameters.items() if
|
||
|
k in _callable_args_set or v.default == Parameter.empty} == _callable_args_set:
|
||
|
|
||
|
s = s(target=lvars['TARGETS'],
|
||
|
source=lvars['SOURCES'],
|
||
|
env=self.env,
|
||
|
for_signature=(self.mode == SUBST_SIG))
|
||
|
else:
|
||
|
# This probably indicates that it's a callable
|
||
|
# object that doesn't match our calling arguments
|
||
|
# (like an Action).
|
||
|
if self.mode == SUBST_RAW:
|
||
|
return s
|
||
|
s = self.conv(s)
|
||
|
return self.substitute(s, lvars)
|
||
|
elif s is None:
|
||
|
return ''
|
||
|
else:
|
||
|
return s
|
||
|
|
||
|
def substitute(self, args, lvars):
|
||
|
"""Substitute expansions in an argument or list of arguments.
|
||
|
|
||
|
This serves as a wrapper for splitting up a string into
|
||
|
separate tokens.
|
||
|
"""
|
||
|
if is_String(args) and not isinstance(args, CmdStringHolder):
|
||
|
args = str(args) # In case it's a UserString.
|
||
|
try:
|
||
|
def sub_match(match):
|
||
|
return self.conv(self.expand(match.group(1), lvars))
|
||
|
result = _dollar_exps.sub(sub_match, args)
|
||
|
except TypeError:
|
||
|
# If the internal conversion routine doesn't return
|
||
|
# strings (it could be overridden to return Nodes, for
|
||
|
# example), then the 1.5.2 re module will throw this
|
||
|
# exception. Back off to a slower, general-purpose
|
||
|
# algorithm that works for all data types.
|
||
|
args = _separate_args.findall(args)
|
||
|
result = []
|
||
|
for a in args:
|
||
|
result.append(self.conv(self.expand(a, lvars)))
|
||
|
if len(result) == 1:
|
||
|
result = result[0]
|
||
|
else:
|
||
|
result = ''.join(map(str, result))
|
||
|
return result
|
||
|
else:
|
||
|
return self.expand(args, lvars)
|
||
|
|
||
|
|
||
|
class ListSubber(collections.UserList):
|
||
|
"""A class to construct the results of a scons_subst_list() call.
|
||
|
|
||
|
Like StringSubber, this class binds a specific construction
|
||
|
environment, mode, target and source with two methods
|
||
|
(substitute() and expand()) that handle the expansion.
|
||
|
|
||
|
In addition, however, this class is used to track the state of
|
||
|
the result(s) we're gathering so we can do the appropriate thing
|
||
|
whenever we have to append another word to the result--start a new
|
||
|
line, start a new word, append to the current word, etc. We do
|
||
|
this by setting the "append" attribute to the right method so
|
||
|
that our wrapper methods only need ever call ListSubber.append(),
|
||
|
and the rest of the object takes care of doing the right thing
|
||
|
internally.
|
||
|
"""
|
||
|
def __init__(self, env, mode, conv, gvars):
|
||
|
super().__init__([])
|
||
|
self.env = env
|
||
|
self.mode = mode
|
||
|
self.conv = conv
|
||
|
self.gvars = gvars
|
||
|
|
||
|
if self.mode == SUBST_RAW:
|
||
|
self.add_strip = lambda x: self.append(x)
|
||
|
else:
|
||
|
self.add_strip = lambda x: None
|
||
|
self.in_strip = None
|
||
|
self.next_line()
|
||
|
|
||
|
def expanded(self, s):
|
||
|
"""Determines if the string s requires further expansion.
|
||
|
|
||
|
Due to the implementation of ListSubber expand will call
|
||
|
itself 2 additional times for an already expanded string. This
|
||
|
method is used to determine if a string is already fully
|
||
|
expanded and if so exit the loop early to prevent these
|
||
|
recursive calls.
|
||
|
"""
|
||
|
if not is_String(s) or isinstance(s, CmdStringHolder):
|
||
|
return False
|
||
|
|
||
|
s = str(s) # in case it's a UserString
|
||
|
return _separate_args.findall(s) is None
|
||
|
|
||
|
def expand(self, s, lvars, within_list):
|
||
|
"""Expand a single "token" as necessary, appending the
|
||
|
expansion to the current result.
|
||
|
|
||
|
This handles expanding different types of things (strings,
|
||
|
lists, callables) appropriately. It calls the wrapper
|
||
|
substitute() method to re-expand things as necessary, so that
|
||
|
the results of expansions of side-by-side strings still get
|
||
|
re-evaluated separately, not smushed together.
|
||
|
"""
|
||
|
|
||
|
if is_String(s):
|
||
|
try:
|
||
|
s0, s1 = s[:2]
|
||
|
except (IndexError, ValueError):
|
||
|
self.append(s)
|
||
|
return
|
||
|
if s0 != '$':
|
||
|
self.append(s)
|
||
|
return
|
||
|
if s1 == '$':
|
||
|
self.append('$')
|
||
|
elif s1 == '(':
|
||
|
self.open_strip('$(')
|
||
|
elif s1 == ')':
|
||
|
self.close_strip('$)')
|
||
|
else:
|
||
|
key = s[1:]
|
||
|
if key[0] == '{' or key.find('.') >= 0:
|
||
|
if key[0] == '{':
|
||
|
key = key[1:-1]
|
||
|
|
||
|
# Store for error messages if we fail to expand the
|
||
|
# value
|
||
|
old_s = s
|
||
|
s = None
|
||
|
if key in lvars:
|
||
|
s = lvars[key]
|
||
|
elif key in self.gvars:
|
||
|
s = self.gvars[key]
|
||
|
else:
|
||
|
try:
|
||
|
s = eval(key, self.gvars, lvars)
|
||
|
except KeyboardInterrupt:
|
||
|
raise
|
||
|
except Exception as e:
|
||
|
if e.__class__ in AllowableExceptions:
|
||
|
return
|
||
|
raise_exception(e, lvars['TARGETS'], old_s)
|
||
|
|
||
|
if s is None and NameError not in AllowableExceptions:
|
||
|
raise_exception(NameError(), lvars['TARGETS'], old_s)
|
||
|
elif s is None:
|
||
|
return
|
||
|
|
||
|
# If the string is already full expanded there's no
|
||
|
# need to continue recursion.
|
||
|
if self.expanded(s):
|
||
|
self.append(s)
|
||
|
return
|
||
|
|
||
|
# Before re-expanding the result, handle
|
||
|
# recursive expansion by copying the local
|
||
|
# variable dictionary and overwriting a null
|
||
|
# string for the value of the variable name
|
||
|
# we just expanded.
|
||
|
lv = lvars.copy()
|
||
|
var = key.split('.')[0]
|
||
|
lv[var] = ''
|
||
|
self.substitute(s, lv, 0)
|
||
|
self.this_word()
|
||
|
elif is_Sequence(s):
|
||
|
for a in s:
|
||
|
self.substitute(a, lvars, 1)
|
||
|
self.next_word()
|
||
|
elif callable(s):
|
||
|
# SCons has the unusual Null class where any __getattr__ call returns it's self,
|
||
|
# which does not work the signature module, and the Null class returns an empty
|
||
|
# string if called on, so we make an exception in this condition for Null class
|
||
|
# Also allow callables where the only non default valued args match the expected defaults
|
||
|
# this should also allow functools.partial's to work.
|
||
|
if isinstance(s, SCons.Util.Null) or {k for k, v in signature(s).parameters.items() if
|
||
|
k in _callable_args_set or v.default == Parameter.empty} == _callable_args_set:
|
||
|
|
||
|
s = s(target=lvars['TARGETS'],
|
||
|
source=lvars['SOURCES'],
|
||
|
env=self.env,
|
||
|
for_signature=(self.mode != SUBST_CMD))
|
||
|
else:
|
||
|
# This probably indicates that it's a callable
|
||
|
# object that doesn't match our calling arguments
|
||
|
# (like an Action).
|
||
|
if self.mode == SUBST_RAW:
|
||
|
self.append(s)
|
||
|
return
|
||
|
s = self.conv(s)
|
||
|
self.substitute(s, lvars, within_list)
|
||
|
elif s is None:
|
||
|
self.this_word()
|
||
|
else:
|
||
|
self.append(s)
|
||
|
|
||
|
def substitute(self, args, lvars, within_list):
|
||
|
"""Substitute expansions in an argument or list of arguments.
|
||
|
|
||
|
This serves as a wrapper for splitting up a string into
|
||
|
separate tokens.
|
||
|
"""
|
||
|
|
||
|
if is_String(args) and not isinstance(args, CmdStringHolder):
|
||
|
args = str(args) # In case it's a UserString.
|
||
|
args = _separate_args.findall(args)
|
||
|
for a in args:
|
||
|
if a[0] in ' \t\n\r\f\v':
|
||
|
if '\n' in a:
|
||
|
self.next_line()
|
||
|
elif within_list:
|
||
|
self.append(a)
|
||
|
else:
|
||
|
self.next_word()
|
||
|
else:
|
||
|
self.expand(a, lvars, within_list)
|
||
|
else:
|
||
|
self.expand(args, lvars, within_list)
|
||
|
|
||
|
def next_line(self):
|
||
|
"""Arrange for the next word to start a new line. This
|
||
|
is like starting a new word, except that we have to append
|
||
|
another line to the result."""
|
||
|
collections.UserList.append(self, [])
|
||
|
self.next_word()
|
||
|
|
||
|
def this_word(self):
|
||
|
"""Arrange for the next word to append to the end of the
|
||
|
current last word in the result."""
|
||
|
self.append = self.add_to_current_word
|
||
|
|
||
|
def next_word(self):
|
||
|
"""Arrange for the next word to start a new word."""
|
||
|
self.append = self.add_new_word
|
||
|
|
||
|
def add_to_current_word(self, x):
|
||
|
"""Append the string x to the end of the current last word
|
||
|
in the result. If that is not possible, then just add
|
||
|
it as a new word. Make sure the entire concatenated string
|
||
|
inherits the object attributes of x (in particular, the
|
||
|
escape function) by wrapping it as CmdStringHolder."""
|
||
|
|
||
|
if not self.in_strip or self.mode != SUBST_SIG:
|
||
|
try:
|
||
|
current_word = self[-1][-1]
|
||
|
except IndexError:
|
||
|
self.add_new_word(x)
|
||
|
else:
|
||
|
# All right, this is a hack and it should probably
|
||
|
# be refactored out of existence in the future.
|
||
|
# The issue is that we want to smoosh words together
|
||
|
# and make one file name that gets escaped if
|
||
|
# we're expanding something like foo$EXTENSION,
|
||
|
# but we don't want to smoosh them together if
|
||
|
# it's something like >$TARGET, because then we'll
|
||
|
# treat the '>' like it's part of the file name.
|
||
|
# So for now, just hard-code looking for the special
|
||
|
# command-line redirection characters...
|
||
|
try:
|
||
|
last_char = str(current_word)[-1]
|
||
|
except IndexError:
|
||
|
last_char = '\0'
|
||
|
if last_char in '<>|':
|
||
|
self.add_new_word(x)
|
||
|
else:
|
||
|
y = current_word + x
|
||
|
|
||
|
# We used to treat a word appended to a literal
|
||
|
# as a literal itself, but this caused problems
|
||
|
# with interpreting quotes around space-separated
|
||
|
# targets on command lines. Removing this makes
|
||
|
# none of the "substantive" end-to-end tests fail,
|
||
|
# so we'll take this out but leave it commented
|
||
|
# for now in case there's a problem not covered
|
||
|
# by the test cases and we need to resurrect this.
|
||
|
#literal1 = self.literal(self[-1][-1])
|
||
|
#literal2 = self.literal(x)
|
||
|
y = self.conv(y)
|
||
|
if is_String(y):
|
||
|
#y = CmdStringHolder(y, literal1 or literal2)
|
||
|
y = CmdStringHolder(y, None)
|
||
|
self[-1][-1] = y
|
||
|
|
||
|
def add_new_word(self, x):
|
||
|
if not self.in_strip or self.mode != SUBST_SIG:
|
||
|
literal = self.literal(x)
|
||
|
x = self.conv(x)
|
||
|
if is_String(x):
|
||
|
x = CmdStringHolder(x, literal)
|
||
|
self[-1].append(x)
|
||
|
self.append = self.add_to_current_word
|
||
|
|
||
|
def literal(self, x):
|
||
|
try:
|
||
|
l = x.is_literal
|
||
|
except AttributeError:
|
||
|
return None
|
||
|
else:
|
||
|
return l()
|
||
|
|
||
|
def open_strip(self, x):
|
||
|
"""Handle the "open strip" $( token."""
|
||
|
self.add_strip(x)
|
||
|
self.in_strip = 1
|
||
|
|
||
|
def close_strip(self, x):
|
||
|
"""Handle the "close strip" $) token."""
|
||
|
self.add_strip(x)
|
||
|
self.in_strip = None
|
||
|
|
||
|
|
||
|
# Constants for the "mode" parameter to scons_subst_list() and
|
||
|
# scons_subst(). SUBST_RAW gives the raw command line. SUBST_CMD
|
||
|
# gives a command line suitable for passing to a shell. SUBST_SIG
|
||
|
# gives a command line appropriate for calculating the signature
|
||
|
# of a command line...if this changes, we should rebuild.
|
||
|
SUBST_CMD = 0
|
||
|
SUBST_RAW = 1
|
||
|
SUBST_SIG = 2
|
||
|
|
||
|
_rm = re.compile(r'\$[()]')
|
||
|
|
||
|
# Note the pattern below only matches $( or $) when there is no
|
||
|
# preceeding $. (Thus the (?<!\$))
|
||
|
_rm_split = re.compile(r'(?<!\$)(\$[()])')
|
||
|
|
||
|
# Indexed by the SUBST_* constants above.
|
||
|
_regex_remove = [ _rm, None, _rm_split ]
|
||
|
|
||
|
def _rm_list(list):
|
||
|
return [l for l in list if l not in ('$(', '$)')]
|
||
|
|
||
|
def _remove_list(list):
|
||
|
result = []
|
||
|
depth = 0
|
||
|
for l in list:
|
||
|
if l == '$(':
|
||
|
depth += 1
|
||
|
elif l == '$)':
|
||
|
depth -= 1
|
||
|
if depth < 0:
|
||
|
break
|
||
|
elif depth == 0:
|
||
|
result.append(l)
|
||
|
if depth != 0:
|
||
|
return None
|
||
|
return result
|
||
|
|
||
|
# Indexed by the SUBST_* constants above.
|
||
|
_list_remove = [ _rm_list, None, _remove_list ]
|
||
|
|
||
|
# Regular expressions for splitting strings and handling substitutions,
|
||
|
# for use by the scons_subst() and scons_subst_list() functions:
|
||
|
#
|
||
|
# The first expression compiled matches all of the $-introduced tokens
|
||
|
# that we need to process in some way, and is used for substitutions.
|
||
|
# The expressions it matches are:
|
||
|
#
|
||
|
# "$$"
|
||
|
# "$("
|
||
|
# "$)"
|
||
|
# "$variable" [must begin with alphabetic or underscore]
|
||
|
# "${any stuff}"
|
||
|
#
|
||
|
# The second expression compiled is used for splitting strings into tokens
|
||
|
# to be processed, and it matches all of the tokens listed above, plus
|
||
|
# the following that affect how arguments do or don't get joined together:
|
||
|
#
|
||
|
# " " [white space]
|
||
|
# "non-white-space" [without any dollar signs]
|
||
|
# "$" [single dollar sign]
|
||
|
#
|
||
|
_dollar_exps_str = r'\$[\$\(\)]|\$[_a-zA-Z][\.\w]*|\${[^}]*}'
|
||
|
_dollar_exps = re.compile(r'(%s)' % _dollar_exps_str)
|
||
|
_separate_args = re.compile(r'(%s|\s+|[^\s$]+|\$)' % _dollar_exps_str)
|
||
|
|
||
|
# This regular expression is used to replace strings of multiple white
|
||
|
# space characters in the string result from the scons_subst() function.
|
||
|
_space_sep = re.compile(r'[\t ]+(?![^{]*})')
|
||
|
|
||
|
|
||
|
def scons_subst(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None, overrides=False):
|
||
|
"""Expand a string or list containing construction variable
|
||
|
substitutions.
|
||
|
|
||
|
This is the work-horse function for substitutions in file names
|
||
|
and the like. The companion scons_subst_list() function (below)
|
||
|
handles separating command lines into lists of arguments, so see
|
||
|
that function if that's what you're looking for.
|
||
|
"""
|
||
|
if (isinstance(strSubst, str) and '$' not in strSubst) or isinstance(strSubst, CmdStringHolder):
|
||
|
return strSubst
|
||
|
|
||
|
if conv is None:
|
||
|
conv = _strconv[mode]
|
||
|
|
||
|
# Doing this every time is a bit of a waste, since the Executor
|
||
|
# has typically already populated the OverrideEnvironment with
|
||
|
# $TARGET/$SOURCE variables. We're keeping this (for now), though,
|
||
|
# because it supports existing behavior that allows us to call
|
||
|
# an Action directly with an arbitrary target+source pair, which
|
||
|
# we use in Tool/tex.py to handle calling $BIBTEX when necessary.
|
||
|
# If we dropped that behavior (or found another way to cover it),
|
||
|
# we could get rid of this call completely and just rely on the
|
||
|
# Executor setting the variables.
|
||
|
if 'TARGET' not in lvars:
|
||
|
d = subst_dict(target, source)
|
||
|
if d:
|
||
|
lvars = lvars.copy()
|
||
|
lvars.update(d)
|
||
|
|
||
|
# Allow last ditch chance to override lvars
|
||
|
if overrides:
|
||
|
lvars.update(overrides)
|
||
|
|
||
|
# We're (most likely) going to eval() things. If Python doesn't
|
||
|
# find a __builtins__ value in the global dictionary used for eval(),
|
||
|
# it copies the current global values for you. Avoid this by
|
||
|
# setting it explicitly and then deleting, so we don't pollute the
|
||
|
# construction environment Dictionary(ies) that are typically used
|
||
|
# for expansion.
|
||
|
gvars['__builtins__'] = __builtins__
|
||
|
|
||
|
ss = StringSubber(env, mode, conv, gvars)
|
||
|
result = ss.substitute(strSubst, lvars)
|
||
|
|
||
|
try:
|
||
|
del gvars['__builtins__']
|
||
|
except KeyError:
|
||
|
pass
|
||
|
|
||
|
res = result
|
||
|
if is_String(result):
|
||
|
# Remove $(-$) pairs and any stuff in between,
|
||
|
# if that's appropriate.
|
||
|
remove = _regex_remove[mode]
|
||
|
if remove:
|
||
|
if mode == SUBST_SIG:
|
||
|
result = _list_remove[mode](remove.split(result))
|
||
|
if result is None:
|
||
|
raise SCons.Errors.UserError("Unbalanced $(/$) in: " + res)
|
||
|
result = ' '.join(result)
|
||
|
else:
|
||
|
result = remove.sub('', result)
|
||
|
if mode != SUBST_RAW:
|
||
|
# Compress strings of white space characters into
|
||
|
# a single space.
|
||
|
result = _space_sep.sub(' ', result).strip()
|
||
|
|
||
|
# Now replace escaped $'s currently "$$"
|
||
|
# This is needed because we now retain $$ instead of
|
||
|
# replacing them during substition to avoid
|
||
|
# improperly trying to escape "$$(" as being "$("
|
||
|
result = result.replace('$$','$')
|
||
|
elif is_Sequence(result):
|
||
|
remove = _list_remove[mode]
|
||
|
if remove:
|
||
|
result = remove(result)
|
||
|
if result is None:
|
||
|
raise SCons.Errors.UserError("Unbalanced $(/$) in: " + str(res))
|
||
|
|
||
|
return result
|
||
|
|
||
|
def scons_subst_list(strSubst, env, mode=SUBST_RAW, target=None, source=None, gvars={}, lvars={}, conv=None,overrides=False):
|
||
|
"""Substitute construction variables in a string (or list or other
|
||
|
object) and separate the arguments into a command list.
|
||
|
|
||
|
The companion scons_subst() function (above) handles basic
|
||
|
substitutions within strings, so see that function instead
|
||
|
if that's what you're looking for.
|
||
|
"""
|
||
|
if conv is None:
|
||
|
conv = _strconv[mode]
|
||
|
|
||
|
# Doing this every time is a bit of a waste, since the Executor
|
||
|
# has typically already populated the OverrideEnvironment with
|
||
|
# $TARGET/$SOURCE variables. We're keeping this (for now), though,
|
||
|
# because it supports existing behavior that allows us to call
|
||
|
# an Action directly with an arbitrary target+source pair, which
|
||
|
# we use in Tool/tex.py to handle calling $BIBTEX when necessary.
|
||
|
# If we dropped that behavior (or found another way to cover it),
|
||
|
# we could get rid of this call completely and just rely on the
|
||
|
# Executor setting the variables.
|
||
|
if 'TARGET' not in lvars:
|
||
|
d = subst_dict(target, source)
|
||
|
if d:
|
||
|
lvars = lvars.copy()
|
||
|
lvars.update(d)
|
||
|
|
||
|
# Allow caller to specify last ditch override of lvars
|
||
|
if overrides:
|
||
|
lvars.update(overrides)
|
||
|
|
||
|
# We're (most likely) going to eval() things. If Python doesn't
|
||
|
# find a __builtins__ value in the global dictionary used for eval(),
|
||
|
# it copies the current global values for you. Avoid this by
|
||
|
# setting it explicitly and then deleting, so we don't pollute the
|
||
|
# construction environment Dictionary(ies) that are typically used
|
||
|
# for expansion.
|
||
|
gvars['__builtins__'] = __builtins__
|
||
|
|
||
|
ls = ListSubber(env, mode, conv, gvars)
|
||
|
ls.substitute(strSubst, lvars, 0)
|
||
|
|
||
|
try:
|
||
|
del gvars['__builtins__']
|
||
|
except KeyError:
|
||
|
pass
|
||
|
|
||
|
return ls.data
|
||
|
|
||
|
def scons_subst_once(strSubst, env, key):
|
||
|
"""Perform single (non-recursive) substitution of a single
|
||
|
construction variable keyword.
|
||
|
|
||
|
This is used when setting a variable when copying or overriding values
|
||
|
in an Environment. We want to capture (expand) the old value before
|
||
|
we override it, so people can do things like:
|
||
|
|
||
|
env2 = env.Clone(CCFLAGS = '$CCFLAGS -g')
|
||
|
|
||
|
We do this with some straightforward, brute-force code here...
|
||
|
"""
|
||
|
if isinstance(strSubst, str) and strSubst.find('$') < 0:
|
||
|
return strSubst
|
||
|
|
||
|
matchlist = ['$' + key, '${' + key + '}']
|
||
|
val = env.get(key, '')
|
||
|
def sub_match(match, val=val, matchlist=matchlist):
|
||
|
a = match.group(1)
|
||
|
if a in matchlist:
|
||
|
a = val
|
||
|
if is_Sequence(a):
|
||
|
return ' '.join(map(str, a))
|
||
|
else:
|
||
|
return str(a)
|
||
|
|
||
|
if is_Sequence(strSubst):
|
||
|
result = []
|
||
|
for arg in strSubst:
|
||
|
if is_String(arg):
|
||
|
if arg in matchlist:
|
||
|
arg = val
|
||
|
if is_Sequence(arg):
|
||
|
result.extend(arg)
|
||
|
else:
|
||
|
result.append(arg)
|
||
|
else:
|
||
|
result.append(_dollar_exps.sub(sub_match, arg))
|
||
|
else:
|
||
|
result.append(arg)
|
||
|
return result
|
||
|
elif is_String(strSubst):
|
||
|
return _dollar_exps.sub(sub_match, strSubst)
|
||
|
else:
|
||
|
return strSubst
|
||
|
|
||
|
# Local Variables:
|
||
|
# tab-width:4
|
||
|
# indent-tabs-mode:nil
|
||
|
# End:
|
||
|
# vim: set expandtab tabstop=4 shiftwidth=4:
|