- Add a --debug=presub option to print actions prior to substitution.
+ - Add a warning upon use of the override keywords "targets" and
+ "sources" when calling Builders. These are usually mistakes which
+ are otherwise silently (and confusingly) turned into construction
+ variable overrides.
+
From Simon Perkins:
- Fix a bug introduced in building shared libraries under MinGW.
Builder object subsystem.
A Builder object is a callable that encapsulates information about how
-to execute actions to create a Node (file) from other Nodes (files), and
-how to create those dependencies for tracking.
+to execute actions to create a target Node (file) from source Nodes
+(files), and how to create those dependencies for tracking.
-The main entry point here is the Builder() factory method. This
-provides a procedural interface that creates the right underlying
-Builder object based on the keyword arguments supplied and the types of
-the arguments.
+The main entry point here is the Builder() factory method. This provides
+a procedural interface that creates the right underlying Builder object
+based on the keyword arguments supplied and the types of the arguments.
The goal is for this external interface to be simple enough that the
vast majority of users can create new Builders as necessary to support
building new types of files in their configurations, without having to
dive any deeper into this subsystem.
+The base class here is BuilderBase. This is a concrete base class which
+does, in fact, represent most Builder objects that we (or users) create.
+
+There is (at present) one subclasses:
+
+ MultiStepBuilder
+
+ This is a Builder that knows how to "chain" Builders so that
+ users can specify a source file that requires multiple steps
+ to turn into a target file. A canonical example is building a
+ program from yacc input file, which requires invoking a builder
+ to turn the .y into a .c, the .c into a .o, and the .o into an
+ executable program.
+
+There is also two proxies that look like Builders:
+
+ CompositeBuilder
+
+ This proxies for a Builder with an action that is actually a
+ dictionary that knows how to map file suffixes to a specific
+ action. This is so that we can invoke different actions
+ (compilers, compile options) for different flavors of source
+ files.
+
+ ListBuilder
+
+ This proxies for a Builder *invocation* where the target
+ is a list of files, not a single file.
+
+Builders and their proxies have the following public interface methods
+used by other modules:
+
+ __call__()
+ THE public interface. Calling a Builder object (with the
+ use of internal helper methods) sets up the target and source
+ dependencies, appropriate mapping to a specific action, and the
+ environment manipulation necessary for overridden construction
+ variable. This also takes care of warning about possible mistakes
+ in keyword arguments.
+
+ targets()
+ Returns the list of targets for a specific builder instance.
+
+ add_emitter()
+ Adds an emitter for a specific file suffix, used by some Tool
+ modules to specify that (for example) a yacc invocation on a .y
+ can create a .h *and* a .c file.
+
+ add_action()
+ Adds an action for a specific file suffix, heavily used by
+ Tool modules to add their specific action(s) for turning
+ a source file into an object file to the global static
+ and shared object file Builders.
+
+There are the following methods for internal use within this module:
+
+ _execute()
+ The internal method that handles the heavily lifting when a
+ Builder is called. This is used so that the __call__() methods
+ can set up warning about possible mistakes in keyword-argument
+ overrides, and *then* execute all of the steps necessary so that
+ the warnings only occur once.
+
+ get_name()
+ Returns the Builder's name within a specific Environment,
+ primarily used to try to return helpful information in error
+ messages.
+
+ adjust_suffix()
+ get_prefix()
+ get_suffix()
+ get_src_suffix()
+ set_src_suffix()
+ Miscellaneous stuff for handling the prefix and suffix
+ manipulation we use in turning source file names into target
+ file names.
+
"""
#
target, source = e(target, source, env)
return (target, source)
+# These are a common errors when calling a Builder;
+# they are similar to the 'target' and 'source' keyword args to builders,
+# so we issue warnings when we see them. The warnings can, of course,
+# be disabled.
+misleading_keywords = {
+ 'targets' : 'target',
+ 'sources' : 'source',
+}
+
+class OverrideWarner(UserDict.UserDict):
+ """A class for warning about keyword arguments that we use as
+ overrides in a Builder call.
+
+ This class exists to handle the fact that a single MultiStepBuilder
+ call can actually invoke multiple builders as a result of a single
+ user-level Builder call. This class only emits the warnings once,
+ no matter how many Builders are invoked.
+ """
+ def __init__(self, dict):
+ UserDict.UserDict.__init__(self, dict)
+ self.already_warned = None
+ def warn(self):
+ if self.already_warned:
+ return
+ for k in self.keys():
+ try:
+ alt = misleading_keywords[k]
+ except KeyError:
+ pass
+ else:
+ SCons.Warnings.warn(SCons.Warnings.MisleadingKeywordsWarning,
+ "Did you mean to use `%s' instead of `%s'?" % (alt, k))
+ self.already_warned = 1
+
def Builder(**kw):
"""A factory for builder objects."""
composite = None
def splitext(self, path):
return SCons.Util.splitext(path)
- def _create_nodes(self, env, overrides, target = None, source = None):
+ def _create_nodes(self, env, overwarn, target = None, source = None):
"""Create and return lists of target and source nodes.
"""
def _adjustixes(files, pre, suf):
result.append(f)
return result
- env = env.Override(overrides)
+ overwarn.warn()
+
+ env = env.Override(overwarn.data)
src_suf = self.get_src_suffix(env)
return tlist, slist
- def __call__(self, env, target = None, source = _null, **overrides):
+ def _execute(self, env, target = None, source = _null, overwarn={}):
if source is _null:
source = target
target = None
- tlist, slist = self._create_nodes(env, overrides, target, source)
+ tlist, slist = self._create_nodes(env, overwarn, target, source)
if len(tlist) == 1:
- _init_nodes(self, env, overrides, tlist, slist)
- tlist = tlist[0]
+ builder = self
+ result = tlist[0]
else:
- _init_nodes(ListBuilder(self, env, tlist), env, overrides, tlist, slist)
+ builder = ListBuilder(self, env, tlist)
+ result = tlist
+ _init_nodes(builder, env, overwarn.data, tlist, slist)
- return tlist
+ return result
+
+ def __call__(self, env, target = None, source = _null, **kw):
+ return self._execute(env, target, source, OverrideWarner(kw))
def adjust_suffix(self, suff):
if suff and not suff[0] in [ '.', '_', '$' ]:
self.sdict = {}
self.cached_src_suffixes = {} # source suffixes keyed on id(env)
- def __call__(self, env, target = None, source = _null, **kw):
+ def _execute(self, env, target = None, source = _null, overwarn={}):
if source is _null:
source = target
target = None
for snode in slist:
base, ext = self.splitext(str(snode))
if sdict.has_key(ext):
- tgt = apply(sdict[ext], (env, None, snode), kw)
+ tgt = sdict[ext]._execute(env, None, snode, overwarn)
# Only supply the builder with sources it is capable
# of building.
if SCons.Util.is_List(tgt):
else:
final_sources.append(snode)
- return apply(BuilderBase.__call__,
- (self, env, target, final_sources), kw)
+ return BuilderBase._execute(self, env, target, final_sources, overwarn)
def get_src_builders(self, env):
"""Return all the src_builders for this Builder.
target = builder(env, source='n21')
assert target.name == 'p-n21.s', target
+ def test_mistaken_variables(self):
+ """Test keyword arguments that are often mistakes
+ """
+ import SCons.Warnings
+ env = Environment()
+ builder = SCons.Builder.Builder(action="foo")
+
+ save_warn = SCons.Warnings.warn
+ warned = []
+ def my_warn(exception, warning, warned=warned):
+ warned.append(warning)
+ SCons.Warnings.warn = my_warn
+
+ try:
+ target = builder(env, 'mistaken1', sources='mistaken1.c')
+ assert warned == ["Did you mean to use `source' instead of `sources'?"], warned
+ del warned[:]
+
+ target = builder(env, 'mistaken2', targets='mistaken2.c')
+ assert warned == ["Did you mean to use `target' instead of `targets'?"], warned
+ del warned[:]
+
+ target = builder(env, 'mistaken3', targets='mistaken3', sources='mistaken3.c')
+ assert "Did you mean to use `source' instead of `sources'?" in warned, warned
+ assert "Did you mean to use `target' instead of `targets'?" in warned, warned
+ del warned[:]
+ finally:
+ SCons.Warnings.warn = save_warn
+
def test_action(self):
"""Test Builder creation
else:
tool(env)
+# These names are controlled by SCons; users should never set or override
+# them. This warning can optionally be turned off, but scons will still
+# ignore the illegal variable names even if it's off.
+reserved_construction_var_names = \
+ ['TARGET', 'TARGETS', 'SOURCE', 'SOURCES']
+
+def copy_non_reserved_keywords(dict):
+ result = our_deepcopy(dict)
+ for k in result.keys():
+ if k in reserved_construction_var_names:
+ SCons.Warnings.warn(SCons.Warnings.ReservedVariableWarning,
+ "Ignoring attempt to set reserved variable `%s'" % k)
+ del result[k]
+ return result
+
class BuilderWrapper:
"""Wrapper class that associates an environment with a Builder at
instantiation."""
return self._dict[key]
def __setitem__(self, key, value):
- if key in ['TARGET', 'TARGETS', 'SOURCE', 'SOURCES']:
+ if key in reserved_construction_var_names:
SCons.Warnings.warn(SCons.Warnings.ReservedVariableWarning,
"Ignoring attempt to set reserved variable `%s'" % key)
elif key == 'BUILDERS':
"""Append values to existing construction variables
in an Environment.
"""
- kw = our_deepcopy(kw)
+ kw = copy_non_reserved_keywords(kw)
for key, val in kw.items():
# It would be easier on the eyes to write this using
# "continue" statements whenever we finish processing an item,
"""Append values to existing construction variables
in an Environment, if they're not already there.
"""
- kw = our_deepcopy(kw)
+ kw = copy_non_reserved_keywords(kw)
for key, val in kw.items():
if not self._dict.has_key(key):
self._dict[key] = val
apply_tools(clone, tools, toolpath)
# Apply passed-in variables after the new tools.
+ kw = copy_non_reserved_keywords(kw)
new = {}
for key, value in kw.items():
new[key] = SCons.Util.scons_subst_once(value, self, key)
env = copy.copy(self)
env._dict = copy.copy(self._dict)
env['__env__'] = env
+ overrides = copy_non_reserved_keywords(overrides)
new = {}
for key, value in overrides.items():
new[key] = SCons.Util.scons_subst_once(value, self, key)
"""Prepend values to existing construction variables
in an Environment.
"""
- kw = our_deepcopy(kw)
+ kw = copy_non_reserved_keywords(kw)
for key, val in kw.items():
# It would be easier on the eyes to write this using
# "continue" statements whenever we finish processing an item,
"""Append values to existing construction variables
in an Environment, if they're not already there.
"""
- kw = our_deepcopy(kw)
+ kw = copy_non_reserved_keywords(kw)
for key, val in kw.items():
if not self._dict.has_key(key):
self._dict[key] = val
self.__setitem__('BUILDERS', kwbd)
except KeyError:
pass
+ kw = copy_non_reserved_keywords(kw)
self._dict.update(our_deepcopy(kw))
def ReplaceIxes(self, path, old_prefix, old_suffix, new_prefix, new_suffix):
f = env.xxx('$FOO')
assert f == 'foo', f
+ def test_bad_keywords(type):
+ """Test trying to use reserved keywords in an Environment"""
+ reserved = ['TARGETS','SOURCES', 'SOURCE','TARGET']
+ added = []
+
+ env = SCons.Environment.Environment(TARGETS = 'targets',
+ SOURCES = 'sources',
+ SOURCE = 'source',
+ TARGET = 'target',
+ INIT = 'init')
+ added.append('INIT')
+ for x in reserved:
+ assert not env.has_key(x), env[x]
+ for x in added:
+ assert env.has_key(x), \
+ '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'%x
+
+ env.Append(TARGETS = 'targets',
+ SOURCES = 'sources',
+ SOURCE = 'source',
+ TARGET = 'target',
+ APPEND = 'append')
+ added.append('APPEND')
+ for x in reserved:
+ assert not env.has_key(x), env[x]
+ for x in added:
+ assert env.has_key(x), \
+ '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'%x
+
+ env.AppendUnique(TARGETS = 'targets',
+ SOURCES = 'sources',
+ SOURCE = 'source',
+ TARGET = 'target',
+ APPENDUNIQUE = 'appendunique')
+ added.append('APPENDUNIQUE')
+ for x in reserved:
+ assert not env.has_key(x), env[x]
+ for x in added:
+ assert env.has_key(x), \
+ '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'%x
+
+ env.Prepend(TARGETS = 'targets',
+ SOURCES = 'sources',
+ SOURCE = 'source',
+ TARGET = 'target',
+ PREPEND = 'prepend')
+ added.append('PREPEND')
+ for x in reserved:
+ assert not env.has_key(x), env[x]
+ for x in added:
+ assert env.has_key(x), \
+ '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'%x
+
+ env.Prepend(TARGETS = 'targets',
+ SOURCES = 'sources',
+ SOURCE = 'source',
+ TARGET = 'target',
+ PREPENDUNIQUE = 'prependunique')
+ added.append('PREPENDUNIQUE')
+ for x in reserved:
+ assert not env.has_key(x), env[x]
+ for x in added:
+ assert env.has_key(x), \
+ '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'%x
+
+ env.Replace(TARGETS = 'targets',
+ SOURCES = 'sources',
+ SOURCE = 'source',
+ TARGET = 'target',
+ REPLACE = 'replace')
+ added.append('REPLACE')
+ for x in reserved:
+ assert not env.has_key(x), env[x]
+ for x in added:
+ assert env.has_key(x), \
+ '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'%x
+
+ copy = env.Copy(TARGETS = 'targets',
+ SOURCES = 'sources',
+ SOURCE = 'source',
+ TARGET = 'target',
+ COPY = 'copy')
+ for x in reserved:
+ assert not copy.has_key(x), env[x]
+ for x in added + ['COPY']:
+ assert copy.has_key(x), \
+ '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'%x
+
+ over = env.Override({'TARGETS' : 'targets',
+ 'SOURCES' : 'sources',
+ 'SOURCE' : 'source',
+ 'TARGET' : 'target',
+ 'OVERRIDE' : 'override'})
+ for x in reserved:
+ assert not over.has_key(x), over[x]
+ for x in added + ['OVERRIDE']:
+ assert over.has_key(x), \
+ '%s is not reserved, but got omitted; see Environment.construction_var_name_ok'%x
+
class NoSubstitutionProxyTestCase(unittest.TestCase):
SCons.Warnings.enableWarningClass(SCons.Warnings.DuplicateEnvironmentWarning)
SCons.Warnings.enableWarningClass(SCons.Warnings.MissingSConscriptWarning)
SCons.Warnings.enableWarningClass(SCons.Warnings.NoParallelSupportWarning)
+ # This is good for newbies, and hopefully most everyone else too.
+ SCons.Warnings.enableWarningClass(SCons.Warnings.MisleadingKeywordsWarning)
global ssoptions
ssoptions = SConscriptSettableOptions(options)
class ReservedVariableWarning(Warning):
pass
+class MisleadingKeywordsWarning(Warning):
+ pass
+
_warningAsException = 0
# The below is a list of 2-tuples. The first element is a class object.
test.run(arguments='--warn=no-duplicate-environment file1.out')
+
+test.write('SConstruct', """
+def build(env, target, source):
+ file = open(str(target[0]), 'wb')
+ for s in source:
+ file.write(open(str(s), 'rb').read())
+
+B = Builder(action=build, multi=1)
+env = Environment(BUILDERS = { 'B' : B })
+env.B(targets = 'file3a.out', source = 'file3a.in')
+env.B(target = 'file3b.out', sources = 'file3b.in')
+""")
+
+test.write('file3a.in', 'file3a.in\n')
+test.write('file3b.out', 'file3b.out\n')
+
+test.run(arguments='.',
+ stderr=r"""
+scons: warning: Did you mean to use `target' instead of `targets'\?
+File "SConstruct", line \d+, in .+
+
+scons: warning: Did you mean to use `source' instead of `sources'\?
+File "SConstruct", line \d+, in .+
+""")
+
+test.must_match(['file3a'], 'file3a.in\n')
+test.must_match(['file3b'], 'file3b.out\n')
+
+test.run(arguments='--warn=misleading-keywords .',
+ stderr=r"""
+scons: warning: Did you mean to use `target' instead of `targets'\?
+File "SConstruct", line \d+, in .+
+
+scons: warning: Did you mean to use `source' instead of `sources'\?
+File "SConstruct", line \d+, in .+
+""")
+
+test.run(arguments='--warn=no-misleading-keywords .')
+
+
test.pass_test()
test.up_to_date(arguments='hello.not_exe')
+
+
+test.write('SConstruct', """\
+env = Environment()
+env.Program('goodbye', 'goodbye.c',
+ CC=r'%s mycc.py',
+ LINK=r'%s mylink.py',
+ OBJSUFFIX='.not_obj',
+ PROGSUFFIX='.not_exe',
+ targets='ttt',
+ sources='sss')
+""" % (python, python))
+
+test.write('goodbye.c',"this ain't no c file!\n")
+
+test.write('mycc.py',"""
+open('goodbye.not_obj', 'wt').write('this is no object file!')
+""")
+
+test.write('mylink.py',"""
+open('goodbye.not_exe', 'wt').write('this is not a program!')
+""")
+
+test.run(arguments='goodbye.not_exe', stderr="""\
+
+scons: warning: Did you mean to use `target' instead of `targets'?
+File "SConstruct", line 8, in ?
+
+scons: warning: Did you mean to use `source' instead of `sources'?
+File "SConstruct", line 8, in ?
+""")
+
+assert test.read('goodbye.not_obj') == 'this is no object file!'
+assert test.read('goodbye.not_exe') == 'this is not a program!'
+
+
+
test.pass_test()