Add a QT tool. (Christoph Wiedemann)
[scons.git] / src / engine / SCons / Builder.py
index efef969c347e4c3122e9173cd0dc5608c5ffea7d..2bcd9935bfd47aaa62392d97c82ae4aedb85d721 100644 (file)
@@ -1,11 +1,25 @@
 """SCons.Builder
 
-XXX
+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.
+
+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.
 
 """
 
 #
-# Copyright (c) 2001, 2002 Steven Knight
+# __COPYRIGHT__
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -32,146 +46,363 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
 
 import os.path
-import string
-from Errors import UserError
+from SCons.Errors import InternalError, UserError
 
 import SCons.Action
+import SCons.Executor
+import SCons.Node
 import SCons.Node.FS
 import SCons.Util
+import SCons.Warnings
+
+class _Null:
+    pass
 
+_null = _Null
 
+class DictCmdGenerator:
+    """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, action_dict):
+        self.action_dict = action_dict
+
+    def src_suffixes(self):
+        return self.action_dict.keys()
+
+    def add_action(self, suffix, action):
+        """Add a suffix-action pair to the mapping.
+        """
+        self.action_dict[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 ext:
+            raise UserError("While building `%s': Cannot deduce file extension from source files: %s" % (repr(map(str, target)), repr(map(str, source))))
+        try:
+            return self.action_dict[ext]
+        except KeyError:
+            # Before raising the user error, try to perform Environment
+            # substitution on the keys of action_dict.
+            s_dict = {}
+            for (k,v) in self.action_dict.items():
+                s_k = env.subst(k)
+                if s_dict.has_key(s_k):
+                    # XXX Note that we do only raise errors, when variables
+                    # point to the same suffix. If one suffix is a
+                    # literal and a variable suffix contains this literal
+                    # we don't raise an error (cause the literal 'wins')
+                    raise UserError("Ambiguous suffixes after environment substitution: %s == %s == %s" % (s_dict[s_k][0], k, s_k))
+                s_dict[s_k] = (k,v)
+            try:
+                return s_dict[ext][1]
+            except KeyError:
+                raise UserError("While building `%s': Don't know how to build a file with suffix %s." % (repr(map(str, target)), repr(ext)))
+
+    def __cmp__(self, other):
+        return cmp(self.action_dict, other.action_dict)
 
 def Builder(**kw):
     """A factory for builder objects."""
-    
+    composite = None
     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'])
         del kw['generator']
-
-    if kw.has_key('action') and SCons.Util.is_Dict(kw['action']):
-        return apply(CompositeBuilder, (), kw)
-    elif kw.has_key('src_builder'):
-        return apply(MultiStepBuilder, (), kw)
+    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()
+
+    if kw.has_key('emitter') and \
+       SCons.Util.is_String(kw['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(kw['emitter'])
+        if not var:
+            raise UserError, "Supplied emitter '%s' does not appear to refer to an Environment variable" % kw['emitter']
+        kw['emitter'] = EmitterProxy(var)
+
+    if kw.has_key('src_builder'):
+        ret = apply(MultiStepBuilder, (), kw)
     else:
-        return apply(BuilderBase, (), kw)
+        ret = apply(BuilderBase, (), kw)
 
+    if composite:
+        ret = CompositeBuilder(ret, composite)
 
+    return ret
 
-def _init_nodes(builder, env, tlist, slist):
+def _init_nodes(builder, env, overrides, tlist, slist):
     """Initialize lists of target and source nodes with all of
     the proper Builder information.
     """
+
+    # 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)
+        if t.has_builder():
+            if t.env != env:
+                raise UserError, "Two different environments 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.scanner and builder.scanner != t.target_scanner:
+                raise UserError, "Two different scanners were specified for the same target: %s"%str(t)
+
+            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 t.sources != slist:
+                raise UserError, "Multiple ways to build the same target were specified for: %s" % str(t)
+
+    # 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:
+        executor = SCons.Executor.Executor(builder,
+                                           tlist[0].generate_build_env(env),
+                                           overrides,
+                                           tlist,
+                                           slist)
+
+    # Now set up the relevant information in the target Nodes themselves.
     for t in tlist:
-        t.cwd = SCons.Node.FS.default_fs.getcwd()      # XXX
+        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.scanner:
-            t.scanner_set(builder.scanner.instance(env))
+            t.target_scanner = builder.scanner
 
+    # Last, add scanners from the Environment to the source Nodes.
     for s in slist:
-        s.env_set(env, 1)
-        scanner = env.get_scanner(os.path.splitext(s.name)[1])
+        src_key = s.scanner_key()        # the file suffix
+        scanner = env.get_scanner(src_key)
         if scanner:
-            s.scanner_set(scanner.instance(env))
-
+            s.source_scanner = scanner
+
+
+def _adjust_suffix(suff):
+    if suff and not suff[0] in [ '.', '$' ]:
+        return '.' + suff
+    return suff
+
+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 \
+              env.has_key(emitter):
+            emitter = env[emitter]
+        if not callable(emitter):
+            return (target, source)
+
+        return emitter(target, source, env)
 
+    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 = '',
+    def __init__(self,  action = None,
+                        prefix = '',
+                        suffix = '',
+                        src_suffix = '',
                         node_factory = SCons.Node.FS.default_fs.File,
-                        scanner = None):
-        if name is None:
-            raise UserError, "You must specify a name for the builder."
-       self.name = name
-       self.action = SCons.Action.Action(action)
-
-       self.prefix = prefix
-       self.suffix = suffix
-       self.src_suffix = src_suffix
-       self.node_factory = node_factory
+                        target_factory = None,
+                        source_factory = None,
+                        scanner = None,
+                        emitter = None,
+                        multi = 0,
+                        env = None,
+                        overrides = {}):
+        self.action = SCons.Action.Action(action)
+        self.multi = multi
+        self.prefix = prefix
+        self.suffix = suffix
+        self.env = env
+        self.overrides = overrides
+
+        self.set_src_suffix(src_suffix)
+
+        self.target_factory = target_factory or node_factory
+        self.source_factory = source_factory or 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
+
+        self.emitter = emitter
+
+    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."""
+
+        try:
+            index = env['BUILDERS'].values().index(self)
+            return env['BUILDERS'].keys()[index]
+        except (AttributeError, KeyError, ValueError):
+            return str(self.__class__)
 
     def __cmp__(self, other):
-       return cmp(self.__dict__, other.__dict__)
+        return cmp(self.__dict__, other.__dict__)
 
-    def _create_nodes(self, env, target = None, source = None):
+    def _create_nodes(self, env, overrides, target = None, source = None):
         """Create and return lists of target and source nodes.
         """
-       def adjustixes(files, pre, suf):
-           ret = []
-            if SCons.Util.is_String(files):
-                files = string.split(files)
+        def adjustixes(files, pre, suf):
+            if not files:
+                return []
+            ret = []
             if not SCons.Util.is_List(files):
-               files = [files]
-           for f in files:
+                files = [files]
+
+            for f in files:
                 if SCons.Util.is_String(f):
-                   if pre and f[:len(pre)] != pre:
+                    if 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.Util.scons_str2nodes(adjustixes(target,
-                                                      env.subst(self.prefix),
-                                                      env.subst(self.suffix)),
-                                           self.node_factory)
-
-        slist = SCons.Util.scons_str2nodes(adjustixes(source,
-                                                      None,
-                                                      env.subst(self.src_suffix)),
-                                           self.node_factory)
+                        if fn[:len(pre)] != pre:
+                            f = os.path.join(path, pre + fn)
+                    # Only append a suffix if the file does not have one.
+                    if suf and not SCons.Util.splitext(f)[1]:
+                        if f[-len(suf):] != suf:
+                            f = f + suf
+                ret.append(f)
+            return ret
+
+        env = env.Override(overrides)
+
+        pre = self.get_prefix(env)
+        suf = self.get_suffix(env)
+        src_suf = self.get_src_suffix(env)
+
+        source = adjustixes(source, None, src_suf)
+        if target is None:
+            s = source[0]
+            if isinstance(s, SCons.Node.Node):
+                s = str(s)
+            dir, s = os.path.split(s)
+            target = pre + os.path.splitext(s)[0] + suf
+            if dir:
+                target = [ os.path.join(dir, target) ]
+        else:
+            target = adjustixes(target, pre, suf)
+
+        slist = SCons.Node.arg2nodes(source, self.source_factory)
+        tlist = SCons.Node.arg2nodes(target, self.target_factory)
+
+        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 = self
+                    new_targets.append(t)
+        
+            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
+            # 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
+
+            # Have to call arg2nodes yet again, since it is legal for
+            # emitters to spit out strings as well as Node instances.
+            slist = SCons.Node.arg2nodes(source, self.source_factory)
+            tlist = SCons.Node.arg2nodes(target, self.target_factory)
+
         return tlist, slist
 
-    def __call__(self, env, target = None, source = None):
-        tlist, slist = self._create_nodes(env, target, source)
+    def __call__(self, env, target = None, source = _null, **overrides):
+        if source is _null:
+            source = target
+            target = None
+        tlist, slist = self._create_nodes(env, overrides, target, source)
 
         if len(tlist) == 1:
-            _init_nodes(self, env, tlist, slist)
+            _init_nodes(self, env, overrides, tlist, slist)
             tlist = tlist[0]
         else:
-            _init_nodes(ListBuilder(self, env, tlist), env, tlist, slist)
+            _init_nodes(ListBuilder(self, env, tlist), env, overrides, tlist, slist)
 
         return tlist
 
+    def src_suffixes(self, env):
+        return map(lambda x, e=env: e.subst(_adjust_suffix(x)),
+                   self.src_suffix)
 
-    def execute(self, **kw):
-       """Execute a builder's action to create an output object.
-       """
-       return apply(self.action.execute, (), kw)
+    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
 
-    def get_raw_contents(self, **kw):
-        """Fetch the "contents" of the builder's action.
-        """
-        return apply(self.action.get_raw_contents, (), kw)
+    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 get_contents(self, **kw):
-        """Fetch the "contents" of the builder's action
-        (for signature calculation).
-        """
-        return apply(self.action.get_contents, (), kw)
+    def get_suffix(self, env):
+        return env.subst(_adjust_suffix(self.suffix))
 
-    def src_suffixes(self, env):
-        if self.src_suffix != '':
-            return [env.subst(self.src_suffix)]
-        return []
+    def get_prefix(self, env):
+        return env.subst(self.prefix)
 
     def targets(self, node):
         """Return the list of targets for this builder instance.
@@ -180,47 +411,32 @@ class BuilderBase:
         """
         return [ node ]
 
-class ListBuilder:
-    """This is technically not a Builder object, but a wrapper
-    around another Builder object.  This is designed to look
-    like a Builder object, though, for purposes of building an
-    array of targets from a single Action execution.
+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 __init__(self, builder, env, tlist):
+        SCons.Util.Proxy.__init__(self, builder)
         self.builder = builder
         self.scanner = builder.scanner
         self.env = env
         self.tlist = tlist
-
-    def execute(self, **kw):
-        if hasattr(self, 'status'):
-            return self.status
-        for t in self.tlist:
-            # unlink all targets and make all directories
-            # before building anything
-            t.prepare()
-        kw['target'] = self.tlist[0]
-        self.status = apply(self.builder.execute, (), kw)
-        for t in self.tlist:
-            if not t is kw['target']:
-                t.build()
-        return self.status
-
-    def get_raw_contents(self, **kw):
-        return apply(self.builder.get_raw_contents, (), kw)
-
-    def get_contents(self, **kw):
-        return apply(self.builder.get_contents, (), kw)
-
-    def src_suffixes(self, env):
-        return self.builder.src_suffixes(env)
+        self.multi = builder.multi
 
     def targets(self, node):
         """Return the list of targets for this builder instance.
         """
         return self.tlist
 
+    def __cmp__(self, other):
+        return cmp(self.__dict__, other.__dict__)
+
+    def get_name(self, env):
+        """Attempts to get the name of the Builder."""
+
+        return "ListBuilder(%s)" % self.builder.get_name(env)
+
 class MultiStepBuilder(BuilderBase):
     """This is a builder subclass that can build targets in
     multiple steps.  The src_builder parameter to the constructor
@@ -233,117 +449,115 @@ class MultiStepBuilder(BuilderBase):
     src_suffix.
     """
     def __init__(self,  src_builder,
-                        name = None,
-                       action = None,
-                       prefix = '',
-                       suffix = '',
-                       src_suffix = '',
+                        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)
+                        target_factory = None,
+                        source_factory = None,
+                        scanner=None,
+                        emitter=None):
+        BuilderBase.__init__(self, action, prefix, suffix, src_suffix,
+                             node_factory, target_factory, source_factory,
+                             scanner, emitter)
+        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)
+
+    def __call__(self, env, target = None, source = _null, **kw):
+        if source is _null:
+            source = target
+            target = None
 
-    def __call__(self, env, target = None, source = None):
-        slist = SCons.Util.scons_str2nodes(source, self.node_factory)
+        slist = SCons.Node.arg2nodes(source, self.source_factory)
         final_sources = []
-        src_suffix = env.subst(self.src_suffix)
-        sdict = {}
-        for suff in self.src_builder.src_suffixes(env):
-            sdict[suff] = None
+
+        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:
-            path, ext = os.path.splitext(snode.abspath)
+            path, ext = SCons.Util.splitext(snode.get_abspath())
             if sdict.has_key(ext):
-                tgt = self.src_builder(env, target = [ path ], source = snode)
+                src_bld = sdict[ext]
+                tgt = apply(src_bld, (env, path, snode), kw)
+                # Only supply the builder with sources it is capable
+                # of building.
+                if SCons.Util.is_List(tgt):
+                    tgt = filter(lambda x, suf=src_suffixes:
+                                 SCons.Util.splitext(SCons.Util.to_String(x))[1] in suf,
+                                 tgt)
                 if not SCons.Util.is_List(tgt):
                     final_sources.append(tgt)
                 else:
                     final_sources.extend(tgt)
             else:
                 final_sources.append(snode)
-        return BuilderBase.__call__(self, env, target=target,
-                                    source=final_sources)
 
-    def src_suffixes(self, env):
-        return BuilderBase.src_suffixes(self, env) + \
-               self.src_builder.src_suffixes(env)
-
-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."""
-    def __init__(self,  name = None,
-                        prefix='',
-                        suffix='',
-                        action = {},
-                        src_builder = []):
-        BuilderBase.__init__(self, name=name, prefix=prefix,
-                             suffix=suffix)
-        if src_builder and not SCons.Util.is_List(src_builder):
-            src_builder = [src_builder]
-        self.src_builder = src_builder
-        self.action_dict = action
-        self.sdict = {}
-        self.sbuild = {}
-
-    def __call__(self, env, target = None, source = None):
-        tlist, slist = BuilderBase._create_nodes(self, env,
-                                                 target=target, source=source)
-
-        r = repr(env)
-        if not self.sdict.has_key(r):
-            self.sdict[r] = {}
-            self.sbuild[r] = []
-            for suff in self.src_suffixes(env):
-                suff = env.subst(suff)
-                self.sdict[r][suff] = suff
-                self.sbuild[r].extend(filter(lambda x, e=env, s=suff:
-                                                    e.subst(x.suffix) == s,
-                                             self.src_builder))
-            for sb in self.sbuild[r]:
-                suff = env.subst(sb.suffix)
-                for s in sb.src_suffixes(env):
-                     self.sdict[r][env.subst(s)] = suff
-
-        sufflist = map(lambda x, s=self.sdict[r]:
-                              s[os.path.splitext(x.path)[1]],
-                       slist)
-        last_suffix = ''
-        for suff in sufflist:
-            if last_suffix and last_suffix != suff:
-                raise UserError, "The builder for %s can only build source files of identical suffixes:  %s." % \
-                      (tlist[0].path,
-                       str(map(lambda t: str(t.path), tlist[0].sources)))
-            last_suffix = suff
-
-        if last_suffix:
-            kw = {
-                'name' : self.name,
-                'action' : self.action_dict[last_suffix],
-                'src_suffix' : last_suffix,
-            }
-            if self.sbuild[r]:
-                sb = filter(lambda x, e=env, s=last_suffix:
-                                   e.subst(x.suffix) == s,
-                            self.sbuild[r])
-                if sb:
-                    kw['src_builder'] = sb[0]
-            # XXX We should be able to cache this
-            bld = apply(Builder, (), kw)
-            for tnode in tlist:
-                bld.__call__(env, target = tnode, source = slist)
+        return apply(BuilderBase.__call__,
+                     (self, env, target, final_sources), kw)
 
-        if len(tlist) == 1:
-            tlist = tlist[0]
-        return tlist
+    def get_src_builders(self, env):
+        """Return all the src_builders for this Builder.
+
+        This is essentially a recursive descent of the src_builder "tree."
+        """
+        ret = []
+        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
 
     def src_suffixes(self, env):
-        suffixes = map(lambda k, e=env: e.subst(k), self.action_dict.keys()) + \
-                   reduce(lambda x, y: x + y,
-                          map(lambda b, e=env: b.src_suffixes(e),
-                              self.src_builder),
-                          [])
-        return suffixes
+        """Return a list of the src_suffix attributes for all
+        src_builders of this Builder.
+        """
+        try:
+            return self.cached_src_suffixes[id(env)]
+        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
+
+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.
+    """
+
+    def __init__(self, builder, cmdgen):
+        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 __cmp__(self, other):
+        return cmp(self.__dict__, other.__dict__)