Group --debug=count output by object modules.
[scons.git] / src / engine / SCons / Action.py
index 9ad925afa5132bc27c2d0ab8e6d54a52eb1918f3..077ddfb60d959f603bee66ca160b9bbd2103fa72 100644 (file)
@@ -1,6 +1,74 @@
-"""engine.SCons.Action
-
-XXX
+"""SCons.Action
+
+This encapsulates information about executing any sort of action that
+can build one or more target Nodes (typically files) from one or more
+source Nodes (also typically files) given a specific Environment.
+
+The base class here is ActionBase.  The base class supplies just a few
+OO utility methods and some generic methods for displaying information
+about an Action in response to the various commands that control printing.
+
+A second-level base class is _ActionAction.  This extends ActionBase
+by providing the methods that can be used to show and perform an
+action.  True Action objects will subclass _ActionAction; Action
+factory class objects will subclass ActionBase.
+
+The heavy lifting is handled by subclasses for the different types of
+actions we might execute:
+
+    CommandAction
+    CommandGeneratorAction
+    FunctionAction
+    ListAction
+
+The subclasses supply the following public interface methods used by
+other modules:
+
+    __call__()
+        THE public interface, "calling" an Action object executes the
+        command or Python function.  This also takes care of printing
+        a pre-substitution command for debugging purposes.
+
+    get_contents()
+        Fetches the "contents" of an Action for signature calculation.
+        This is what the Sig/*.py subsystem uses to decide if a target
+        needs to be rebuilt because its action changed.
+
+    genstring()
+        Returns a string representation of the Action *without*
+        command substitution, but allows a CommandGeneratorAction to
+        generate the right action based on the specified target,
+        source and env.  This is used by the Signature subsystem
+        (through the Executor) to obtain an (imprecise) representation
+        of the Action operation for informative purposes.
+
+
+Subclasses also supply the following methods for internal use within
+this module:
+
+    __str__()
+        Returns a string approximation of the Action; no variable
+        substitution is performed.
+        
+    execute()
+        The internal method that really, truly, actually handles the
+        execution of a command or Python function.  This is used so
+        that the __call__() methods can take care of displaying any
+        pre-substitution representations, and *then* execute an action
+        without worrying about the specific Actions involved.
+
+    strfunction()
+        Returns a substituted string representation of the Action.
+        This is used by the _ActionAction.show() command to display the
+        command/function that will be executed to generate the target(s).
+
+There is a related independent ActionCaller class that looks like a
+regular Action, and which serves as a wrapper for arbitrary functions
+that we want to let the user specify the arguments to now, but actually
+execute later (when an out-of-date check determines that it's needed to
+be executed, for example).  Objects of this class are returned by an
+ActionFactory class that provides a __call__() method as a convenient
+way for wrapping up the functions.
 
 """
 
@@ -33,7 +101,9 @@ import os
 import os.path
 import re
 import string
+import sys
 
+from SCons.Debug import logInstanceCreation
 import SCons.Errors
 import SCons.Util
 
@@ -42,13 +112,9 @@ class _Null:
 
 _null = _Null
 
-print_actions = 1;
-execute_actions = 1;
-
-exitvalmap = {
-    2 : 127,
-    13 : 126,
-}
+print_actions = 1
+execute_actions = 1
+print_actions_presub = 0
 
 default_ENV = None
 
@@ -58,12 +124,6 @@ def rfile(n):
     except AttributeError:
         return n
 
-def SetCommandHandler(func, escape = lambda x: x):
-    raise SCons.Errors.UserError("SetCommandHandler() is no longer supported, use the SPAWN and ESCAPE construction variables.")
-
-def GetCommandHandler():
-    raise SCons.Errors.UserError("GetCommandHandler() is no longer supported, use the SPAWN construction variable.")
-
 def _actionAppend(act1, act2):
     # This function knows how to slap two actions together.
     # Mainly, it handles ListActions by concatenating into
@@ -97,7 +157,7 @@ class CommandGenerator:
     def __radd__(self, other):
         return _actionAppend(other, self)
 
-def _do_create_action(act, strfunction=_null, varlist=[]):
+def _do_create_action(act, *args, **kw):
     """This is the actual "implementation" for the
     Action factory method, below.  This handles the
     fact that passing lists to Action() itself has
@@ -110,55 +170,58 @@ def _do_create_action(act, strfunction=_null, varlist=[]):
 
     if isinstance(act, ActionBase):
         return act
-    elif SCons.Util.is_List(act):
-        return CommandAction(act)
-    elif isinstance(act, CommandGenerator):
-        return CommandGeneratorAction(act.generator)
-    elif callable(act):
-        return FunctionAction(act, strfunction=strfunction, varlist=varlist)
-    elif SCons.Util.is_String(act):
+    if SCons.Util.is_List(act):
+        return apply(CommandAction, (act,)+args, kw)
+    if isinstance(act, CommandGenerator):
+        return apply(CommandGeneratorAction, (act.generator,)+args, kw)
+    if callable(act):
+        return apply(FunctionAction, (act,)+args, kw)
+    if SCons.Util.is_String(act):
         var=SCons.Util.get_environment_var(act)
         if var:
             # This looks like a string that is purely an Environment
             # variable reference, like "$FOO" or "${FOO}".  We do
             # something special here...we lazily evaluate the contents
-            # of that Environment variable, so a user could but something
+            # of that Environment variable, so a user could put something
             # like a function or a CommandGenerator in that variable
             # instead of a string.
-            return CommandGeneratorAction(LazyCmdGenerator(var))
-        listCmds = map(lambda x: CommandAction(x), string.split(act, '\n'))
-        if len(listCmds) == 1:
-            return listCmds[0]
+            return apply(LazyAction, (var,)+args, kw)
+        commands = string.split(str(act), '\n')
+        if len(commands) == 1:
+            return apply(CommandAction, (commands[0],)+args, kw)
         else:
-            return ListAction(listCmds)
-    else:
-        return None
+            listCmdActions = map(lambda x, args=args, kw=kw:
+                                 apply(CommandAction, (x,)+args, kw),
+                                 commands)
+            return ListAction(listCmdActions)
+    return None
 
-def Action(act, strfunction=_null, varlist=[]):
+def Action(act, *args, **kw):
     """A factory for action objects."""
     if SCons.Util.is_List(act):
-        acts = map(lambda x, s=strfunction, v=varlist:
-                          _do_create_action(x, s, v),
+        acts = map(lambda a, args=args, kw=kw:
+                          apply(_do_create_action, (a,)+args, kw),
                    act)
-        acts = filter(lambda x: not x is None, acts)
+        acts = filter(None, acts)
         if len(acts) == 1:
             return acts[0]
         else:
             return ListAction(acts)
     else:
-        return _do_create_action(act, strfunction=strfunction, varlist=varlist)
+        return apply(_do_create_action, (act,)+args, kw)
 
 class ActionBase:
-    """Base class for actions that create output objects."""
-    def __cmp__(self, other):
-        return cmp(self.__dict__, other.__dict__)
+    """Base class for all types of action objects that can be held by
+    other objects (Builders, Executors, etc.)  This provides the
+    common methods for manipulating and combining those actions."""
+    
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
 
-    def show(self, string):
-        if print_actions:
-            print string
+    def __cmp__(self, other):
+        return cmp(self.__dict__, other)
 
-    def get_actions(self):
-        return [self]
+    def genstring(self, target, source, env):
+        return str(self)
 
     def __add__(self, other):
         return _actionAppend(self, other)
@@ -166,6 +229,98 @@ class ActionBase:
     def __radd__(self, other):
         return _actionAppend(other, self)
 
+    def presub_lines(self, env):
+        # CommandGeneratorAction needs a real environment
+        # in order to return the proper string here, since
+        # it may call LazyAction, which looks up a key
+        # in that env.  So we temporarily remember the env here,
+        # and CommandGeneratorAction will use this env
+        # when it calls its _generate method.
+        self.presub_env = env
+        lines = string.split(str(self), '\n')
+        self.presub_env = None      # don't need this any more
+        return lines
+
+if not SCons.Memoize.has_metaclass:
+    _Base = ActionBase
+    class ActionBase(SCons.Memoize.Memoizer, _Base):
+        "Cache-backed version of ActionBase"
+        def __init__(self, *args, **kw):
+            apply(_Base.__init__, (self,)+args, kw)
+            SCons.Memoize.Memoizer.__init__(self)
+
+
+class _ActionAction(ActionBase):
+    """Base class for actions that create output objects."""
+    def __init__(self, strfunction=_null, presub=_null, chdir=None, **kw):
+        if not strfunction is _null:
+            self.strfunction = strfunction
+        if presub is _null:
+            presub = print_actions_presub
+        self.presub = presub
+        self.chdir = chdir
+
+    def print_cmd_line(self, s, target, source, env):
+        sys.stdout.write(s + "\n")
+
+    def __call__(self, target, source, env,
+                               errfunc=None,
+                               presub=_null,
+                               show=_null,
+                               execute=_null,
+                               chdir=_null):
+        if not SCons.Util.is_List(target):
+            target = [target]
+        if not SCons.Util.is_List(source):
+            source = [source]
+        if presub is _null:  presub = self.presub
+        if show is _null:  show = print_actions
+        if execute is _null:  execute = execute_actions
+        if chdir is _null: chdir = self.chdir
+        save_cwd = None
+        if chdir:
+            save_cwd = os.getcwd()
+            try:
+                chdir = str(chdir.abspath)
+            except AttributeError:
+                if not SCons.Util.is_String(chdir):
+                    chdir = str(target[0].dir)
+        if presub:
+            t = string.join(map(str, target), ' and ')
+            l = string.join(self.presub_lines(env), '\n  ')
+            out = "Building %s with action:\n  %s\n" % (t, l)
+            sys.stdout.write(out)
+        s = None
+        if show and self.strfunction:
+            s = self.strfunction(target, source, env)
+            if s:
+                if chdir:
+                    s = ('os.chdir(%s)\n' % repr(chdir)) + s
+                try:
+                    get = env.get
+                except AttributeError:
+                    print_func = self.print_cmd_line
+                else:
+                    print_func = get('PRINT_CMD_LINE_FUNC')
+                    if not print_func:
+                        print_func = self.print_cmd_line
+                print_func(s, target, source, env)
+        stat = 0
+        if execute:
+            if chdir:
+                os.chdir(chdir)
+            try:
+                stat = self.execute(target, source, env)
+                if stat and errfunc:
+                    errfunc(stat)
+            finally:
+                if save_cwd:
+                    os.chdir(save_cwd)
+        if s and save_cwd:
+            print_func('os.chdir(%s)' % repr(save_cwd), target, source, env)
+        return stat
+
+
 def _string_from_cmd_list(cmd_list):
     """Takes a list of command line arguments and returns a pretty
     representation for printing."""
@@ -176,23 +331,41 @@ def _string_from_cmd_list(cmd_list):
         cl.append(arg)
     return string.join(cl)
 
-_rm = re.compile(r'\$[()]')
-_remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)')
-
-class CommandAction(ActionBase):
+class CommandAction(_ActionAction):
     """Class for command-execution actions."""
-    def __init__(self, cmd):
-        # Cmd list can actually be a list or a single item...basically
-        # anything that we could pass in as the first arg to
-        # scons_subst_list().
+    def __init__(self, cmd, cmdstr=None, *args, **kw):
+        # Cmd can actually be a list or a single item; if it's a
+        # single item it should be the command string to execute; if a
+        # list then it should be the words of the command string to
+        # execute.  Only a single command should be executed by this
+        # object; lists of commands should be handled by embedding
+        # these objects in a ListAction object (which the Action()
+        # factory above does).  cmd will be passed to
+        # Environment.subst_list() for substituting environment
+        # variables.
+        if __debug__: logInstanceCreation(self, 'Action.CommandAction')
+        apply(_ActionAction.__init__, (self,)+args, kw)
+        if SCons.Util.is_List(cmd):
+            if filter(SCons.Util.is_List, cmd):
+                raise TypeError, "CommandAction should be given only " \
+                      "a single command"
         self.cmd_list = cmd
+        self.cmdstr = cmdstr
 
-    def strfunction(self, target, source, env):
-        cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm,
-                                               target, source)
-        return map(_string_from_cmd_list, cmd_list)
+    def __str__(self):
+        if SCons.Util.is_List(self.cmd_list):
+            return string.join(map(str, self.cmd_list), ' ')
+        return str(self.cmd_list)
 
-    def __call__(self, target, source, env):
+    def strfunction(self, target, source, env):
+        if not self.cmdstr is None:
+            c = env.subst(self.cmdstr, 0, target, source)
+            if c:
+                return c
+        cmd_list = env.subst_list(self.cmd_list, 0, target, source)
+        return _string_from_cmd_list(cmd_list[0])
+
+    def execute(self, target, source, env):
         """Execute a command action.
 
         This will handle lists of commands as well as individual commands,
@@ -201,84 +374,56 @@ class CommandAction(ActionBase):
         handle lists of commands, even though that's not how we use it
         externally.
         """
-        import SCons.Util
-
-        escape = env.get('ESCAPE', lambda x: x)
+        from SCons.Util import is_String, is_List, flatten, escape_list
 
-        if env.has_key('SHELL'):
+        try:
             shell = env['SHELL']
-        else:
+        except KeyError:
             raise SCons.Errors.UserError('Missing SHELL construction variable.')
 
-        # for SConf support (by now): check, if we want to pipe the command
-        # output to somewhere else
-        if env.has_key('PIPE_BUILD'):
-            pipe_build = 1
-            if env.has_key('PSPAWN'):
-                pspawn = env['PSPAWN']
-            else:
-                raise SCons.Errors.UserError('Missing PSPAWN construction variable.')
-            if env.has_key('PSTDOUT'):
-                pstdout = env['PSTDOUT']
-            else:
-                raise SCons.Errors.UserError('Missing PSTDOUT construction variable.')
-            if env.has_key('PSTDERR'):
-                pstderr = env['PSTDERR']
-            else:
-                raise SCons.Errors.UserError('Missing PSTDOUT construction variable.')
-        else:
-            pipe_build = 0
-            if env.has_key('SPAWN'):
-                spawn = env['SPAWN']
-            else:
-                raise SCons.Errors.UserError('Missing SPAWN construction variable.')
-
-        cmd_list = SCons.Util.scons_subst_list(self.cmd_list, env, _rm,
-                                               target, source)
-        for cmd_line in cmd_list:
-            if len(cmd_line):
-                if print_actions:
-                    self.show(_string_from_cmd_list(cmd_line))
-                if execute_actions:
-                    try:
-                        ENV = env['ENV']
-                    except KeyError:
-                        global default_ENV
-                        if not default_ENV:
-                            import SCons.Environment
-                            default_ENV = SCons.Environment.Environment()['ENV']
-                        ENV = default_ENV
-                    # Escape the command line for the command
-                    # interpreter we are using
-                    map(lambda x, e=escape: x.escape(e), cmd_line)
-                    cmd_line = map(str, cmd_line)
-                    if pipe_build:
-                        ret = pspawn( shell, escape, cmd_line[0], cmd_line,
-                                      ENV, pstdout, pstderr )
-                    else:
-                        ret = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
-                    if ret:
-                        return ret
-        return 0
+        try:
+            spawn = env['SPAWN']
+        except KeyError:
+            raise SCons.Errors.UserError('Missing SPAWN construction variable.')
 
-    def get_raw_contents(self, target, source, env):
-        """Return the complete contents of this action's command line.
-        """
-        # We've discusssed using the real target and source names in
-        # a CommandAction's signature contents.  This would have the
-        # advantage of recompiling when a file's name changes (keeping
-        # debug info current), but it would currently break repository
-        # logic that will change the file name based on whether the
-        # files come from a repository or locally.  If we ever move to
-        # that scheme, though, here's how we'd do it:
-        #return SCons.Util.scons_subst(string.join(self.cmd_list),
-        #                              self.subst_dict(target, source, env),
-        #                              {})
-        cmd = self.cmd_list
-        if not SCons.Util.is_List(cmd):
-            cmd = [ cmd ]
-        return SCons.Util.scons_subst(string.join(map(str, cmd)),
-                                      env)
+        escape = env.get('ESCAPE', lambda x: x)
+
+        try:
+            ENV = env['ENV']
+        except KeyError:
+            global default_ENV
+            if not default_ENV:
+                import SCons.Environment
+                default_ENV = SCons.Environment.Environment()['ENV']
+            ENV = default_ENV
+
+        # Ensure that the ENV values are all strings:
+        for key, value in ENV.items():
+            if not is_String(value):
+                if is_List(value):
+                    # If the value is a list, then we assume it is a
+                    # path list, because that's a pretty common list-like
+                    # value to stick in an environment variable:
+                    value = flatten(value)
+                    ENV[key] = string.join(map(str, value), os.pathsep)
+                else:
+                    # If it isn't a string or a list, then we just coerce
+                    # it to a string, which is the proper way to handle
+                    # Dir and File instances and will produce something
+                    # reasonable for just about everything else:
+                    ENV[key] = str(value)
+
+        cmd_list = env.subst_list(self.cmd_list, 0, target,
+                                  map(rfile, source))
+
+        # Use len() to filter out any "command" that's zero-length.
+        for cmd_line in filter(len, cmd_list):
+            # Escape the command line for the interpreter we are using.
+            cmd_line = escape_list(cmd_line, escape)
+            result = spawn(shell, escape, cmd_line[0], cmd_line, ENV)
+            if result:
+                return result
+        return 0
 
     def get_contents(self, target, source, env):
         """Return the signature contents of this action's command line.
@@ -286,47 +431,48 @@ class CommandAction(ActionBase):
         This strips $(-$) and everything in between the string,
         since those parts don't affect signatures.
         """
-        # We've discusssed using the real target and source names in
-        # a CommandAction's signature contents.  This would have the
-        # advantage of recompiling when a file's name changes (keeping
-        # debug info current), but it would currently break repository
-        # logic that will change the file name based on whether the
-        # files come from a repository or locally.  If we ever move to
-        # that scheme, though, here's how we'd do it:
-        #return SCons.Util.scons_subst(string.join(map(str, self.cmd_list)),
-        #                              self.subst_dict(target, source, env),
-        #                              {},
-        #                              _remove)
         cmd = self.cmd_list
-        if not SCons.Util.is_List(cmd):
-            cmd = [ cmd ]
-        return SCons.Util.scons_subst(string.join(map(str, cmd)),
-                                      env,
-                                      _remove)
+        if SCons.Util.is_List(cmd):
+            cmd = string.join(map(str, cmd))
+        else:
+            cmd = str(cmd)
+        return env.subst_target_source(cmd, SCons.Util.SUBST_SIG, target, source)
 
 class CommandGeneratorAction(ActionBase):
     """Class for command-generator actions."""
-    def __init__(self, generator):
+    def __init__(self, generator, *args, **kw):
+        if __debug__: logInstanceCreation(self, 'Action.CommandGeneratorAction')
         self.generator = generator
+        self.gen_kw = kw
 
-    def __generate(self, target, source, env, for_signature):
+    def _generate(self, target, source, env, for_signature):
         # ensure that target is a list, to make it easier to write
         # generator functions:
         if not SCons.Util.is_List(target):
             target = [target]
 
         ret = self.generator(target=target, source=source, env=env, for_signature=for_signature)
-        gen_cmd = Action(ret)
+        gen_cmd = apply(Action, (ret,), self.gen_kw)
         if not gen_cmd:
             raise SCons.Errors.UserError("Object returned from command generator: %s cannot be used to create an Action." % repr(ret))
         return gen_cmd
 
-    def __call__(self, target, source, env):
-        if not SCons.Util.is_List(source):
-            source = [source]
-        rsources = map(rfile, source)
-        act = self.__generate(target, source, env, 0)
-        return act(target, rsources, env)
+    def __str__(self):
+        try:
+            env = self.presub_env or {}
+        except AttributeError:
+            env = {}
+        act = self._generate([], [], env, 1)
+        return str(act)
+
+    def genstring(self, target, source, env):
+        return self._generate(target, source, env, 1).genstring(target, source, env)
+
+    def __call__(self, target, source, env, errfunc=None, presub=_null,
+                 show=_null, execute=_null, chdir=_null):
+        act = self._generate(target, source, env, 0)
+        return act(target, source, env, errfunc, presub,
+                   show, execute, chdir)
 
     def get_contents(self, target, source, env):
         """Return the signature contents of this action's command line.
@@ -334,61 +480,123 @@ class CommandGeneratorAction(ActionBase):
         This strips $(-$) and everything in between the string,
         since those parts don't affect signatures.
         """
-        return self.__generate(target, source, env, 1).get_contents(target, source, env)
-
-class LazyCmdGenerator:
-    """This is a simple callable class that acts as a command generator.
-    It holds on to a key into an Environment dictionary, then waits
-    until execution time to see what type it is, then tries to
-    create an Action out of it."""
-    def __init__(self, var):
+        return self._generate(target, source, env, 1).get_contents(target, source, env)
+
+
+
+# A LazyAction is a kind of hybrid generator and command action for
+# strings of the form "$VAR".  These strings normally expand to other
+# strings (think "$CCCOM" to "$CC -c -o $TARGET $SOURCE"), but we also
+# want to be able to replace them with functions in the construction
+# environment.  Consequently, we want lazy evaluation and creation of
+# an Action in the case of the function, but that's overkill in the more
+# normal case of expansion to other strings.
+#
+# So we do this with a subclass that's both a generator *and*
+# a command action.  The overridden methods all do a quick check
+# of the construction variable, and if it's a string we just call
+# the corresponding CommandAction method to do the heavy lifting.
+# If not, then we call the same-named CommandGeneratorAction method.
+# The CommandGeneratorAction methods work by using the overridden
+# _generate() method, that is, our own way of handling "generation" of
+# an action based on what's in the construction variable.
+
+class LazyAction(CommandGeneratorAction, CommandAction):
+
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
+    def __init__(self, var, *args, **kw):
+        if __debug__: logInstanceCreation(self, 'Action.LazyAction')
+        apply(CommandAction.__init__, (self, '$'+var)+args, kw)
         self.var = SCons.Util.to_String(var)
+        self.gen_kw = kw
+
+    def get_parent_class(self, env):
+        c = env.get(self.var)
+        if SCons.Util.is_String(c) and not '\n' in c:
+            return CommandAction
+        return CommandGeneratorAction
+
+    def _generate_cache(self, env):
+        """__cacheable__"""
+        c = env.get(self.var, '')
+        gen_cmd = apply(Action, (c,), self.gen_kw)
+        if not gen_cmd:
+            raise SCons.Errors.UserError("$%s value %s cannot be used to create an Action." % (self.var, repr(c)))
+        return gen_cmd
+
+    def _generate(self, target, source, env, for_signature):
+        return self._generate_cache(env)
+
+    def __call__(self, target, source, env, *args, **kw):
+        args = (self, target, source, env) + args
+        c = self.get_parent_class(env)
+        return apply(c.__call__, args, kw)
+
+    def get_contents(self, target, source, env):
+        c = self.get_parent_class(env)
+        return c.get_contents(self, target, source, env)
+
+if not SCons.Memoize.has_metaclass:
+    _Base = LazyAction
+    class LazyAction(SCons.Memoize.Memoizer, _Base):
+        def __init__(self, *args, **kw):
+            SCons.Memoize.Memoizer.__init__(self)
+            apply(_Base.__init__, (self,)+args, kw)
 
-    def __call__(self, target, source, env, for_signature):
-        if env.has_key(self.var):
-            return env[self.var]
-        else:
-            # The variable reference substitutes to nothing.
-            return ''
 
-class FunctionAction(ActionBase):
+
+class FunctionAction(_ActionAction):
     """Class for Python function actions."""
 
-    def __init__(self, execfunction, strfunction=_null, varlist=[]):
+    def __init__(self, execfunction, *args, **kw):
+        if __debug__: logInstanceCreation(self, 'Action.FunctionAction')
         self.execfunction = execfunction
-        if strfunction is _null:
-            def strfunction(target, source, env, execfunction=execfunction):
-                def quote(s):
-                    return '"' + str(s) + '"'
-                def array(a, q=quote):
-                    return '[' + string.join(map(lambda x, q=q: q(x), a), ", ") + ']'
-                try:
-                    name = execfunction.__name__
-                except AttributeError:
-                    try:
-                        name = execfunction.__class__.__name__
-                    except AttributeError:
-                        name = "unknown_python_function"
-                tstr = len(target) == 1 and quote(target[0]) or array(target)
-                sstr = len(source) == 1 and quote(source[0]) or array(source)
-                return "%s(%s, %s)" % (name, tstr, sstr)
-        self.strfunction = strfunction
-        self.varlist = varlist
+        apply(_ActionAction.__init__, (self,)+args, kw)
+        self.varlist = kw.get('varlist', [])
 
-    def __call__(self, target, source, env):
-        r = 0
-        if not SCons.Util.is_List(target):
-            target = [target]
-        if not SCons.Util.is_List(source):
-            source = [source]
-        if print_actions and self.strfunction:
-            s = self.strfunction(target, source, env)
-            if s:
-                self.show(s)
-        if execute_actions:
-            rsources = map(rfile, source)
-            r = self.execfunction(target=target, source=rsources, env=env)
-        return r
+    def function_name(self):
+        try:
+            return self.execfunction.__name__
+        except AttributeError:
+            try:
+                return self.execfunction.__class__.__name__
+            except AttributeError:
+                return "unknown_python_function"
+
+    def strfunction(self, target, source, env):
+        def array(a):
+            def quote(s):
+                return '"' + str(s) + '"'
+            return '[' + string.join(map(quote, a), ", ") + ']'
+        try:
+            strfunc = self.execfunction.strfunction
+        except AttributeError:
+            pass
+        else:
+            if strfunc is None:
+                return None
+            if callable(strfunc):
+                return strfunc(target, source, env)
+        name = self.function_name()
+        tstr = array(target)
+        sstr = array(source)
+        return "%s(%s, %s)" % (name, tstr, sstr)
+
+    def __str__(self):
+        name = self.function_name()
+        if name == 'ActionCaller':
+            return str(self.execfunction)
+        return "%s(target, source, env)" % name
+
+    def execute(self, target, source, env):
+        rsources = map(rfile, source)
+        try:
+            result = self.execfunction(target=target, source=rsources, env=env)
+        except EnvironmentError, e:
+            # If an IOError/OSError happens, raise a BuildError.
+            raise SCons.Errors.BuildError(node=target, errstr=e.strerror)
+        return result
 
     def get_contents(self, target, source, env):
         """Return the signature contents of this callable action.
@@ -398,27 +606,46 @@ class FunctionAction(ActionBase):
         """
         try:
             # "self.execfunction" is a function.
-            code = self.execfunction.func_code.co_code
-        except:
+            contents = str(self.execfunction.func_code.co_code)
+        except AttributeError:
             # "self.execfunction" is a callable object.
-            code = self.execfunction.__call__.im_func.func_code.co_code
-        return str(code) + string.join(map(lambda v, e=env: str(e[v]),
-                                       self.varlist))
+            try:
+                contents = str(self.execfunction.__call__.im_func.func_code.co_code)
+            except AttributeError:
+                try:
+                    # See if execfunction will do the heavy lifting for us.
+                    gc = self.execfunction.get_contents
+                except AttributeError:
+                    # This is weird, just do the best we can.
+                    contents = str(self.execfunction)
+                else:
+                    contents = gc(target, source, env)
+        return contents + env.subst(string.join(map(lambda v: '${'+v+'}',
+                                                     self.varlist)))
 
 class ListAction(ActionBase):
     """Class for lists of other actions."""
     def __init__(self, list):
-        self.list = map(lambda x: Action(x), list)
-
-    def get_actions(self):
-        return self.list
+        if __debug__: logInstanceCreation(self, 'Action.ListAction')
+        def list_of_actions(x):
+            if isinstance(x, ActionBase):
+                return x
+            return Action(x)
+        self.list = map(list_of_actions, list)
+
+    def genstring(self, target, source, env):
+        return string.join(map(lambda a, t=target, s=source, e=env:
+                                  a.genstring(t, s, e),
+                               self.list),
+                           '\n')
 
-    def __call__(self, target, source, env):
-        for l in self.list:
-            r = l(target, source, env)
-            if r:
-                return r
-        return 0
+    def __str__(self):
+        return string.join(map(str, self.list), '\n')
+    
+    def presub_lines(self, env):
+        return SCons.Util.flatten(map(lambda a, env=env:
+                                      a.presub_lines(env),
+                                      self.list))
 
     def get_contents(self, target, source, env):
         """Return the signature contents of this action list.
@@ -429,3 +656,84 @@ class ListAction(ActionBase):
                                       x.get_contents(t, s, e),
                                self.list),
                            "")
+
+    def __call__(self, target, source, env, errfunc=None, presub=_null,
+                 show=_null, execute=_null, chdir=_null):
+        for act in self.list:
+            stat = act(target, source, env, errfunc, presub,
+                       show, execute, chdir)
+            if stat:
+                return stat
+        return 0
+
+class ActionCaller:
+    """A class for delaying calling an Action function with specific
+    (positional and keyword) arguments until the Action is actually
+    executed.
+
+    This class looks to the rest of the world like a normal Action object,
+    but what it's really doing is hanging on to the arguments until we
+    have a target, source and env to use for the expansion.
+    """
+    def __init__(self, parent, args, kw):
+        self.parent = parent
+        self.args = args
+        self.kw = kw
+    def get_contents(self, target, source, env):
+        actfunc = self.parent.actfunc
+        try:
+            # "self.actfunc" is a function.
+            contents = str(actfunc.func_code.co_code)
+        except AttributeError:
+            # "self.actfunc" is a callable object.
+            try:
+                contents = str(actfunc.__call__.im_func.func_code.co_code)
+            except AttributeError:
+                # No __call__() method, so it might be a builtin
+                # or something like that.  Do the best we can.
+                contents = str(actfunc)
+        return contents
+    def subst(self, s, target, source, env):
+        # Special-case hack:  Let a custom function wrapped in an
+        # ActionCaller get at the environment through which the action
+        # was called by using this hard-coded value as a special return.
+        if s == '$__env__':
+            return env
+        else:
+            return env.subst(s, 0, target, source)
+    def subst_args(self, target, source, env):
+        return map(lambda x, self=self, t=target, s=source, e=env:
+                          self.subst(x, t, s, e),
+                   self.args)
+    def subst_kw(self, target, source, env):
+        kw = {}
+        for key in self.kw.keys():
+            kw[key] = self.subst(self.kw[key], target, source, env)
+        return kw
+    def __call__(self, target, source, env):
+        args = self.subst_args(target, source, env)
+        kw = self.subst_kw(target, source, env)
+        return apply(self.parent.actfunc, args, kw)
+    def strfunction(self, target, source, env):
+        args = self.subst_args(target, source, env)
+        kw = self.subst_kw(target, source, env)
+        return apply(self.parent.strfunc, args, kw)
+    def __str__(self):
+        return apply(self.parent.strfunc, self.args, self.kw)
+
+class ActionFactory:
+    """A factory class that will wrap up an arbitrary function
+    as an SCons-executable Action object.
+
+    The real heavy lifting here is done by the ActionCaller class.
+    We just collect the (positional and keyword) arguments that we're
+    called with and give them to the ActionCaller object we create,
+    so it can hang onto them until it needs them.
+    """
+    def __init__(self, actfunc, strfunc):
+        self.actfunc = actfunc
+        self.strfunc = strfunc
+    def __call__(self, *args, **kw):
+        ac = ActionCaller(self, args, kw)
+        action = Action(ac, strfunction=ac.strfunction)
+        return action