Fix use of varlist in CommandGeneratorAction, ListAction and LazyAction
[scons.git] / src / engine / SCons / Action.py
index e2fe07f0fa4a755e4ed5b4275d4244c832172536..41c73add1f237e2400efe4a1ab8aabd5304e26ad 100644 (file)
@@ -1,11 +1,82 @@
-"""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
+        plus the varlist.  This is what gets MD5 checksummed 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.
+
+    get_presig()
+        Fetches the "contents" of a subclass for signature calculation.
+        The varlist is added to this to produce the Action's contents.
+
+    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.
 
 """
 
-#
-# 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
@@ -25,159 +96,249 @@ XXX
 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
-import copy
+import cPickle
+import dis
 import os
-import os.path
 import re
 import string
 import sys
-import UserDict
+import subprocess
 
-import SCons.Util
+from SCons.Debug import logInstanceCreation
 import SCons.Errors
+import SCons.Executor
+import SCons.Util
+import SCons.Subst
 
-print_actions = 1;
-execute_actions = 1;
+# we use these a lot, so try to optimize them
+is_String = SCons.Util.is_String
+is_List = SCons.Util.is_List
 
-exitvalmap = {
-    2 : 127,
-    13 : 126,
-}
+class _null:
+    pass
 
-default_ENV = None
+print_actions = 1
+execute_actions = 1
+print_actions_presub = 0
 
-def quote(x):
-    if ' ' in x or '\t' in x:
-        return '"'+x+'"'
-    else:
-        return x
+def rfile(n):
+    try:
+        return n.rfile()
+    except AttributeError:
+        return n
 
-if os.name == 'posix':
+def default_exitstatfunc(s):
+    return s
 
-    def defaultSpawn(cmd, args, env):
-        pid = os.fork()
-        if not pid:
-            # Child process.
-            exitval = 127
-            args = ['sh', '-c', string.join(map(quote, args))]
-            try:
-                os.execvpe('sh', args, env)
-            except OSError, e:
-                exitval = exitvalmap[e[0]]
-                sys.stderr.write("scons: %s: %s\n" % (cmd, e[1]))
-            os._exit(exitval)
-        else:
-            # Parent process.
-            pid, stat = os.waitpid(pid, 0)
-            ret = stat >> 8
-            return ret
-
-elif os.name == 'nt':
-
-    def pathsearch(cmd, env):
-        # In order to deal with the fact that 1.5.2 doesn't have
-        # os.spawnvpe(), roll our own PATH search.
-        if os.path.isabs(cmd):
-            if not os.path.exists(cmd):
-                exts = env['PATHEXT']
-                if not SCons.Util.is_List(exts):
-                    exts = string.split(exts, os.pathsep)
-                for e in exts:
-                    f = cmd + e
-                    if os.path.exists(f):
-                        return f
+try:
+    SET_LINENO = dis.SET_LINENO
+    HAVE_ARGUMENT = dis.HAVE_ARGUMENT
+except AttributeError:
+    remove_set_lineno_codes = lambda x: x
+else:
+    def remove_set_lineno_codes(code):
+        result = []
+        n = len(code)
+        i = 0
+        while i < n:
+            c = code[i]
+            op = ord(c)
+            if op >= HAVE_ARGUMENT:
+                if op != SET_LINENO:
+                    result.append(code[i:i+3])
+                i = i+3
             else:
-                return cmd
-        else:
-            path = env['PATH']
-            if not SCons.Util.is_List(path):
-                path = string.split(path, os.pathsep)
-            exts = env['PATHEXT']
-            if not SCons.Util.is_List(exts):
-                exts = string.split(exts, os.pathsep)
-            pairs = []
-            for dir in path:
-                for e in exts:
-                    pairs.append((dir, e))
-            for dir, ext in pairs:
-                f = os.path.join(dir, cmd)
-                if not ext is None:
-                    f = f + ext
-                if os.path.exists(f):
-                    return f
-        return None
+                result.append(c)
+                i = i+1
+        return string.join(result, '')
+
+strip_quotes = re.compile('^[\'"](.*)[\'"]$')
 
-    # Attempt to find cmd.exe (for WinNT/2k/XP) or
-    # command.com for Win9x
 
-    cmd_interp = ''
-    # First see if we can look in the registry...
-    if SCons.Util.can_read_reg:
+def _callable_contents(obj):
+    """Return the signature contents of a callable Python object.
+    """
+    try:
+        # Test if obj is a method.
+        return _function_contents(obj.im_func)
+
+    except AttributeError:
         try:
-            # Look for Windows NT system root
-            k=SCons.Util.RegOpenKeyEx(SCons.Util.hkey_mod.HKEY_LOCAL_MACHINE,
-                                          'Software\\Microsoft\\Windows NT\\CurrentVersion')
-            val, tok = SCons.Util.RegQueryValueEx(k, 'SystemRoot')
-            cmd_interp = os.path.join(val, 'System32\\cmd.exe')
-        except SCons.Util.RegError:
-            try:
-                # Okay, try the Windows 9x system root
-                k=SCons.Util.RegOpenKeyEx(SCons.Util.hkey_mod.HKEY_LOCAL_MACHINE,
-                                              'Software\\Microsoft\\Windows\\CurrentVersion')
-                val, tok = SCons.Util.RegQueryValueEx(k, 'SystemRoot')
-                cmd_interp = os.path.join(val, 'command.com')
-            except:
-                pass
-    if not cmd_interp:
-        cmd_interp = pathsearch('cmd', os.environ)
-        if not cmd_interp:
-            cmd_interp = pathsearch('command', os.environ)
-
-    # The upshot of all this is that, if you are using Python 1.5.2,
-    # you had better have cmd or command.com in your PATH when you run
-    # scons.
-
-    def defaultSpawn(cmd, args, env):
-        if not cmd_interp:
-            sys.stderr.write("scons: Could not find command interpreter, is it in your PATH?\n")
-            return 127
-        else:
+            # Test if obj is a callable object.
+            return _function_contents(obj.__call__.im_func)
+
+        except AttributeError:
             try:
-                args = [cmd_interp, '/C', quote(string.join(map(quote, args)))]
-                ret = os.spawnve(os.P_WAIT, cmd_interp, args, env)
-            except OSError, e:
-                ret = exitvalmap[e[0]]
-                sys.stderr.write("scons: %s: %s\n" % (cmd, e[1]))
-            return ret
-else:
-    def defaultSpawn(cmd, args, env):
-        sys.stderr.write("scons: Unknown os '%s', cannot spawn command interpreter.\n" % os.name)
-        sys.stderr.write("scons: Set your command handler with SetCommandHandler().\n")
-        return 127
+                # Test if obj is a code object.
+                return _code_contents(obj)
 
-spawn = defaultSpawn
+            except AttributeError:
+                    # Test if obj is a function object.
+                    return _function_contents(obj)
 
-def SetCommandHandler(func):
-    global spawn
-    spawn = func
 
-def GetCommandHandler():
-    global spawn
-    return spawn
+def _object_contents(obj):
+    """Return the signature contents of any Python object.
 
-class CommandGenerator:
+    We have to handle the case where object contains a code object
+    since it can be pickled directly.
     """
-    Wraps a command generator function so the Action() factory
-    function can tell a generator function from a function action.
+    try:
+        # Test if obj is a method.
+        return _function_contents(obj.im_func)
+
+    except AttributeError:
+        try:
+            # Test if obj is a callable object.
+            return _function_contents(obj.__call__.im_func)
+
+        except AttributeError:
+            try:
+                # Test if obj is a code object.
+                return _code_contents(obj)
+
+            except AttributeError:
+                try:
+                    # Test if obj is a function object.
+                    return _function_contents(obj)
+
+                except AttributeError:
+                    # Should be a pickable Python object.
+                    try:
+                        return cPickle.dumps(obj)
+                    except (cPickle.PicklingError, TypeError):
+                        # This is weird, but it seems that nested classes
+                        # are unpickable. The Python docs say it should
+                        # always be a PicklingError, but some Python
+                        # versions seem to return TypeError.  Just do
+                        # the best we can.
+                        return str(obj)
+
+
+def _code_contents(code):
+    """Return the signature contents of a code object.
+
+    By providing direct access to the code object of the
+    function, Python makes this extremely easy.  Hooray!
+
+    Unfortunately, older versions of Python include line
+    number indications in the compiled byte code.  Boo!
+    So we remove the line number byte codes to prevent
+    recompilations from moving a Python function.
     """
-    def __init__(self, generator):
-        self.generator = generator
 
-def _do_create_action(act):
+    contents = []
+
+    # The code contents depends on the number of local variables
+    # but not their actual names.
+    contents.append("%s,%s" % (code.co_argcount, len(code.co_varnames)))
+    try:
+        contents.append(",%s,%s" % (len(code.co_cellvars), len(code.co_freevars)))
+    except AttributeError:
+        # Older versions of Python do not support closures.
+        contents.append(",0,0")
+
+    # The code contents depends on any constants accessed by the
+    # function. Note that we have to call _object_contents on each
+    # constants because the code object of nested functions can
+    # show-up among the constants.
+    #
+    # Note that we also always ignore the first entry of co_consts
+    # which contains the function doc string. We assume that the
+    # function does not access its doc string.
+    contents.append(',(' + string.join(map(_object_contents,code.co_consts[1:]),',') + ')')
+
+    # The code contents depends on the variable names used to
+    # accessed global variable, as changing the variable name changes
+    # the variable actually accessed and therefore changes the
+    # function result.
+    contents.append(',(' + string.join(map(_object_contents,code.co_names),',') + ')')
+
+
+    # The code contents depends on its actual code!!!
+    contents.append(',(' + str(remove_set_lineno_codes(code.co_code)) + ')')
+
+    return string.join(contents, '')
+
+
+def _function_contents(func):
+    """Return the signature contents of a function."""
+
+    contents = [_code_contents(func.func_code)]
+
+    # The function contents depends on the value of defaults arguments
+    if func.func_defaults:
+        contents.append(',(' + string.join(map(_object_contents,func.func_defaults),',') + ')')
+    else:
+        contents.append(',()')
+
+    # The function contents depends on the closure captured cell values.
+    try:
+        closure = func.func_closure or []
+    except AttributeError:
+        # Older versions of Python do not support closures.
+        closure = []
+
+    #xxx = [_object_contents(x.cell_contents) for x in closure]
+    try:
+        xxx = map(lambda x: _object_contents(x.cell_contents), closure)
+    except AttributeError:
+        xxx = []
+    contents.append(',(' + string.join(xxx, ',') + ')')
+
+    return string.join(contents, '')
+
+
+def _actionAppend(act1, act2):
+    # This function knows how to slap two actions together.
+    # Mainly, it handles ListActions by concatenating into
+    # a single ListAction.
+    a1 = Action(act1)
+    a2 = Action(act2)
+    if a1 is None or a2 is None:
+        raise TypeError, "Cannot append %s to %s" % (type(act1), type(act2))
+    if isinstance(a1, ListAction):
+        if isinstance(a2, ListAction):
+            return ListAction(a1.list + a2.list)
+        else:
+            return ListAction(a1.list + [ a2 ])
+    else:
+        if isinstance(a2, ListAction):
+            return ListAction([ a1 ] + a2.list)
+        else:
+            return ListAction([ a1, a2 ])
+
+def _do_create_keywords(args, kw):
+    """This converts any arguments after the action argument into
+    their equivalent keywords and adds them to the kw argument.
+    """
+    v = kw.get('varlist', ())
+    # prevent varlist="FOO" from being interpreted as ['F', 'O', 'O']
+    if is_String(v): v = (v,)
+    kw['varlist'] = tuple(v)
+    if args:
+        # turn positional args into equivalent keywords
+        cmdstrfunc = args[0]
+        if cmdstrfunc is None or is_String(cmdstrfunc):
+            kw['cmdstr'] = cmdstrfunc
+        elif callable(cmdstrfunc):
+            kw['strfunction'] = cmdstrfunc
+        else:
+            raise SCons.Errors.UserError(
+                'Invalid command display variable type. '
+                'You must either pass a string or a callback which '
+                'accepts (target, source, env) as parameters.')
+        if len(args) > 1:
+            kw['varlist'] = args[1:] + kw['varlist']
+    if kw.get('strfunction', _null) is not _null \
+                      and kw.get('cmdstr', _null) is not _null:
+        raise SCons.Errors.UserError(
+            'Cannot have both strfunction and cmdstr args to Action()')
+
+def _do_create_action(act, kw):
     """This is the actual "implementation" for the
     Action factory method, below.  This handles the
     fact that passing lists to Action() itself has
@@ -190,323 +351,906 @@ def _do_create_action(act):
 
     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)
-    elif SCons.Util.is_String(act):
+
+    if is_List(act):
+        #TODO(1.5) return CommandAction(act, **kw)
+        return apply(CommandAction, (act,), kw)
+
+    if callable(act):
+        try:
+            gen = kw['generator']
+            del kw['generator']
+        except KeyError:
+            gen = 0
+        if gen:
+            action_type = CommandGeneratorAction
+        else:
+            action_type = FunctionAction
+        return action_type(act, kw)
+
+    if 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(string.split(x)),
-                       string.split(act, '\n'))
-        if len(listCmds) == 1:
-            return listCmds[0]
-        else:
-            return ListAction(listCmds)
+            return LazyAction(var, kw)
+        commands = string.split(str(act), '\n')
+        if len(commands) == 1:
+            #TODO(1.5) return CommandAction(commands[0], **kw)
+            return apply(CommandAction, (commands[0],), kw)
+        # The list of string commands may include a LazyAction, so we
+        # reprocess them via _do_create_list_action.
+        return _do_create_list_action(commands, kw)
+    return None
+
+def _do_create_list_action(act, kw):
+    """A factory for list actions.  Convert the input list into Actions
+    and then wrap them in a ListAction."""
+    acts = []
+    for a in act:
+        aa = _do_create_action(a, kw)
+        if aa is not None: acts.append(aa)
+    if not acts:
+        return ListAction([])
+    elif len(acts) == 1:
+        return acts[0]
     else:
-        return None
+        return ListAction(acts)
 
-def Action(act):
+def Action(act, *args, **kw):
     """A factory for action objects."""
-    if SCons.Util.is_List(act):
-        acts = filter(lambda x: not x is None,
-                      map(_do_create_action, act))
-        if len(acts) == 1:
-            return acts[0]
-        else:
-            return ListAction(acts)
-    else:
-        return _do_create_action(act)
+    # Really simple: the _do_create_* routines do the heavy lifting.
+    _do_create_keywords(args, kw)
+    if is_List(act):
+        return _do_create_list_action(act, kw)
+    return _do_create_action(act, kw)
 
 class ActionBase:
-    """Base class for actions that create output objects."""
-    def __cmp__(self, other):
-        return cmp(self.__dict__, other.__dict__)
-
-    def show(self, string):
-        print string
-
-    def subst_dict(self, **kw):
-        """Create a dictionary for substitution of construction
-        variables.
-
-        This translates the following special arguments:
-
-            env    - the construction environment itself,
-                     the values of which (CC, CCFLAGS, etc.)
-                     are copied straight into the dictionary
+    """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."""
 
-            target - the target (object or array of objects),
-                     used to generate the TARGET and TARGETS
-                     construction variables
-
-            source - the source (object or array of objects),
-                     used to generate the SOURCES and SOURCE 
-                     construction variables
-
-        Any other keyword arguments are copied into the
-        dictionary."""
+    def __cmp__(self, other):
+        return cmp(self.__dict__, other)
 
-        dict = {}
-        if kw.has_key('env'):
-            dict.update(kw['env'])
-            del kw['env']
+    def no_batch_key(self, env, target, source):
+        return None
 
-        try:
-            cwd = kw['dir']
-        except KeyError:
-            cwd = None
-        else:
-            del kw['dir']
+    batch_key = no_batch_key
+
+    def genstring(self, target, source, env):
+        return str(self)
+
+    def get_contents(self, target, source, env):
+        result = [ self.get_presig(target, source, env) ]
+        # This should never happen, as the Action() factory should wrap
+        # the varlist, but just in case an action is created directly,
+        # we duplicate this check here.
+        vl = self.get_varlist(target, source, env)
+        if is_String(vl): vl = (vl,)
+        for v in vl:
+            result.append(env.subst('${'+v+'}'))
+        return string.join(result, '')
+
+    def __add__(self, other):
+        return _actionAppend(self, other)
+
+    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
+
+    def get_varlist(self, target, source, env, executor=None):
+        return self.varlist
+
+    def get_targets(self, env, executor):
+        """
+        Returns the type of targets ($TARGETS, $CHANGED_TARGETS) used
+        by this action.
+        """
+        return self.targets
 
-        if kw.has_key('target'):
-            t = kw['target']
-            del kw['target']
-            if not SCons.Util.is_List(t):
-                t = [t]
+class _ActionAction(ActionBase):
+    """Base class for actions that create output objects."""
+    def __init__(self, cmdstr=_null, strfunction=_null, varlist=(),
+                       presub=_null, chdir=None, exitstatfunc=None,
+                       batch_key=None, targets='$TARGETS',
+                 **kw):
+        self.cmdstr = cmdstr
+        if strfunction is not _null:
+            if strfunction is None:
+                self.cmdstr = None
+            else:
+                self.strfunction = strfunction
+        self.varlist = varlist
+        self.presub = presub
+        self.chdir = chdir
+        if not exitstatfunc:
+            exitstatfunc = default_exitstatfunc
+        self.exitstatfunc = exitstatfunc
+
+        self.targets = targets
+
+        if batch_key:
+            if not callable(batch_key):
+                # They have set batch_key, but not to their own
+                # callable.  The default behavior here will batch
+                # *all* targets+sources using this action, separated
+                # for each construction environment.
+                def default_batch_key(self, env, target, source):
+                    return (id(self), id(env))
+                batch_key = default_batch_key
+            SCons.Util.AddMethod(self, batch_key, 'batch_key')
+
+    def print_cmd_line(self, s, target, source, env):
+        sys.stdout.write(s + "\n")
+
+    def __call__(self, target, source, env,
+                               exitstatfunc=_null,
+                               presub=_null,
+                               show=_null,
+                               execute=_null,
+                               chdir=_null,
+                               executor=None):
+        if not is_List(target):
+            target = [target]
+        if not is_List(source):
+            source = [source]
+
+        if presub is _null:
+            presub = self.presub
+            if presub is _null:
+                presub = print_actions_presub
+        if exitstatfunc is _null: exitstatfunc = self.exitstatfunc
+        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 is_String(chdir):
+                    if executor:
+                        chdir = str(executor.batches[0].targets[0].dir)
+                    else:
+                        chdir = str(target[0].dir)
+        if presub:
+            if executor:
+                target = executor.get_all_targets()
+                source = executor.get_all_sources()
+            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)
+        cmd = None
+        if show and self.strfunction:
+            if executor:
+                target = executor.get_all_targets()
+                source = executor.get_all_sources()
             try:
-                cwd = t[0].cwd
-            except (IndexError, AttributeError):
-                pass
-            dict['TARGETS'] = SCons.Util.PathList(map(os.path.normpath, map(str, t)))
-            if dict['TARGETS']:
-                dict['TARGET'] = dict['TARGETS'][0]
-
-        if kw.has_key('source'):
-            def rstr(x):
+                cmd = self.strfunction(target, source, env, executor)
+            except TypeError:
+                cmd = self.strfunction(target, source, env)
+            if cmd:
+                if chdir:
+                    cmd = ('os.chdir(%s)\n' % repr(chdir)) + cmd
                 try:
-                    return x.rstr()
+                    get = env.get
                 except AttributeError:
-                    return str(x)
-            s = kw['source']
-            del kw['source']
-            if not SCons.Util.is_List(s):
-                s = [s]
-            dict['SOURCES'] = SCons.Util.PathList(map(os.path.normpath, map(rstr, s)))
-            if dict['SOURCES']:
-                dict['SOURCE'] = dict['SOURCES'][0]
-
-        dict.update(kw)
+                    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(cmd, target, source, env)
+        stat = 0
+        if execute:
+            if chdir:
+                os.chdir(chdir)
+            try:
+                stat = self.execute(target, source, env, executor=executor)
+                if isinstance(stat, SCons.Errors.BuildError):
+                    s = exitstatfunc(stat.status)
+                    if s:
+                        stat.status = s
+                    else:
+                        stat = s
+                else:
+                    stat = exitstatfunc(stat)
+            finally:
+                if save_cwd:
+                    os.chdir(save_cwd)
+        if cmd and save_cwd:
+            print_func('os.chdir(%s)' % repr(save_cwd), target, source, env)
+
+        return stat
 
-        return dict
 
 def _string_from_cmd_list(cmd_list):
     """Takes a list of command line arguments and returns a pretty
     representation for printing."""
     cl = []
-    for arg in cmd_list:
+    for arg in map(str, cmd_list):
         if ' ' in arg or '\t' in arg:
             arg = '"' + arg + '"'
         cl.append(arg)
     return string.join(cl)
 
-_rm = re.compile(r'\$[()]')
-_remove = re.compile(r'\$\(([^\$]|\$[^\(])*?\$\)')
-
-class EnvDictProxy(UserDict.UserDict):
-    """This is a dictionary-like class that contains the
-    Environment dictionary we pass to FunctionActions
-    and CommandGeneratorActions.
-
-    In addition to providing
-    normal dictionary-like access to the variables in the
-    Environment, it also exposes the functions subst()
-    and subst_list(), allowing users to easily do variable
-    interpolation when writing their FunctionActions
-    and CommandGeneratorActions."""
-
-    def __init__(self, env):
-        UserDict.UserDict.__init__(self, env)
-
-    def subst(self, string, raw=0):
-        if raw:
-            regex_remove = None
-        else:
-            regex_remove = _rm
-        return SCons.Util.scons_subst(string, self.data, {}, regex_remove)
-
-    def subst_list(self, string, raw=0):
-        if raw:
-            regex_remove = None
+# A fiddlin' little function that has an 'import SCons.Environment' which
+# can't be moved to the top level without creating an import loop.  Since
+# this import creates a local variable named 'SCons', it blocks access to
+# the global variable, so we move it here to prevent complaints about local
+# variables being used uninitialized.
+default_ENV = None
+def get_default_ENV(env):
+    global default_ENV
+    try:
+        return env['ENV']
+    except KeyError:
+        if not default_ENV:
+            import SCons.Environment
+            # This is a hideously expensive way to get a default shell
+            # environment.  What it really should do is run the platform
+            # setup to get the default ENV.  Fortunately, it's incredibly
+            # rare for an Environment not to have a shell environment, so
+            # we're not going to worry about it overmuch.
+            default_ENV = SCons.Environment.Environment()['ENV']
+        return default_ENV
+
+# This function is still in draft mode.  We're going to need something like
+# it in the long run as more and more places use subprocess, but I'm sure
+# it'll have to be tweaked to get the full desired functionality.
+# one special arg (so far?), 'error', to tell what to do with exceptions.
+def _subproc(env, cmd, error = 'ignore', **kw):
+    """Do common setup for a subprocess.Popen() call"""
+    # allow std{in,out,err} to be "'devnull'"
+    io = kw.get('stdin')
+    if is_String(io) and io == 'devnull':
+        kw['stdin'] = open(os.devnull)
+    io = kw.get('stdout')
+    if is_String(io) and io == 'devnull':
+        kw['stdout'] = open(os.devnull, 'w')
+    io = kw.get('stderr')
+    if is_String(io) and io == 'devnull':
+        kw['stderr'] = open(os.devnull, 'w')
+
+    # Figure out what shell environment to use
+    ENV = kw.get('env', None)
+    if ENV is None: ENV = get_default_ENV(env)
+
+    # Ensure that the ENV values are all strings:
+    new_env = {}
+    for key, value in ENV.items():
+        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 = SCons.Util.flatten_sequence(value)
+            new_env[key] = string.join(map(str, value), os.pathsep)
         else:
-            regex_remove = _rm
-        return SCons.Util.scons_subst_list(string, self.data, {}, regex_remove)
-
-class CommandAction(ActionBase):
+            # It's either a string or something else.  If it's a string,
+            # we still want to call str() because it might be a *Unicode*
+            # string, which makes subprocess.Popen() gag.  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:
+            new_env[key] = str(value)
+    kw['env'] = new_env
+
+    try:
+        #FUTURE return subprocess.Popen(cmd, **kw)
+        return apply(subprocess.Popen, (cmd,), kw)
+    except EnvironmentError, e:
+        if error == 'raise': raise
+        # return a dummy Popen instance that only returns error
+        class dummyPopen:
+            def __init__(self, e): self.exception = e
+            def communicate(self): return ('','')
+            def wait(self): return -self.exception.errno
+            stdin = None
+            class f:
+                def read(self): return ''
+                def readline(self): return ''
+            stdout = stderr = f()
+        return dummyPopen(e)
+
+class CommandAction(_ActionAction):
     """Class for command-execution actions."""
-    def __init__(self, cmd):
-        import SCons.Util
-        
-        self.cmd_list = map(SCons.Util.to_String, cmd)
-
-    def execute(self, **kw):
-        dict = apply(self.subst_dict, (), kw)
-        import SCons.Util
-        cmd_list = SCons.Util.scons_subst_list(self.cmd_list, dict, {}, _rm)
-        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 = kw['env']['ENV']
-                    except KeyError:
-                        global default_ENV
-                        if not default_ENV:
-                            import SCons.Environment
-                            default_ENV = SCons.Environment.Environment()['ENV']
-                        ENV = default_ENV
-                    ret = spawn(cmd_line[0], cmd_line, ENV)
-                    if ret:
-                        return ret
-        return 0
+    def __init__(self, cmd, **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')
+
+        #TODO(1.5) _ActionAction.__init__(self, **kw)
+        apply(_ActionAction.__init__, (self,), kw)
+        if is_List(cmd):
+            if filter(is_List, cmd):
+                raise TypeError, "CommandAction should be given only " \
+                      "a single command"
+        self.cmd_list = cmd
+
+    def __str__(self):
+        if is_List(self.cmd_list):
+            return string.join(map(str, self.cmd_list), ' ')
+        return str(self.cmd_list)
+
+    def process(self, target, source, env, executor=None):
+        if executor:
+            result = env.subst_list(self.cmd_list, 0, executor=executor)
+        else:
+            result = env.subst_list(self.cmd_list, 0, target, source)
+        silent = None
+        ignore = None
+        while 1:
+            try: c = result[0][0][0]
+            except IndexError: c = None
+            if c == '@': silent = 1
+            elif c == '-': ignore = 1
+            else: break
+            result[0][0] = result[0][0][1:]
+        try:
+            if not result[0][0]:
+                result[0] = result[0][1:]
+        except IndexError:
+            pass
+        return result, ignore, silent
+
+    def strfunction(self, target, source, env, executor=None):
+        if self.cmdstr is None:
+            return None
+        if self.cmdstr is not _null:
+            from SCons.Subst import SUBST_RAW
+            if executor:
+                c = env.subst(self.cmdstr, SUBST_RAW, executor=executor)
+            else:
+                c = env.subst(self.cmdstr, SUBST_RAW, target, source)
+            if c:
+                return c
+        cmd_list, ignore, silent = self.process(target, source, env, executor)
+        if silent:
+            return ''
+        return _string_from_cmd_list(cmd_list[0])
 
-    def _sig_dict(self, kw):
-        """Supply a dictionary for use in computing signatures.
+    def execute(self, target, source, env, executor=None):
+        """Execute a command action.
 
-        For signature purposes, it doesn't matter what targets or
-        sources we use, so long as we use the same ones every time
-        so the signature stays the same.  We supply an array of two
-        of each to allow for distinction between TARGET and TARGETS.
+        This will handle lists of commands as well as individual commands,
+        because construction variable substitution may turn a single
+        "command" into a list.  This means that this class can actually
+        handle lists of commands, even though that's not how we use it
+        externally.
         """
-        kw['target'] = ['__t1__', '__t2__']
-        kw['source'] = ['__s1__', '__s2__']
-        return apply(self.subst_dict, (), kw)
+        escape_list = SCons.Subst.escape_list
+        flatten_sequence = SCons.Util.flatten_sequence
 
-    def get_raw_contents(self, **kw):
-        """Return the complete contents of this action's command line.
-        """
-        return SCons.Util.scons_subst(string.join(self.cmd_list),
-                                      self._sig_dict(kw), {})
+        try:
+            shell = env['SHELL']
+        except KeyError:
+            raise SCons.Errors.UserError('Missing SHELL construction variable.')
+
+        try:
+            spawn = env['SPAWN']
+        except KeyError:
+            raise SCons.Errors.UserError('Missing SPAWN construction variable.')
+        else:
+            if is_String(spawn):
+                spawn = env.subst(spawn, raw=1, conv=lambda x: x)
+
+        escape = env.get('ESCAPE', lambda x: x)
+
+        ENV = get_default_ENV(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_sequence(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)
+
+        if executor:
+            target = executor.get_all_targets()
+            source = executor.get_all_sources()
+        cmd_list, ignore, silent = self.process(target, map(rfile, source), env, executor)
+
+        # 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 not ignore and result:
+                msg = "Error %s" % result
+                return SCons.Errors.BuildError(errstr=msg,
+                                               status=result,
+                                               action=self,
+                                               command=cmd_line)
+        return 0
 
-    def get_contents(self, **kw):
+    def get_presig(self, target, source, env, executor=None):
         """Return the signature contents of this action's command line.
 
         This strips $(-$) and everything in between the string,
         since those parts don't affect signatures.
         """
-        return SCons.Util.scons_subst(string.join(self.cmd_list),
-                                      self._sig_dict(kw), {}, _remove)
+        from SCons.Subst import SUBST_SIG
+        cmd = self.cmd_list
+        if is_List(cmd):
+            cmd = string.join(map(str, cmd))
+        else:
+            cmd = str(cmd)
+        if executor:
+            return env.subst_target_source(cmd, SUBST_SIG, executor=executor)
+        else:
+            return env.subst_target_source(cmd, SUBST_SIG, target, source)
+
+    def get_implicit_deps(self, target, source, env, executor=None):
+        icd = env.get('IMPLICIT_COMMAND_DEPENDENCIES', True)
+        if is_String(icd) and icd[:1] == '$':
+            icd = env.subst(icd)
+        if not icd or icd in ('0', 'None'):
+            return []
+        from SCons.Subst import SUBST_SIG
+        if executor:
+            cmd_list = env.subst_list(self.cmd_list, SUBST_SIG, executor=executor)
+        else:
+            cmd_list = env.subst_list(self.cmd_list, SUBST_SIG, target, source)
+        res = []
+        for cmd_line in cmd_list:
+            if cmd_line:
+                d = str(cmd_line[0])
+                m = strip_quotes.match(d)
+                if m:
+                    d = m.group(1)
+                d = env.WhereIs(d)
+                if d:
+                    res.append(env.fs.File(d))
+        return res
 
 class CommandGeneratorAction(ActionBase):
     """Class for command-generator actions."""
-    def __init__(self, generator):
+    def __init__(self, generator, kw):
+        if __debug__: logInstanceCreation(self, 'Action.CommandGeneratorAction')
         self.generator = generator
+        self.gen_kw = kw
+        self.varlist = kw.get('varlist', ())
+        self.targets = kw.get('targets', '$TARGETS')
 
-    def __generate(self, kw, for_signature):
-        import SCons.Util
-
-        # Wrap the environment dictionary in an EnvDictProxy
-        # object to make variable interpolation easier for the
-        # client.
-        args = copy.copy(kw)
-        args['for_signature'] = for_signature
-        if args.has_key("env") and not isinstance(args["env"], EnvDictProxy):
-            args["env"] = EnvDictProxy(args["env"])
-
+    def _generate(self, target, source, env, for_signature, executor=None):
         # ensure that target is a list, to make it easier to write
         # generator functions:
-        if args.has_key("target") and not SCons.Util.is_List(args["target"]):
-            args["target"] = [args["target"]]
-
-        ret = apply(self.generator, (), args)
-        gen_cmd = Action(ret)
+        if not is_List(target):
+            target = [target]
+
+        if executor:
+            target = executor.get_all_targets()
+            source = executor.get_all_sources()
+        ret = self.generator(target=target,
+                             source=source,
+                             env=env,
+                             for_signature=for_signature)
+        #TODO(1.5) gen_cmd = Action(ret, **self.gen_kw)
+        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 execute(self, **kw):
-        return apply(self.__generate(kw, 0).execute, (), kw)
-
-    def get_contents(self, **kw):
+    def __str__(self):
+        try:
+            env = self.presub_env
+        except AttributeError:
+            env = None
+        if env is None:
+            env = SCons.Defaults.DefaultEnvironment()
+        act = self._generate([], [], env, 1)
+        return str(act)
+
+    def batch_key(self, env, target, source):
+        return self._generate(target, source, env, 1).batch_key(env, target, source)
+
+    def genstring(self, target, source, env, executor=None):
+        return self._generate(target, source, env, 1, executor).genstring(target, source, env)
+
+    def __call__(self, target, source, env, exitstatfunc=_null, presub=_null,
+                 show=_null, execute=_null, chdir=_null, executor=None):
+        act = self._generate(target, source, env, 0, executor)
+        if act is None:
+            raise UserError("While building `%s': Cannot deduce file extension from source files: %s" % (repr(map(str, target)), repr(map(str, source))))
+        return act(target, source, env, exitstatfunc, presub,
+                   show, execute, chdir, executor)
+
+    def get_presig(self, target, source, env, executor=None):
         """Return the signature contents of this action's command line.
 
         This strips $(-$) and everything in between the string,
         since those parts don't affect signatures.
         """
-        return apply(self.__generate(kw, 1).get_contents, (), kw)
-
-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, executor).get_presig(target, source, env)
+
+    def get_implicit_deps(self, target, source, env, executor=None):
+        return self._generate(target, source, env, 1, executor).get_implicit_deps(target, source, env)
+
+    def get_varlist(self, target, source, env, executor=None):
+        return self._generate(target, source, env, 1, executor).get_varlist(target, source, env, executor)
+
+    def get_targets(self, env, executor):
+        return self._generate(None, None, env, 1, executor).get_targets(env, executor)
+
+
+
+# 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):
+
+    def __init__(self, var, kw):
+        if __debug__: logInstanceCreation(self, 'Action.LazyAction')
+        #FUTURE CommandAction.__init__(self, '${'+var+'}', **kw)
+        apply(CommandAction.__init__, (self, '${'+var+'}'), kw)
         self.var = SCons.Util.to_String(var)
+        self.gen_kw = kw
+
+    def get_parent_class(self, env):
+        c = env.get(self.var)
+        if is_String(c) and not '\n' in c:
+            return CommandAction
+        return CommandGeneratorAction
 
-    def __call__(self, env, **kw):
-        if env.has_key(self.var):
-            return env[self.var]
+    def _generate_cache(self, env):
+        if env:
+            c = env.get(self.var, '')
         else:
-            # The variable reference substitutes to nothing.
-            return ''
+            c = ''
+        #TODO(1.5) gen_cmd = Action(c, **self.gen_kw)
+        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, executor=None):
+        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)
+        #TODO(1.5) return c.__call__(*args, **kw)
+        return apply(c.__call__, args, kw)
+
+    def get_presig(self, target, source, env):
+        c = self.get_parent_class(env)
+        return c.get_presig(self, target, source, env)
+
+    def get_varlist(self, target, source, env, executor=None):
+        c = self.get_parent_class(env)
+        return c.get_varlist(self, target, source, env, executor)
+
 
-class FunctionAction(ActionBase):
+class FunctionAction(_ActionAction):
     """Class for Python function actions."""
-    def __init__(self, function):
-        self.function = function
-
-    def execute(self, **kw):
-        # if print_actions:
-        # XXX:  WHAT SHOULD WE PRINT HERE?
-        if execute_actions:
-            if kw.has_key('target') and not \
-               SCons.Util.is_List(kw['target']):
-                kw['target'] = [ kw['target'] ]
-            if kw.has_key('source'):
-                def rfile(n):
-                    try:
-                        return n.rfile()
-                    except AttributeError:
-                        return n
-                if not SCons.Util.is_List(kw['source']):
-                    kw['source'] = [ kw['source'] ]
-                kw['source'] = map(rfile, kw['source'])
-            if kw.has_key("env") and not isinstance(kw["env"], EnvDictProxy):
-                kw["env"] = EnvDictProxy(kw["env"])
-            return apply(self.function, (), kw)
-
-    def get_contents(self, **kw):
-        """Return the signature contents of this callable action.
-
-        By providing direct access to the code object of the
-        function, Python makes this extremely easy.  Hooray!
-        """
-        #XXX DOES NOT ACCOUNT FOR CHANGES IN ENVIRONMENT VARIABLES
-        #THE FUNCTION MAY USE
+
+    def __init__(self, execfunction, kw):
+        if __debug__: logInstanceCreation(self, 'Action.FunctionAction')
+
+        self.execfunction = execfunction
+        try:
+            self.funccontents = _callable_contents(execfunction)
+        except AttributeError:
+            try:
+                # See if execfunction will do the heavy lifting for us.
+                self.gc = execfunction.get_contents
+            except AttributeError:
+                # This is weird, just do the best we can.
+                self.funccontents = _object_contents(execfunction)
+
+        #TODO(1.5) _ActionAction.__init__(self, **kw)
+        apply(_ActionAction.__init__, (self,), kw)
+
+    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, executor=None):
+        if self.cmdstr is None:
+            return None
+        if self.cmdstr is not _null:
+            from SCons.Subst import SUBST_RAW
+            if executor:
+                c = env.subst(self.cmdstr, SUBST_RAW, executor=executor)
+            else:
+                c = env.subst(self.cmdstr, SUBST_RAW, target, source)
+            if c:
+                return c
+        def array(a):
+            def quote(s):
+                try:
+                    str_for_display = s.str_for_display
+                except AttributeError:
+                    s = repr(s)
+                else:
+                    s = str_for_display()
+                return 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, executor=None):
+        exc_info = (None,None,None)
         try:
-            # "self.function" is a function.
-            code = self.function.func_code.co_code
-        except:
-            # "self.function" is a callable object.
-            code = self.function.__call__.im_func.func_code.co_code
-        return str(code)
+            if executor:
+                target = executor.get_all_targets()
+                source = executor.get_all_sources()
+            rsources = map(rfile, source)
+            try:
+                result = self.execfunction(target=target, source=rsources, env=env)
+            except KeyboardInterrupt, e:
+                raise
+            except SystemExit, e:
+                raise
+            except Exception, e:
+                result = e
+                exc_info = sys.exc_info()
+
+            if result:
+                result = SCons.Errors.convert_to_BuildError(result, exc_info)
+                result.node=target
+                result.action=self
+                try:
+                    result.command=self.strfunction(target, source, env, executor)
+                except TypeError:
+                    result.command=self.strfunction(target, source, env)
+
+                # FIXME: This maintains backward compatibility with respect to
+                # which type of exceptions were returned by raising an
+                # exception and which ones were returned by value. It would
+                # probably be best to always return them by value here, but
+                # some codes do not check the return value of Actions and I do
+                # not have the time to modify them at this point.
+                if (exc_info[1] and
+                    not isinstance(exc_info[1],EnvironmentError)):
+                    raise result
+
+            return result
+        finally:
+            # Break the cycle between the traceback object and this
+            # function stack frame. See the sys.exc_info() doc info for
+            # more information about this issue.
+            del exc_info
+
+
+    def get_presig(self, target, source, env):
+        """Return the signature contents of this callable action."""
+        try:
+            return self.gc(target, source, env)
+        except AttributeError:
+            return self.funccontents
+
+    def get_implicit_deps(self, target, source, env):
+        return []
 
 class ListAction(ActionBase):
     """Class for lists of other actions."""
     def __init__(self, list):
-        self.list = map(lambda x: Action(x), list)
-
-    def execute(self, **kw):
-        for l in self.list:
-            r = apply(l.execute, (), kw)
-            if r:
-                return r
-        return 0
-
-    def get_contents(self, **kw):
+        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)
+        # our children will have had any varlist
+        # applied; we don't need to do it again
+        self.varlist = ()
+        self.targets = '$TARGETS'
+
+    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 __str__(self):
+        return string.join(map(str, self.list), '\n')
+
+    def presub_lines(self, env):
+        return SCons.Util.flatten_sequence(
+            map(lambda a, env=env: a.presub_lines(env), self.list))
+
+    def get_presig(self, target, source, env):
         """Return the signature contents of this action list.
 
         Simple concatenation of the signatures of the elements.
         """
+        return string.join(map(lambda x, t=target, s=source, e=env:
+                                      x.get_contents(t, s, e),
+                               self.list),
+                           "")
+
+    def __call__(self, target, source, env, exitstatfunc=_null, presub=_null,
+                 show=_null, execute=_null, chdir=_null, executor=None):
+        if executor:
+            target = executor.get_all_targets()
+            source = executor.get_all_sources()
+        for act in self.list:
+            stat = act(target, source, env, exitstatfunc, presub,
+                       show, execute, chdir, executor)
+            if stat:
+                return stat
+        return 0
 
-        return reduce(lambda x, y, kw=kw: x + str(apply(y.get_contents, (), kw)), self.list, "")
+    def get_implicit_deps(self, target, source, env):
+        result = []
+        for act in self.list:
+            result.extend(act.get_implicit_deps(target, source, env))
+        return result
+
+    def get_varlist(self, target, source, env, executor=None):
+        result = SCons.Util.OrderedDict()
+        for act in self.list:
+            for var in act.get_varlist(target, source, env, executor):
+                result[var] = True
+        return result.keys()
+
+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)
+        contents = remove_set_lineno_codes(contents)
+        return contents
+
+    def subst(self, s, target, source, env):
+        # If s is a list, recursively apply subst()
+        # to every element in the list
+        if is_List(s):
+            result = []
+            for elem in s:
+                result.append(self.subst(elem, target, source, env))
+            return self.parent.convert(result)
+
+        # 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
+        elif is_String(s):
+            return env.subst(s, 1, target, source)
+        return self.parent.convert(s)
+
+    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, executor=None):
+        args = self.subst_args(target, source, env)
+        kw = self.subst_kw(target, source, env)
+        #TODO(1.5) return self.parent.actfunc(*args, **kw)
+        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)
+        #TODO(1.5) return self.parent.strfunc(*args, **kw)
+        return apply(self.parent.strfunc, args, kw)
+
+    def __str__(self):
+        #TODO(1.5) return self.parent.strfunc(*self.args, **self.kw)
+        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, convert=lambda x: x):
+        self.actfunc = actfunc
+        self.strfunc = strfunc
+        self.convert = convert
+
+    def __call__(self, *args, **kw):
+        ac = ActionCaller(self, args, kw)
+        action = Action(ac, strfunction=ac.strfunction)
+        return action
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: