http://scons.tigris.org/issues/show_bug.cgi?id=2345
[scons.git] / src / engine / SCons / Builder.py
index 27d93e940da3f9396a946248fb0e47579596bf24..bbf503c07d7b22f31a41ac40e1d442b7ca57d049 100644 (file)
@@ -1,11 +1,82 @@
 """SCons.Builder
 
-XXX
+Builder object subsystem.
+
+A Builder object is a callable that encapsulates information about how
+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 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 the Builder objects that we (or users) create.
+
+There is also a proxy that looks like a Builder:
+
+    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.
+
+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.
+
+    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.
 
 """
 
 #
-# Copyright (c) 2001 Steven Knight
+# __COPYRIGHT__
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -26,456 +97,781 @@ XXX
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 #
+from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
+import collections
 
-
-import os
-import os.path
+import SCons.Action
+from SCons.Debug import logInstanceCreation
+from SCons.Errors import InternalError, UserError
+import SCons.Executor
+import SCons.Memoize
+import SCons.Node
 import SCons.Node.FS
-from SCons.Util import PathList, scons_str2nodes, scons_subst, scons_subst_list, autogenerate
-import string
-import sys
-import types
-from UserList import UserList
-from UserDict import UserDict
-from Errors import UserError
-
-try:
-    from UserString import UserString
-except ImportError:
-    class UserString:
-        pass
-
-
-exitvalmap = {
-    2 : 127,
-    13 : 126,
-}
+import SCons.Util
+import SCons.Warnings
+
+class _Null:
+    pass
+
+_null = _Null
+
+def match_splitext(path, suffixes = []):
+    if suffixes:
+        matchsuf = [S for S in suffixes if path[-len(S):] == S]
+        if matchsuf:
+            suf = max(list(map(None, list(map(len, matchsuf)), matchsuf)))[1]
+            return [path[:-len(suf)], path[-len(suf):]]
+    return SCons.Util.splitext(path)
+
+class DictCmdGenerator(SCons.Util.Selector):
+    """This is a callable class that can be used as a
+    command generator function.  It holds on to a dictionary
+    mapping file suffixes to Actions.  It uses that dictionary
+    to return the proper action based on the file suffix of
+    the source file."""
+
+    def __init__(self, dict=None, source_ext_match=1):
+        SCons.Util.Selector.__init__(self, dict)
+        self.source_ext_match = source_ext_match
+
+    def src_suffixes(self):
+        return self.keys()
+
+    def add_action(self, suffix, action):
+        """Add a suffix-action pair to the mapping.
+        """
+        self[suffix] = action
+
+    def __call__(self, target, source, env, for_signature):
+        if not source:
+            return []
+
+        if self.source_ext_match:
+            suffixes = self.src_suffixes()
+            ext = None
+            for src in map(str, source):
+                my_ext = match_splitext(src, suffixes)[1]
+                if ext and my_ext != ext:
+                    raise UserError("While building `%s' from `%s': Cannot build multiple sources with different extensions: %s, %s"
+                             % (repr(list(map(str, target))), src, ext, my_ext))
+                ext = my_ext
+        else:
+            ext = match_splitext(str(source[0]), self.src_suffixes())[1]
 
-if os.name == 'posix':
+        if not ext:
+            #return ext
+            raise UserError("While building `%s': "
+                            "Cannot deduce file extension from source files: %s"
+                 % (repr(list(map(str, target))), repr(list(map(str, source)))))
 
-    def spawn(cmd, args, env):
-        pid = os.fork()
-        if not pid:
-            # Child process.
-            exitval = 127
-            try:
-                os.execvpe(cmd, args, env)
-            except OSError, e:
-                exitval = exitvalmap[e[0]]
-                sys.stderr.write("scons: %s: %s\n" % (cmd, e[1]))
-            os._exit(exitval)
-        else:
-            # Parent process.
-            pid, stat = os.waitpid(pid, 0)
-            ret = stat >> 8
-           return ret
-
-elif os.name == 'nt':
-
-    def pathsearch(cmd, env):
-       # In order to deal with the fact that 1.5.2 doesn't have
-       # os.spawnvpe(), roll our own PATH search.
-       if os.path.isabs(cmd):
-           if not os.path.exists(cmd):
-               exts = env['PATHEXT']
-               if type(exts) != type([]):
-                   exts = string.split(exts, os.pathsep)
-               for e in exts:
-                   f = cmd + e
-                   if os.path.exists(f):
-                       return f
-       else:
-           path = env['PATH']
-           if type(path) != type([]):
-               path = string.split(path, os.pathsep)
-           exts = env['PATHEXT']
-           if type(exts) != type([]):
-               exts = string.split(exts, os.pathsep)
-           pairs = []
-           for dir in path:
-               for e in exts:
-                   pairs.append((dir, e))
-           for dir, ext in pairs:
-               f = os.path.join(dir, cmd)
-               if not ext is None:
-                   f = f + ext
-               if os.path.exists(f):
-                   return f
-       return cmd
-
-    def spawn(cmd, args, env):
         try:
-            try:
-                ret = os.spawnvpe(os.P_WAIT, cmd, args, env)
-            except AttributeError:
-                cmd = pathsearch(cmd, env)
-                ret = os.spawnve(os.P_WAIT, cmd, args, env)
-        except OSError, e:
-            ret = exitvalmap[e[0]]
-            sys.stderr.write("scons: %s: %s\n" % (cmd, e[1]))
+            ret = SCons.Util.Selector.__call__(self, env, source, ext)
+        except KeyError, e:
+            raise UserError("Ambiguous suffixes after environment substitution: %s == %s == %s" % (e[0], e[1], e[2]))
+        if ret is None:
+            raise UserError("While building `%s' from `%s': Don't know how to build from a source file with suffix `%s'.  Expected a suffix in this list: %s." % \
+                            (repr(list(map(str, target))), repr(list(map(str, source))), ext, repr(self.keys())))
         return ret
 
+class CallableSelector(SCons.Util.Selector):
+    """A callable dictionary that will, in turn, call the value it
+    finds if it can."""
+    def __call__(self, env, source):
+        value = SCons.Util.Selector.__call__(self, env, source)
+        if callable(value):
+            value = value(env, source)
+        return value
+
+class DictEmitter(SCons.Util.Selector):
+    """A callable dictionary that maps file suffixes to emitters.
+    When called, it finds the right emitter in its dictionary for the
+    suffix of the first source file, and calls that emitter to get the
+    right lists of targets and sources to return.  If there's no emitter
+    for the suffix in its dictionary, the original target and source are
+    returned.
+    """
+    def __call__(self, target, source, env):
+        emitter = SCons.Util.Selector.__call__(self, env, source)
+        if emitter:
+            target, source = emitter(target, source, env)
+        return (target, source)
+
+class ListEmitter(collections.UserList):
+    """A callable list of emitters that calls each in sequence,
+    returning the result.
+    """
+    def __call__(self, target, source, env):
+        for e in self.data:
+            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(collections.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 Builder call
+    can actually invoke multiple builders.  This class only emits the
+    warnings once, no matter how many Builders are invoked.
+    """
+    def __init__(self, dict):
+        collections.UserDict.__init__(self, dict)
+        if __debug__: logInstanceCreation(self, 'Builder.OverrideWarner')
+        self.already_warned = None
+    def warn(self):
+        if self.already_warned:
+            return
+        for k in self.keys():
+            if k in misleading_keywords:
+                alt = misleading_keywords[k]
+                msg = "Did you mean to use `%s' instead of `%s'?" % (alt, k)
+                SCons.Warnings.warn(SCons.Warnings.MisleadingKeywordsWarning, msg)
+        self.already_warned = 1
 
 def Builder(**kw):
     """A factory for builder objects."""
-    if kw.has_key('src_builder'):
-        return apply(MultiStepBuilder, (), kw)
-    elif kw.has_key('action') and (type(kw['action']) is types.DictType or
-                                   isinstance(kw['action'], UserDict)):
-        return apply(CompositeBuilder, (), kw)
-    else:
-        return apply(BuilderBase, (), kw)
+    composite = None
+    if 'generator' in kw:
+        if 'action' in kw:
+            raise UserError("You must not specify both an action and a generator.")
+        kw['action'] = SCons.Action.CommandGeneratorAction(kw['generator'], {})
+        del kw['generator']
+    elif 'action' in kw:
+        source_ext_match = kw.get('source_ext_match', 1)
+        if 'source_ext_match' in kw:
+            del kw['source_ext_match']
+        if SCons.Util.is_Dict(kw['action']):
+            composite = DictCmdGenerator(kw['action'], source_ext_match)
+            kw['action'] = SCons.Action.CommandGeneratorAction(composite, {})
+            kw['src_suffix'] = composite.src_suffixes()
+        else:
+            kw['action'] = SCons.Action.Action(kw['action'])
+
+    if 'emitter' in kw:
+        emitter = kw['emitter']
+        if SCons.Util.is_String(emitter):
+            # This allows users to pass in an Environment
+            # variable reference (like "$FOO") as an emitter.
+            # We will look in that Environment variable for
+            # a callable to use as the actual emitter.
+            var = SCons.Util.get_environment_var(emitter)
+            if not var:
+                raise UserError("Supplied emitter '%s' does not appear to refer to an Environment variable" % emitter)
+            kw['emitter'] = EmitterProxy(var)
+        elif SCons.Util.is_Dict(emitter):
+            kw['emitter'] = DictEmitter(emitter)
+        elif SCons.Util.is_List(emitter):
+            kw['emitter'] = ListEmitter(emitter)
+
+    result = BuilderBase(**kw)
+
+    if not composite is None:
+        result = CompositeBuilder(result, composite)
+
+    return result
+
+def _node_errors(builder, env, tlist, slist):
+    """Validate that the lists of target and source nodes are
+    legal for this builder and environment.  Raise errors or
+    issue warnings as appropriate.
+    """
+
+    # First, figure out if there are any errors in the way the targets
+    # were specified.
+    for t in tlist:
+        if t.side_effect:
+            raise UserError("Multiple ways to build the same target were specified for: %s" % t)
+        if t.has_explicit_builder():
+            if not t.env is None and not t.env is env:
+                action = t.builder.action
+                t_contents = action.get_contents(tlist, slist, t.env)
+                contents = action.get_contents(tlist, slist, env)
+
+                if t_contents == contents:
+                    msg = "Two different environments were specified for target %s,\n\tbut they appear to have the same action: %s" % (t, action.genstring(tlist, slist, t.env))
+                    SCons.Warnings.warn(SCons.Warnings.DuplicateEnvironmentWarning, msg)
+                else:
+                    msg = "Two environments with different actions were specified for the same target: %s" % t
+                    raise UserError(msg)
+            if builder.multi:
+                if t.builder != builder:
+                    msg = "Two different builders (%s and %s) were specified for the same target: %s" % (t.builder.get_name(env), builder.get_name(env), t)
+                    raise UserError(msg)
+                # TODO(batch):  list constructed each time!
+                if t.get_executor().get_all_targets() != tlist:
+                    msg = "Two different target lists have a target in common: %s  (from %s and from %s)" % (t, list(map(str, t.get_executor().get_all_targets())), list(map(str, tlist)))
+                    raise UserError(msg)
+            elif t.sources != slist:
+                msg = "Multiple ways to build the same target were specified for: %s  (from %s and from %s)" % (t, list(map(str, t.sources)), list(map(str, slist)))
+                raise UserError(msg)
+
+    if builder.single_source:
+        if len(slist) > 1:
+            raise UserError("More than one source given for single-source builder: targets=%s sources=%s" % (list(map(str,tlist)), list(map(str,slist))))
+
+class EmitterProxy:
+    """This is a callable class that can act as a
+    Builder emitter.  It holds on to a string that
+    is a key into an Environment dictionary, and will
+    look there at actual build time to see if it holds
+    a callable.  If so, we will call that as the actual
+    emitter."""
+    def __init__(self, var):
+        self.var = SCons.Util.to_String(var)
+
+    def __call__(self, target, source, env):
+        emitter = self.var
+
+        # Recursively substitute the variable.
+        # We can't use env.subst() because it deals only
+        # in strings.  Maybe we should change that?
+        while SCons.Util.is_String(emitter) and emitter in env:
+            emitter = env[emitter]
+        if callable(emitter):
+            target, source = emitter(target, source, env)
+        elif SCons.Util.is_List(emitter):
+            for e in emitter:
+                target, source = e(target, source, env)
+
+        return (target, source)
 
 
+    def __cmp__(self, other):
+        return cmp(self.var, other.var)
 
 class BuilderBase:
     """Base class for Builders, objects that create output
     nodes (files) from input nodes (files).
     """
 
-    def __init__(self, name = None,
-                       action = None,
-                       prefix = '',
-                       suffix = '',
-                       src_suffix = '',
-                        node_factory = SCons.Node.FS.default_fs.File,
-                        scanner = None):
-       self.name = name
-       self.action = Action(action)
-
-       self.prefix = prefix
-       self.suffix = suffix
-       self.src_suffix = src_suffix
-       self.node_factory = node_factory
-        self.scanner = scanner
-        if self.suffix and self.suffix[0] not in '.$':
-           self.suffix = '.' + self.suffix
-        if self.src_suffix and self.src_suffix[0] not in '.$':
-           self.src_suffix = '.' + self.src_suffix
+    if SCons.Memoize.use_memoizer:
+        __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
+    memoizer_counters = []
+
+    def __init__(self,  action = None,
+                        prefix = '',
+                        suffix = '',
+                        src_suffix = '',
+                        target_factory = None,
+                        source_factory = None,
+                        target_scanner = None,
+                        source_scanner = None,
+                        emitter = None,
+                        multi = 0,
+                        env = None,
+                        single_source = 0,
+                        name = None,
+                        chdir = _null,
+                        is_explicit = 1,
+                        src_builder = None,
+                        ensure_suffix = False,
+                        **overrides):
+        if __debug__: logInstanceCreation(self, 'Builder.BuilderBase')
+        self._memo = {}
+        self.action = action
+        self.multi = multi
+        if SCons.Util.is_Dict(prefix):
+            prefix = CallableSelector(prefix)
+        self.prefix = prefix
+        if SCons.Util.is_Dict(suffix):
+            suffix = CallableSelector(suffix)
+        self.env = env
+        self.single_source = single_source
+        if 'overrides' in overrides:
+            SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
+                "The \"overrides\" keyword to Builder() creation has been deprecated;\n" +\
+                "\tspecify the items as keyword arguments to the Builder() call instead.")
+            overrides.update(overrides['overrides'])
+            del overrides['overrides']
+        if 'scanner' in overrides:
+            SCons.Warnings.warn(SCons.Warnings.DeprecatedWarning,
+                                "The \"scanner\" keyword to Builder() creation has been deprecated;\n"
+                                "\tuse: source_scanner or target_scanner as appropriate.")
+            del overrides['scanner']
+        self.overrides = overrides
+
+        self.set_suffix(suffix)
+        self.set_src_suffix(src_suffix)
+        self.ensure_suffix = ensure_suffix
+
+        self.target_factory = target_factory
+        self.source_factory = source_factory
+        self.target_scanner = target_scanner
+        self.source_scanner = source_scanner
+
+        self.emitter = emitter
+
+        # Optional Builder name should only be used for Builders
+        # that don't get attached to construction environments.
+        if name:
+            self.name = name
+        self.executor_kw = {}
+        if not chdir is _null:
+            self.executor_kw['chdir'] = chdir
+        self.is_explicit = is_explicit
+
+        if src_builder is None:
+            src_builder = []
+        elif not SCons.Util.is_List(src_builder):
+            src_builder = [ src_builder ]
+        self.src_builder = src_builder
+
+    def __nonzero__(self):
+        raise InternalError("Do not test for the Node.builder attribute directly; use Node.has_builder() instead")
+
+    def get_name(self, env):
+        """Attempts to get the name of the Builder.
+
+        Look at the BUILDERS variable of env, expecting it to be a
+        dictionary containing this Builder, and return the key of the
+        dictionary.  If there's no key, then return a directly-configured
+        name (if there is one) or the name of the class (by default)."""
+
+        try:
+            index = env['BUILDERS'].values().index(self)
+            return env['BUILDERS'].keys()[index]
+        except (AttributeError, KeyError, TypeError, ValueError):
+            try:
+                return self.name
+            except AttributeError:
+                return str(self.__class__)
 
     def __cmp__(self, other):
-       return cmp(self.__dict__, other.__dict__)
-
-    def __call__(self, env, target = None, source = None):
-       def adjustixes(files, pre, suf):
-           ret = []
-            if type(files) is types.StringType or isinstance(files, UserString):
-                files = string.split(files)
-           if not type(files) is type([]):
-               files = [files]
-           for f in files:
-                if type(f) is types.StringType or isinstance(f, UserString):
-                   if pre and f[:len(pre)] != pre:
-                        path, fn = os.path.split(os.path.normpath(f))
-                        f = os.path.join(path, pre + fn)
-                   if suf:
-                       if f[-len(suf):] != suf:
-                           f = f + suf
-               ret.append(f)
-           return ret
-
-       tlist = scons_str2nodes(adjustixes(target,
-                                           env.subst(self.prefix),
-                                           env.subst(self.suffix)),
-                                self.node_factory)
-
-       slist = scons_str2nodes(adjustixes(source, None,
-                                           env.subst(self.src_suffix)),
-                                self.node_factory)
-
-       for t in tlist:
-            t.cwd = SCons.Node.FS.default_fs.getcwd()  # XXX
-           t.builder_set(self)
-           t.env_set(env)
-           t.add_source(slist)
-            if self.scanner:
-                t.scanner_set(self.scanner.instance(env))
-
-       for s in slist:
-           s.env_set(env, 1)
-            scanner = env.get_scanner(os.path.splitext(s.name)[1])
-            if scanner:
-                s.scanner_set(scanner.instance(env))
-
-       if len(tlist) == 1:
-           tlist = tlist[0]
-       return tlist
-
-    def execute(self, **kw):
-       """Execute a builder's action to create an output object.
-       """
-       return apply(self.action.execute, (), kw)
-
-    def get_contents(self, **kw):
-        """Fetch the "contents" of the builder's action
-        (for signature calculation).
+        return cmp(self.__dict__, other.__dict__)
+
+    def splitext(self, path, env=None):
+        if not env:
+            env = self.env
+        if env:
+            suffixes = self.src_suffixes(env)
+        else:
+            suffixes = []
+        return match_splitext(path, suffixes)
+
+    def _adjustixes(self, files, pre, suf, ensure_suffix=False):
+        if not files:
+            return []
+        result = []
+        if not SCons.Util.is_List(files):
+            files = [files]
+
+        for f in files:
+            if SCons.Util.is_String(f):
+                f = SCons.Util.adjustixes(f, pre, suf, ensure_suffix)
+            result.append(f)
+        return result
+
+    def _create_nodes(self, env, target = None, source = None):
+        """Create and return lists of target and source nodes.
         """
-        return apply(self.action.get_contents, (), kw)
-
-class MultiStepBuilder(BuilderBase):
-    """This is a builder subclass that can build targets in
-    multiple steps.  The src_builder parameter to the constructor
-    accepts a builder that is called to build sources supplied to
-    this builder.  The targets of that first build then become
-    the sources of this builder.
-
-    If this builder has a src_suffix supplied, then the src_builder
-    builder is NOT invoked if the suffix of a source file matches
-    src_suffix.
-    """
-    def __init__(self,  src_builder,
-                        name = None,
-                       action = None,
-                       prefix = '',
-                       suffix = '',
-                       src_suffix = '',
-                        node_factory = SCons.Node.FS.default_fs.File,
-                        scanner=None):
-        BuilderBase.__init__(self, name, action, prefix, suffix, src_suffix,
-                             node_factory, scanner)
-        self.src_builder = src_builder
+        src_suf = self.get_src_suffix(env)
 
-    def __call__(self, env, target = None, source = None):
-        slist = scons_str2nodes(source, self.node_factory)
-        final_sources = []
-        src_suffix = env.subst(self.src_suffix)
-        for snode in slist:
-            path, ext = os.path.splitext(snode.abspath)
-            if not src_suffix or ext != src_suffix:
-                tgt = self.src_builder(env, target = [ path ],
-                                     source=snode)
-                if not type(tgt) is types.ListType:
-                    final_sources.append(tgt)
-                else:
-                    final_sources.extend(tgt)
+        target_factory = env.get_factory(self.target_factory)
+        source_factory = env.get_factory(self.source_factory)
+
+        source = self._adjustixes(source, None, src_suf)
+        slist = env.arg2nodes(source, source_factory)
+
+        pre = self.get_prefix(env, slist)
+        suf = self.get_suffix(env, slist)
+
+        if target is None:
+            try:
+                t_from_s = slist[0].target_from_source
+            except AttributeError:
+                raise UserError("Do not know how to create a target from source `%s'" % slist[0])
+            except IndexError:
+                tlist = []
             else:
-                final_sources.append(snode)
-        return BuilderBase.__call__(self, env, target=target,
-                                    source=final_sources)
-
-class CompositeBuilder(BuilderBase):
-    """This is a convenient Builder subclass that can build different
-    files based on their suffixes.  For each target, this builder
-    will examine the target's sources.  If they are all the same
-    suffix, and that suffix is equal to one of the child builders'
-    src_suffix, then that child builder will be used.  Otherwise,
-    UserError is thrown.
-
-    Child builders are supplied via the builders arg to the
-    constructor.  Each must have its src_suffix set."""
-    def __init__(self,  name = None,
-                        prefix='',
-                        suffix='',
-                        action = {}):
-        BuilderBase.__init__(self, name=name, prefix=prefix,
-                             suffix=suffix)
-        self.builder_dict = {}
-        for suff, act in action.items():
-             self.builder_dict[suff] = BuilderBase(name = name,
-                                                   action = act,
-                                                   src_suffix = suff)
-
-    def __call__(self, env, target = None, source = None):
-        ret = BuilderBase.__call__(self, env, target=target, source=source)
-
-        builder_dict = {}
-        for suffix, bld in self.builder_dict.items():
-            builder_dict[env.subst(bld.src_suffix)] = bld
-
-        if type(ret) is types.ListType:
-            tlist = ret
+                splitext = lambda S: self.splitext(S,env)
+                tlist = [ t_from_s(pre, suf, splitext) ]
         else:
-            tlist = [ ret ]
-        for tnode in tlist:
-            suflist = map(lambda x: os.path.splitext(x.path)[1],
-                          tnode.sources)
-            last_suffix=''
-            for suffix in suflist:
-                if last_suffix and last_suffix != suffix:
-                    raise UserError, "The builder for %s is only capable of building source files of identical suffixes." % tnode.path
-                last_suffix = suffix
-            if last_suffix:
+            target = self._adjustixes(target, pre, suf, self.ensure_suffix)
+            tlist = env.arg2nodes(target, target_factory, target=target, source=source)
+
+        if self.emitter:
+            # The emitter is going to do str(node), but because we're
+            # being called *from* a builder invocation, the new targets
+            # don't yet have a builder set on them and will look like
+            # source files.  Fool the emitter's str() calls by setting
+            # up a temporary builder on the new targets.
+            new_targets = []
+            for t in tlist:
+                if not t.is_derived():
+                    t.builder_set(self)
+                    new_targets.append(t)
+
+            orig_tlist = tlist[:]
+            orig_slist = slist[:]
+
+            target, source = self.emitter(target=tlist, source=slist, env=env)
+
+            # Now delete the temporary builders that we attached to any
+            # new targets, so that _node_errors() doesn't do weird stuff
+            # to them because it thinks they already have builders.
+            for t in new_targets:
+                if t.builder is self:
+                    # Only delete the temporary builder if the emitter
+                    # didn't change it on us.
+                    t.builder_set(None)
+
+            # Have to call arg2nodes yet again, since it is legal for
+            # emitters to spit out strings as well as Node instances.
+            tlist = env.arg2nodes(target, target_factory,
+                                  target=orig_tlist, source=orig_slist)
+            slist = env.arg2nodes(source, source_factory,
+                                  target=orig_tlist, source=orig_slist)
+
+        return tlist, slist
+
+    def _execute(self, env, target, source, overwarn={}, executor_kw={}):
+        # We now assume that target and source are lists or None.
+        if self.src_builder:
+            source = self.src_builder_sources(env, source, overwarn)
+
+        if self.single_source and len(source) > 1 and target is None:
+            result = []
+            if target is None: target = [None]*len(source)
+            for tgt, src in zip(target, source):
+                if not tgt is None: tgt = [tgt]
+                if not src is None: src = [src]
+                result.extend(self._execute(env, tgt, src, overwarn))
+            return SCons.Node.NodeList(result)
+
+        overwarn.warn()
+
+        tlist, slist = self._create_nodes(env, target, source)
+
+        # Check for errors with the specified target/source lists.
+        _node_errors(self, env, tlist, slist)
+
+        # The targets are fine, so find or make the appropriate Executor to
+        # build this particular list of targets from this particular list of
+        # sources.
+
+        executor = None
+        key = None
+
+        if self.multi:
+            try:
+                executor = tlist[0].get_executor(create = 0)
+            except (AttributeError, IndexError):
+                pass
+            else:
+                executor.add_sources(slist)
+
+        if executor is None:
+            if not self.action:
+                fmt = "Builder %s must have an action to build %s."
+                raise UserError(fmt % (self.get_name(env or self.env),
+                                        list(map(str,tlist))))
+            key = self.action.batch_key(env or self.env, tlist, slist)
+            if key:
                 try:
-                    tnode.builder_set(builder_dict[last_suffix])
+                    executor = SCons.Executor.GetBatchExecutor(key)
                 except KeyError:
-                    raise UserError, "Builder not capable of building files with suffix: %s" % suffix
-        return ret
-
-print_actions = 1;
-execute_actions = 1;
-
-def Action(act):
-    """A factory for action objects."""
-    if callable(act):
-       return FunctionAction(act)
-    elif type(act) == types.StringType or isinstance(act, UserString):
-       return CommandAction(act)
-    elif type(act) == types.ListType or isinstance(act, UserList):
-       return ListAction(act)
-    else:
-       return None
-    
-class ActionBase:
-    """Base class for actions that create output objects.
-    
-    We currently expect Actions will only be accessible through
-    Builder objects, so they don't yet merit their own module."""
-    def __cmp__(self, other):
-       return cmp(self.__dict__, other.__dict__)
+                    pass
+                else:
+                    executor.add_batch(tlist, slist)
+
+        if executor is None:
+            executor = SCons.Executor.Executor(self.action, env, [],
+                                               tlist, slist, executor_kw)
+            if key:
+                SCons.Executor.AddBatchExecutor(key, executor)
+
+        # Now set up the relevant information in the target Nodes themselves.
+        for t in tlist:
+            t.cwd = env.fs.getcwd()
+            t.builder_set(self)
+            t.env_set(env)
+            t.add_source(slist)
+            t.set_executor(executor)
+            t.set_explicit(self.is_explicit)
+
+        return SCons.Node.NodeList(tlist)
+
+    def __call__(self, env, target=None, source=None, chdir=_null, **kw):
+        # We now assume that target and source are lists or None.
+        # The caller (typically Environment.BuilderWrapper) is
+        # responsible for converting any scalar values to lists.
+        if chdir is _null:
+            ekw = self.executor_kw
+        else:
+            ekw = self.executor_kw.copy()
+            ekw['chdir'] = chdir
+        if kw:
+            if 'srcdir' in kw:
+                def prependDirIfRelative(f, srcdir=kw['srcdir']):
+                    import os.path
+                    if SCons.Util.is_String(f) and not os.path.isabs(f):
+                        f = os.path.join(srcdir, f)
+                    return f
+                if not SCons.Util.is_List(source):
+                    source = [source]
+                source = list(map(prependDirIfRelative, source))
+                del kw['srcdir']
+            if self.overrides:
+                env_kw = self.overrides.copy()
+                env_kw.update(kw)
+            else:
+                env_kw = kw
+        else:
+            env_kw = self.overrides
+        env = env.Override(env_kw)
+        return self._execute(env, target, source, OverrideWarner(kw), ekw)
+
+    def adjust_suffix(self, suff):
+        if suff and not suff[0] in [ '.', '_', '$' ]:
+            return '.' + suff
+        return suff
+
+    def get_prefix(self, env, sources=[]):
+        prefix = self.prefix
+        if callable(prefix):
+            prefix = prefix(env, sources)
+        return env.subst(prefix)
+
+    def set_suffix(self, suffix):
+        if not callable(suffix):
+            suffix = self.adjust_suffix(suffix)
+        self.suffix = suffix
+
+    def get_suffix(self, env, sources=[]):
+        suffix = self.suffix
+        if callable(suffix):
+            suffix = suffix(env, sources)
+        return env.subst(suffix)
+
+    def set_src_suffix(self, src_suffix):
+        if not src_suffix:
+            src_suffix = []
+        elif not SCons.Util.is_List(src_suffix):
+            src_suffix = [ src_suffix ]
+        self.src_suffix = [callable(suf) and suf or self.adjust_suffix(suf) for suf in src_suffix]
+
+    def get_src_suffix(self, env):
+        """Get the first src_suffix in the list of src_suffixes."""
+        ret = self.src_suffixes(env)
+        if not ret:
+            return ''
+        return ret[0]
+
+    def add_emitter(self, suffix, emitter):
+        """Add a suffix-emitter mapping to this Builder.
+
+        This assumes that emitter has been initialized with an
+        appropriate dictionary type, and will throw a TypeError if
+        not, so the caller is responsible for knowing that this is an
+        appropriate method to call for the Builder in question.
+        """
+        self.emitter[suffix] = emitter
 
-    def show(self, string):
-       print string
+    def add_src_builder(self, builder):
+        """
+        Add a new Builder to the list of src_builders.
 
-    def subst_dict(self, **kw):
-        """Create a dictionary for substitution of construction
-        variables.
+        This requires wiping out cached values so that the computed
+        lists of source suffixes get re-calculated.
+        """
+        self._memo = {}
+        self.src_builder.append(builder)
 
-        This translates the following special arguments:
+    def _get_sdict(self, env):
+        """
+        Returns a dictionary mapping all of the source suffixes of all
+        src_builders of this Builder to the underlying Builder that
+        should be called first.
+
+        This dictionary is used for each target specified, so we save a
+        lot of extra computation by memoizing it for each construction
+        environment.
+
+        Note that this is re-computed each time, not cached, because there
+        might be changes to one of our source Builders (or one of their
+        source Builders, and so on, and so on...) that we can't "see."
+
+        The underlying methods we call cache their computed values,
+        though, so we hope repeatedly aggregating them into a dictionary
+        like this won't be too big a hit.  We may need to look for a
+        better way to do this if performance data show this has turned
+        into a significant bottleneck.
+        """
+        sdict = {}
+        for bld in self.get_src_builders(env):
+            for suf in bld.src_suffixes(env):
+                sdict[suf] = bld
+        return sdict
+
+    def src_builder_sources(self, env, source, overwarn={}):
+        sdict = self._get_sdict(env)
+
+        src_suffixes = self.src_suffixes(env)
+
+        lengths = list(set(map(len, src_suffixes)))
+
+        def match_src_suffix(name, src_suffixes=src_suffixes, lengths=lengths):
+            node_suffixes = [name[-l:] for l in lengths]
+            for suf in src_suffixes:
+                if suf in node_suffixes:
+                    return suf
+            return None
+
+        result = []
+        for s in SCons.Util.flatten(source):
+            if SCons.Util.is_String(s):
+                match_suffix = match_src_suffix(env.subst(s))
+                if not match_suffix and not '.' in s:
+                    src_suf = self.get_src_suffix(env)
+                    s = self._adjustixes(s, None, src_suf)[0]
+            else:
+                match_suffix = match_src_suffix(s.name)
+            if match_suffix:
+                try:
+                    bld = sdict[match_suffix]
+                except KeyError:
+                    result.append(s)
+                else:
+                    tlist = bld._execute(env, None, [s], overwarn)
+                    # If the subsidiary Builder returned more than one
+                    # target, then filter out any sources that this
+                    # Builder isn't capable of building.
+                    if len(tlist) > 1:
+                        tlist = [t for t in tlist if match_src_suffix(t.name)]
+                    result.extend(tlist)
+            else:
+                result.append(s)
 
-            env    - the construction environment itself,
-                     the values of which (CC, CCFLAGS, etc.)
-                     are copied straight into the dictionary
+        source_factory = env.get_factory(self.source_factory)
 
-            target - the target (object or array of objects),
-                     used to generate the TARGET and TARGETS
-                     construction variables
+        return env.arg2nodes(result, source_factory)
 
-            source - the source (object or array of objects),
-                     used to generate the SOURCES construction
-                     variable
+    def _get_src_builders_key(self, env):
+        return id(env)
 
-        Any other keyword arguments are copied into the
-        dictionary."""
+    memoizer_counters.append(SCons.Memoize.CountDict('get_src_builders', _get_src_builders_key))
 
-        dict = {}
-        if kw.has_key('env'):
-            dict.update(kw['env'])
-            del kw['env']
+    def get_src_builders(self, env):
+        """
+        Returns the list of source Builders for this Builder.
 
+        This exists mainly to look up Builders referenced as
+        strings in the 'BUILDER' variable of the construction
+        environment and cache the result.
+        """
+        memo_key = id(env)
         try:
-            cwd = kw['dir']
-        except:
-            cwd = None
+            memo_dict = self._memo['get_src_builders']
+        except KeyError:
+            memo_dict = {}
+            self._memo['get_src_builders'] = memo_dict
         else:
-            del kw['dir']
-
-        if kw.has_key('target'):
-            t = kw['target']
-            del kw['target']
-            if not type(t) is types.ListType and not isinstance(t, UserList):
-                t = [t]
             try:
-                cwd = t[0].cwd
-            except AttributeError:
+                return memo_dict[memo_key]
+            except KeyError:
                 pass
-            dict['TARGETS'] = PathList(map(os.path.normpath, map(str, t)))
-            if dict['TARGETS']:
-                dict['TARGET'] = dict['TARGETS'][0]
-
-        if kw.has_key('source'):
-            s = kw['source']
-            del kw['source']
-            if not type(s) is types.ListType:
-                s = [s]
-            dict['SOURCES'] = PathList(map(os.path.normpath, map(str, s)))
-
-        dict.update(kw)
-
-        # Autogenerate necessary construction variables.
-        autogenerate(dict, dir = cwd)
-
-        return dict
-
-class CommandAction(ActionBase):
-    """Class for command-execution actions."""
-    def __init__(self, string):
-        self.command = string
-
-    def execute(self, **kw):
-        dict = apply(self.subst_dict, (), kw)
-        cmd_list = scons_subst_list(self.command, dict, {})
-        for cmd_line in cmd_list:
-            if len(cmd_line):
-                if print_actions:
-                    self.show(string.join(cmd_line))
-                if execute_actions:
-                    try:
-                        ENV = kw['env']['ENV']
-                    except:
-                        import SCons.Defaults
-                        ENV = SCons.Defaults.ConstructionEnvironment['ENV']
-                    ret = spawn(cmd_line[0], cmd_line, ENV)
-                    if ret:
-                        return ret
-        return 0
-
-    def get_contents(self, **kw):
-        """Return the signature contents of this action's command line.
-
-        For signature purposes, it doesn't matter what targets or
-        sources we use, so long as we use the same ones every time
-        so the signature stays the same.  We supply an array of two
-        of each to allow for distinction between TARGET and TARGETS.
-        """
-        kw['target'] = ['__t1__', '__t2__']
-        kw['source'] = ['__s1__', '__s2__']
-        dict = apply(self.subst_dict, (), kw)
-        return scons_subst(self.command, dict, {})
-
-class FunctionAction(ActionBase):
-    """Class for Python function actions."""
-    def __init__(self, function):
-       self.function = function
-
-    def execute(self, **kw):
-       # if print_actions:
-       # XXX:  WHAT SHOULD WE PRINT HERE?
-       if execute_actions:
-            if kw.has_key('target'):
-                if type(kw['target']) is types.ListType:
-                    kw['target'] = map(str, kw['target'])
-                else:
-                    kw['target'] = str(kw['target'])
-            if kw.has_key('source'):
-                kw['source'] = map(str, kw['source'])
-            return apply(self.function, (), kw)
 
-    def get_contents(self, **kw):
-        """Return the signature contents of this callable action.
+        builders = []
+        for bld in self.src_builder:
+            if SCons.Util.is_String(bld):
+                try:
+                    bld = env['BUILDERS'][bld]
+                except KeyError:
+                    continue
+            builders.append(bld)
+
+        memo_dict[memo_key] = builders
+        return builders
 
-        By providing direct access to the code object of the
-        function, Python makes this extremely easy.  Hooray!
+    def _subst_src_suffixes_key(self, env):
+        return id(env)
+
+    memoizer_counters.append(SCons.Memoize.CountDict('subst_src_suffixes', _subst_src_suffixes_key))
+
+    def subst_src_suffixes(self, env):
+        """
+        The suffix list may contain construction variable expansions,
+        so we have to evaluate the individual strings.  To avoid doing
+        this over and over, we memoize the results for each construction
+        environment.
         """
-        #XXX DOES NOT ACCOUNT FOR CHANGES IN ENVIRONMENT VARIABLES
-        #THE FUNCTION MAY USE
+        memo_key = id(env)
         try:
-            # "self.function" is a function.
-            code = self.function.func_code.co_code
-        except:
-            # "self.function" is a callable object.
-            code = self.function.__call__.im_func.func_code.co_code
-        return str(code)
-
-class ListAction(ActionBase):
-    """Class for lists of other actions."""
-    def __init__(self, list):
-       self.list = map(lambda x: Action(x), list)
-
-    def execute(self, **kw):
-       for l in self.list:
-           r = apply(l.execute, (), kw)
-           if r != 0:
-               return r
-       return 0
-
-    def get_contents(self, **kw):
-        """Return the signature contents of this action list.
-
-        Simple concatenation of the signatures of the elements.
+            memo_dict = self._memo['subst_src_suffixes']
+        except KeyError:
+            memo_dict = {}
+            self._memo['subst_src_suffixes'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
+        suffixes = [env.subst(x) for x in self.src_suffix]
+        memo_dict[memo_key] = suffixes
+        return suffixes
+
+    def src_suffixes(self, env):
         """
+        Returns the list of source suffixes for all src_builders of this
+        Builder.
+
+        This is essentially a recursive descent of the src_builder "tree."
+        (This value isn't cached because there may be changes in a
+        src_builder many levels deep that we can't see.)
+        """
+        sdict = {}
+        suffixes = self.subst_src_suffixes(env)
+        for s in suffixes:
+            sdict[s] = 1
+        for builder in self.get_src_builders(env):
+            for s in builder.src_suffixes(env):
+                if s not in sdict:
+                    sdict[s] = 1
+                    suffixes.append(s)
+        return suffixes
+
+class CompositeBuilder(SCons.Util.Proxy):
+    """A Builder Proxy whose main purpose is to always have
+    a DictCmdGenerator as its action, and to provide access
+    to the DictCmdGenerator's add_action() method.
+    """
 
-        return reduce(lambda x, y: x + str(y.get_contents()), self.list, "")
+    def __init__(self, builder, cmdgen):
+        if __debug__: logInstanceCreation(self, 'Builder.CompositeBuilder')
+        SCons.Util.Proxy.__init__(self, builder)
+
+        # cmdgen should always be an instance of DictCmdGenerator.
+        self.cmdgen = cmdgen
+        self.builder = builder
+
+    def add_action(self, suffix, action):
+        self.cmdgen.add_action(suffix, action)
+        self.set_src_suffix(self.cmdgen.src_suffixes())
+
+def is_a_Builder(obj):
+    """"Returns True iff the specified obj is one of our Builder classes.
+
+    The test is complicated a bit by the fact that CompositeBuilder
+    is a proxy, not a subclass of BuilderBase.
+    """
+    return (isinstance(obj, BuilderBase)
+            or isinstance(obj, CompositeBuilder)
+            or callable(obj))
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: