Add emacs and vim editing settings to the bottom of *.py files.
[scons.git] / src / engine / SCons / Builder.py
index d6b7597b88c7659960a281178937e3163f84d786..0f5bc76d97c5b2988551dd2205b84e2560d3c50d 100644 (file)
@@ -16,20 +16,9 @@ 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.
+does, in fact, represent the 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:
+There is also a proxy that looks like a Builder:
 
     CompositeBuilder
 
@@ -39,11 +28,6 @@ There is also two proxies that look like Builders:
         (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:
 
@@ -55,9 +39,6 @@ used by other modules:
         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
@@ -126,6 +107,8 @@ 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
 import SCons.Util
 import SCons.Warnings
@@ -135,6 +118,15 @@ class _Null:
 
 _null = _Null
 
+def match_splitext(path, suffixes = []):
+    if suffixes:
+        matchsuf = filter(lambda S,path=path: path[-len(S):] == S,
+                          suffixes)
+        if matchsuf:
+            suf = max(map(None, 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
@@ -142,6 +134,10 @@ class DictCmdGenerator(SCons.Util.Selector):
     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()
 
@@ -151,22 +147,31 @@ class DictCmdGenerator(SCons.Util.Selector):
         self[suffix] = action
 
     def __call__(self, target, source, env, for_signature):
-        ext = None
-        for src in map(str, source):
-            my_ext = SCons.Util.splitext(src)[1]
-            if ext and my_ext != ext:
-                raise UserError("While building `%s' from `%s': Cannot build multiple sources with different extensions: %s, %s" % (repr(map(str, target)), src, ext, my_ext))
-            ext = my_ext
+        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(map(str, target)), src, ext, my_ext))
+                ext = my_ext
+        else:
+            ext = match_splitext(str(source[0]), self.src_suffixes())[1]
 
         if not ext:
+            #return ext
             raise UserError("While building `%s': Cannot deduce file extension from source files: %s" % (repr(map(str, target)), repr(map(str, source))))
 
         try:
-            ret = SCons.Util.Selector.__call__(self, env, source)
+            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': Don't know how to build a file with suffix `%s'." % (repr(map(str, target)), ext))
+            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(map(str, target)), repr(map(str, source)), ext, repr(self.keys())))
         return ret
 
 class CallableSelector(SCons.Util.Selector):
@@ -214,25 +219,22 @@ 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.
+    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):
         UserDict.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():
-            try:
+            if misleading_keywords.has_key(k):
                 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))
+                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):
@@ -241,12 +243,18 @@ def Builder(**kw):
     if kw.has_key('generator'):
         if kw.has_key('action'):
             raise UserError, "You must not specify both an action and a generator."
-        kw['action'] = SCons.Action.CommandGenerator(kw['generator'])
+        kw['action'] = SCons.Action.CommandGeneratorAction(kw['generator'], {})
         del kw['generator']
-    elif kw.has_key('action') and SCons.Util.is_Dict(kw['action']):
-        composite = DictCmdGenerator(kw['action'])
-        kw['action'] = SCons.Action.CommandGenerator(composite)
-        kw['src_suffix'] = composite.src_suffixes()
+    elif kw.has_key('action'):
+        source_ext_match = kw.get('source_ext_match', 1)
+        if kw.has_key('source_ext_match'):
+            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 kw.has_key('emitter'):
         emitter = kw['emitter']
@@ -264,105 +272,52 @@ def Builder(**kw):
         elif SCons.Util.is_List(emitter):
             kw['emitter'] = ListEmitter(emitter)
 
-    if kw.has_key('src_builder'):
-        ret = apply(MultiStepBuilder, (), kw)
-    else:
-        ret = apply(BuilderBase, (), kw)
+    result = apply(BuilderBase, (), kw)
 
     if not composite is None:
-        ret = CompositeBuilder(ret, composite)
+        result = CompositeBuilder(result, composite)
 
-    return ret
+    return result
 
-def _init_nodes(builder, env, overrides, executor_kw, tlist, slist):
-    """Initialize lists of target and source nodes with all of
-    the proper Builder information.
+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" % str(t)
+            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:
-                t_contents = t.builder.action.get_contents(tlist, slist, t.env)
-                contents = t.builder.action.get_contents(tlist, slist, 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:
-                    SCons.Warnings.warn(SCons.Warnings.DuplicateEnvironmentWarning,
-                                        "Two different environments were specified for target %s,\n\tbut they appear to have the same action: %s"%(str(t), t.builder.action.genstring(tlist, slist, t.env)))
-
+                    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:
-                    raise UserError, "Two environments with different actions were specified for the same target: %s"%str(t)
-
-            elif t.overrides != overrides:
-                raise UserError, "Two different sets of overrides were specified for the same target: %s"%str(t)
-
-            elif builder.target_scanner and t.target_scanner and builder.target_scanner != t.target_scanner:
-                raise UserError, "Two different scanners were specified for the same target: %s"%str(t)
-
+                    msg = "Two environments with different actions were specified for the same target: %s" % t
+                    raise UserError, msg
             if builder.multi:
                 if t.builder != builder:
-                    if isinstance(t.builder, ListBuilder) and isinstance(builder, ListBuilder) and t.builder.builder == builder.builder:
-                        raise UserError, "Two different target sets have a target in common: %s"%str(t)
-                    else:
-                        raise UserError, "Two different builders (%s and %s) were specified for the same target: %s"%(t.builder.get_name(env), builder.get_name(env), str(t))
-                elif isinstance(t.builder, ListBuilder) ^ isinstance(builder, ListBuilder):
-                    raise UserError, "Cannot build same target `%s' as singular and list"%str(t)
+                    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, map(str, t.get_executor().get_all_targets()), map(str, tlist))
+                    raise UserError, msg
             elif t.sources != slist:
-                raise UserError, "Multiple ways to build the same target were specified for: %s  (from %s and from %s)" % (str(t), map(str,t.sources), map(str,slist))
+                msg = "Multiple ways to build the same target were specified for: %s  (from %s and from %s)" % (t, map(str, t.sources), 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" % (map(str,tlist), map(str,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
-    if builder.multi:
-        try:
-            executor = tlist[0].get_executor(create = 0)
-        except AttributeError:
-            pass
-        else:
-            executor.add_sources(slist)
-    if executor is None:
-        if not builder.action:
-            raise UserError, "Builder %s must have an action to build %s."%(builder.get_name(env or builder.env), map(str,tlist))
-        executor = SCons.Executor.Executor(builder.action,
-                                           env or builder.env,
-                                           [builder.overrides, overrides],
-                                           tlist,
-                                           slist,
-                                           executor_kw)
-
-    # Now set up the relevant information in the target Nodes themselves.
-    for t in tlist:
-        t.overrides = overrides
-        t.cwd = SCons.Node.FS.default_fs.getcwd()
-        t.builder_set(builder)
-        t.env_set(env)
-        t.add_source(slist)
-        t.set_executor(executor)
-        if builder.target_scanner:
-            t.target_scanner = builder.target_scanner
-        if t.source_scanner is None:
-            t.source_scanner = builder.source_scanner
-
-    # Add backup source scanners from the environment to the source
-    # nodes.  This may not be necessary if the node will have a real
-    # source scanner added later (which is why these are the "backup"
-    # source scanners, not the real ones), but because source nodes may
-    # be used multiple times for different targets, it ends up being
-    # more efficient to do this calculation once here, as opposed to
-    # delaying it until later when we potentially have to calculate it
-    # over and over and over.
-    for s in slist:
-        if s.source_scanner is None and s.backup_source_scanner is None:
-            s.backup_source_scanner = env.get_scanner(s.scanner_key())
-
 class EmitterProxy:
     """This is a callable class that can act as a
     Builder emitter.  It holds on to a string that
@@ -398,12 +353,17 @@ class BuilderBase:
     nodes (files) from input nodes (files).
     """
 
+    if SCons.Memoize.use_memoizer:
+        __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
+    memoizer_counters = []
+
     def __init__(self,  action = None,
                         prefix = '',
                         suffix = '',
                         src_suffix = '',
-                        target_factory = SCons.Node.FS.default_fs.File,
-                        source_factory = SCons.Node.FS.default_fs.File,
+                        target_factory = None,
+                        source_factory = None,
                         target_scanner = None,
                         source_scanner = None,
                         emitter = None,
@@ -413,16 +373,18 @@ class BuilderBase:
                         name = None,
                         chdir = _null,
                         is_explicit = 1,
+                        src_builder = None,
+                        ensure_suffix = False,
                         **overrides):
-        if __debug__: logInstanceCreation(self, 'BuilderBase')
-        self.action = SCons.Action.Action(action)
+        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.suffix = suffix
         self.env = env
         self.single_source = single_source
         if overrides.has_key('overrides'):
@@ -438,7 +400,9 @@ class BuilderBase:
             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
@@ -456,6 +420,12 @@ class BuilderBase:
             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"
 
@@ -470,7 +440,7 @@ class BuilderBase:
         try:
             index = env['BUILDERS'].values().index(self)
             return env['BUILDERS'].keys()[index]
-        except (AttributeError, KeyError, ValueError):
+        except (AttributeError, KeyError, TypeError, ValueError):
             try:
                 return self.name
             except AttributeError:
@@ -483,37 +453,34 @@ class BuilderBase:
         if not env:
             env = self.env
         if env:
-            matchsuf = filter(lambda S,path=path: path[-len(S):] == S,
-                              self.src_suffixes(env))
-            if matchsuf:
-                suf = max(map(None, map(len, matchsuf), matchsuf))[1]
-                return [path[:-len(suf)], path[-len(suf):]]
-        return SCons.Util.splitext(path)
-
-    def _create_nodes(self, env, overwarn, target = None, source = None):
+            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.
         """
-        def _adjustixes(files, pre, suf):
-            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)
-                result.append(f)
-            return result
-
-        overwarn.warn()
-
-        env = env.Override(overwarn.data)
-
         src_suf = self.get_src_suffix(env)
 
-        source = _adjustixes(source, None, src_suf)
-        slist = env.arg2nodes(source, self.source_factory)
+        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)
@@ -523,11 +490,14 @@ class BuilderBase:
                 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])
-            splitext = lambda S,self=self,env=env: self.splitext(S,env)
-            tlist = [ t_from_s(pre, suf, splitext) ]
+            except IndexError:
+                tlist = []
+            else:
+                splitext = lambda S,self=self,env=env: self.splitext(S,env)
+                tlist = [ t_from_s(pre, suf, splitext) ]
         else:
-            target = _adjustixes(target, pre, suf)
-            tlist = env.arg2nodes(target, self.target_factory)
+            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
@@ -538,62 +508,127 @@ class BuilderBase:
             new_targets = []
             for t in tlist:
                 if not t.is_derived():
-                    t.builder = self
+                    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 _init_nodes() doesn't do weird stuff
+            # 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 = None
+                    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.
-            slist = env.arg2nodes(source, self.source_factory)
-            tlist = env.arg2nodes(target, self.target_factory)
+            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=None, source=_null, overwarn={}, executor_kw={}):
-        if source is _null:
-            source = target
-            target = None
+    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
-           SCons.Util.is_List(source) and
-           len(source) > 1 and
-           target is None):
+        if self.single_source and len(source) > 1 and target is None:
             result = []
             if target is None: target = [None]*len(source)
-            for k in range(len(source)):
-                t = self._execute(env, target[k], source[k], overwarn)
-                if SCons.Util.is_List(t):
-                    result.extend(t)
-                else:
-                    result.append(t)
-            return result
-        
-        tlist, slist = self._create_nodes(env, overwarn, target, 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)
 
-        if len(tlist) == 1:
-            builder = self
-        else:
-            builder = ListBuilder(self, env, tlist)
-        _init_nodes(builder, env, overwarn.data, executor_kw, tlist, slist)
+        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.
 
-        return tlist
+        executor = None
+        key = None
 
-    def __call__(self, env, target=None, source=_null, chdir=_null, **kw):
+        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),
+                                        map(str,tlist))
+            key = self.action.batch_key(env or self.env, tlist, slist)
+            if key:
+                try:
+                    executor = SCons.Executor.GetBatchExecutor(key)
+                except KeyError:
+                    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 kw.has_key('srcdir'):
+                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 = 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):
@@ -607,24 +642,25 @@ class BuilderBase:
             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)
-        else:
-            suffix = self.adjust_suffix(suffix)
         return env.subst(suffix)
 
-    def src_suffixes(self, env):
-        return map(lambda x, s=self, e=env: e.subst(s.adjust_suffix(x)),
-                   self.src_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 = src_suffix
+        adjust = lambda suf, s=self: \
+                        callable(suf) and suf or s.adjust_suffix(suf)
+        self.src_suffix = map(adjust, src_suffix)
 
     def get_src_suffix(self, env):
         """Get the first src_suffix in the list of src_suffixes."""
@@ -633,13 +669,6 @@ class BuilderBase:
             return ''
         return ret[0]
 
-    def targets(self, node):
-        """Return the list of targets for this builder instance.
-
-        For most normal builders, this is just the supplied node.
-        """
-        return [ node ]
-
     def add_emitter(self, suffix, emitter):
         """Add a suffix-emitter mapping to this Builder.
 
@@ -650,137 +679,169 @@ class BuilderBase:
         """
         self.emitter[suffix] = emitter
 
-class ListBuilder(SCons.Util.Proxy):
-    """A Proxy to support building an array of targets (for example,
-    foo.o and foo.h from foo.y) from a single Action execution.
-    """
+    def add_src_builder(self, builder):
+        """
+        Add a new Builder to the list of src_builders.
 
-    def __init__(self, builder, env, tlist):
-        if __debug__: logInstanceCreation(self)
-        SCons.Util.Proxy.__init__(self, builder)
-        self.builder = builder
-        self.target_scanner = builder.target_scanner
-        self.source_scanner = builder.source_scanner
-        self.env = env
-        self.tlist = tlist
-        self.multi = builder.multi
-        self.single_source = builder.single_source
+        This requires wiping out cached values so that the computed
+        lists of source suffixes get re-calculated.
+        """
+        self._memo = {}
+        self.src_builder.append(builder)
 
-    def targets(self, node):
-        """Return the list of targets for this builder instance.
+    def _get_sdict(self, env):
         """
-        return self.tlist
+        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 get_name(self, env):
-        """Attempts to get the name of the Builder."""
+    def src_builder_sources(self, env, source, overwarn={}):
+        sdict = self._get_sdict(env)
 
-        return "ListBuilder(%s)" % self.builder.get_name(env)
+        src_suffixes = self.src_suffixes(env)
 
-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.
+        lengths = list(set(map(len, src_suffixes)))
+
+        def match_src_suffix(name, src_suffixes=src_suffixes, lengths=lengths):
+            node_suffixes = map(lambda l, n=name: n[-l:], 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:
+                        mss = lambda t, m=match_src_suffix: m(t.name)
+                        tlist = filter(mss, tlist)
+                    result.extend(tlist)
+            else:
+                result.append(s)
 
-    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,
-                        action = None,
-                        prefix = '',
-                        suffix = '',
-                        src_suffix = '',
-                        target_factory = SCons.Node.FS.default_fs.File,
-                        source_factory = SCons.Node.FS.default_fs.File,
-                        target_scanner = None,
-                        source_scanner = None,
-                        emitter=None,
-                        single_source=0):
-        if __debug__: logInstanceCreation(self)
-        BuilderBase.__init__(self, action, prefix, suffix, src_suffix,
-                             target_factory, source_factory,
-                             target_scanner, source_scanner, emitter,
-                             single_source = single_source)
-        if not SCons.Util.is_List(src_builder):
-            src_builder = [ src_builder ]
-        self.src_builder = src_builder
-        self.sdict = {}
-        self.cached_src_suffixes = {} # source suffixes keyed on id(env)
+        source_factory = env.get_factory(self.source_factory)
 
-    def _execute(self, env, target = None, source = _null, overwarn={}, executor_kw={}):
-        if source is _null:
-            source = target
-            target = None
+        return env.arg2nodes(result, source_factory)
 
-        slist = env.arg2nodes(source, self.source_factory)
-        final_sources = []
+    def _get_src_builders_key(self, env):
+        return id(env)
 
-        try:
-            sdict = self.sdict[id(env)]
-        except KeyError:
-            sdict = {}
-            self.sdict[id(env)] = sdict
-            for bld in self.src_builder:
-                if SCons.Util.is_String(bld):
-                    try:
-                        bld = env['BUILDERS'][bld]
-                    except KeyError:
-                        continue
-                for suf in bld.src_suffixes(env):
-                    sdict[suf] = bld
-
-        src_suffixes = self.src_suffixes(env)
-
-        for snode in slist:
-            for srcsuf in src_suffixes:
-                if str(snode)[-len(srcsuf):] == srcsuf and sdict.has_key(srcsuf):
-                    tgt = sdict[srcsuf]._execute(env, None, snode, 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(tgt) > 1:
-                        tgt = filter(lambda x, self=self, suf=src_suffixes, e=env:
-                                     self.splitext(SCons.Util.to_String(x),e)[1] in suf,
-                                     tgt)
-                    final_sources.extend(tgt)
-                    snode = None
-                    break
-            if snode:
-                final_sources.append(snode)
-                
-        return BuilderBase._execute(self, env, target, final_sources, overwarn)
+    memoizer_counters.append(SCons.Memoize.CountDict('get_src_builders', _get_src_builders_key))
 
     def get_src_builders(self, env):
-        """Return all the src_builders for this Builder.
+        """
+        Returns the list of source Builders for this Builder.
 
-        This is essentially a recursive descent of the src_builder "tree."
+        This exists mainly to look up Builders referenced as
+        strings in the 'BUILDER' variable of the construction
+        environment and cache the result.
         """
-        ret = []
+        memo_key = id(env)
+        try:
+            memo_dict = self._memo['get_src_builders']
+        except KeyError:
+            memo_dict = {}
+            self._memo['get_src_builders'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
+
+        builders = []
         for bld in self.src_builder:
             if SCons.Util.is_String(bld):
-                # All Environments should have a BUILDERS
-                # variable, so no need to check for it.
                 try:
                     bld = env['BUILDERS'][bld]
                 except KeyError:
                     continue
-            ret.append(bld)
-        return ret
+            builders.append(bld)
 
-    def src_suffixes(self, env):
-        """Return a list of the src_suffix attributes for all
-        src_builders of this Builder.
+        memo_dict[memo_key] = builders
+        return builders
+
+    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.
+        """
+        memo_key = id(env)
         try:
-            return self.cached_src_suffixes[id(env)]
+            memo_dict = self._memo['subst_src_suffixes']
         except KeyError:
-            suffixes = BuilderBase.src_suffixes(self, env)
-            for builder in self.get_src_builders(env):
-                suffixes.extend(builder.src_suffixes(env))
-            self.cached_src_suffixes[id(env)] = suffixes
-            return suffixes
+            memo_dict = {}
+            self._memo['subst_src_suffixes'] = memo_dict
+        else:
+            try:
+                return memo_dict[memo_key]
+            except KeyError:
+                pass
+        suffixes = map(lambda x, s=self, e=env: e.subst(x), 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 not sdict.has_key(s):
+                    sdict[s] = 1
+                    suffixes.append(s)
+        return suffixes
 
 class CompositeBuilder(SCons.Util.Proxy):
     """A Builder Proxy whose main purpose is to always have
@@ -789,7 +850,7 @@ class CompositeBuilder(SCons.Util.Proxy):
     """
 
     def __init__(self, builder, cmdgen):
-        if __debug__: logInstanceCreation(self)
+        if __debug__: logInstanceCreation(self, 'Builder.CompositeBuilder')
         SCons.Util.Proxy.__init__(self, builder)
 
         # cmdgen should always be an instance of DictCmdGenerator.
@@ -799,3 +860,9 @@ class CompositeBuilder(SCons.Util.Proxy):
     def add_action(self, suffix, action):
         self.cmdgen.add_action(suffix, action)
         self.set_src_suffix(self.cmdgen.src_suffixes())
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: