Add AddPreAction() and AddPostAction() methods. (Charles Crain)
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Mon, 20 Jan 2003 06:14:54 +0000 (06:14 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Mon, 20 Jan 2003 06:14:54 +0000 (06:14 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@558 fdb21ef1-2011-0410-befe-b5e4ea1792b1

doc/man/scons.1
src/CHANGES.txt
src/engine/SCons/Action.py
src/engine/SCons/ActionTests.py
src/engine/SCons/Node/NodeTests.py
src/engine/SCons/Node/__init__.py
src/engine/SCons/Script/SConscript.py
test/append-action.py [new file with mode: 0644]
test/pre-post-actions.py [new file with mode: 0644]

index 7ad28578ba458ec62efe871d6bbd261eccdf5d92..00db84e4e1b68d3c05fdec3bb62633c56daeb70a 100644 (file)
@@ -31,7 +31,7 @@
 .RE
 .fi
 ..
-.TH SCONS 1 "November 2002"
+.TH SCONS 1 "February 2003"
 .SH NAME
 scons \- a software construction tool
 .SH SYNOPSIS
@@ -2323,6 +2323,32 @@ build).
 is usually safe, and is always more efficient than 
 .IR duplicate =1.
 
+.TP 
+.RI AddPostAction ( target, action )
+Arranges for the specified
+.I action
+to be performed
+after the specified
+.I target
+has been built.
+The specified action(s) may be
+an Action object, or anything that
+can be converted into an Action object
+(see below).
+
+.TP 
+.RI AddPreAction ( target, action )
+Arranges for the specified
+.I action
+to be performed
+before the specified
+.I target
+is built.
+The specified action(s) may be
+an Action object, or anything that
+can be converted into an Action object
+(see below).
+
 .TP 
 .RI Clean ( target, files_or_dirs )
 This specifies a list of files or directories which should be removed
index 50f58d3101a9794ac03ff6ccfd75588926653b0e..be0a4c9cc92e040853132b92c39423021e1f5290 100644 (file)
 
 RELEASE 0.11 - XXX
 
+  From Charles Crain:
+
+  - Added new AddPreAction() and AddPostAction() functions that support
+    taking additional actions before or after building specific targets.
+
   From Steven Knight:
 
   - Allow Python function Actions to specify a list of construction
index f185641ecd970ed59a48aabde0629ac338e48b58..646ee8dc061c4eeb2302e4461d57d74d69155c17 100644 (file)
@@ -64,6 +64,25 @@ def SetCommandHandler(func, escape = lambda x: x):
 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
+    # 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 ])
+
 class CommandGenerator:
     """
     Wraps a command generator function so the Action() factory
@@ -72,6 +91,12 @@ class CommandGenerator:
     def __init__(self, generator):
         self.generator = generator
 
+    def __add__(self, other):
+        return _actionAppend(self, other)
+
+    def __radd__(self, other):
+        return _actionAppend(other, self)
+
 def _do_create_action(act, strfunction=_null, varlist=[]):
     """This is the actual "implementation" for the
     Action factory method, below.  This handles the
@@ -178,6 +203,12 @@ class ActionBase:
 
         return dict
 
+    def __add__(self, other):
+        return _actionAppend(self, other)
+
+    def __radd__(self, other):
+        return _actionAppend(other, self)
+
 def _string_from_cmd_list(cmd_list):
     """Takes a list of command line arguments and returns a pretty
     representation for printing."""
index ba6c91dd218dbeabfce08c94c206bbf434f97fe0..0f96899f4c803e0c8a14fc2df35755f2c820ddae 100644 (file)
@@ -234,6 +234,83 @@ class ActionBaseTestCase(unittest.TestCase):
         SOURCES.sort()
         assert SOURCES == ['rstr-s4', 's3'], d['SOURCES']
 
+    def test_add(self):
+        """Test adding Actions to stuff."""
+        # Adding actions to other Actions or to stuff that can
+        # be converted into an Action should produce a ListAction
+        # containing all the Actions.
+        def bar():
+            return None
+        baz = SCons.Action.CommandGenerator(bar)
+        act1 = SCons.Action.Action('foo bar')
+        act2 = SCons.Action.Action([ 'foo', bar ])
+
+        sum = act1 + act2
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 3, len(sum.list)
+        assert map(lambda x: isinstance(x, SCons.Action.ActionBase),
+                   sum.list) == [ 1, 1, 1 ]
+
+        sum = act1 + act1
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 2, len(sum.list)
+
+        sum = act2 + act2
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 4, len(sum.list)
+
+        # Should also be able to add command generators to each other
+        # or to actions
+        sum = baz + baz
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 2, len(sum.list)
+
+        sum = baz + act1
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 2, len(sum.list)
+
+        sum = act2 + baz
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 3, len(sum.list)
+
+        # Also should be able to add Actions to anything that can
+        # be converted into an action.
+        sum = act1 + bar
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 2, len(sum.list)
+        assert isinstance(sum.list[1], SCons.Action.FunctionAction)
+
+        sum = 'foo bar' + act2
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 3, len(sum.list)
+        assert isinstance(sum.list[0], SCons.Action.CommandAction)
+
+        sum = [ 'foo', 'bar' ] + act1
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 3, sum.list
+        assert isinstance(sum.list[0], SCons.Action.CommandAction)
+        assert isinstance(sum.list[1], SCons.Action.CommandAction)
+
+        sum = act2 + [ baz, bar ]
+        assert isinstance(sum, SCons.Action.ListAction), str(sum)
+        assert len(sum.list) == 4, len(sum.list)
+        assert isinstance(sum.list[2], SCons.Action.CommandGeneratorAction)
+        assert isinstance(sum.list[3], SCons.Action.FunctionAction)
+
+        try:
+            sum = act2 + 1
+        except TypeError:
+            pass
+        else:
+            assert 0, "Should have thrown a TypeError adding to an int."
+
+        try:
+            sum = 1 + act2
+        except TypeError:
+            pass
+        else:
+            assert 0, "Should have thrown a TypeError adding to an int."
+        
 class CommandActionTestCase(unittest.TestCase):
 
     def test_init(self):
index 23740269206670c8d1b1bb8a7314d6c919e72bac..3b66f431a7e674dbdd25bc5baacf75ff81f4aba3 100644 (file)
@@ -37,14 +37,38 @@ built_it = None
 built_target =  None
 built_source =  None
 cycle_detected = None
+built_order = 0
 
 class MyAction:
+    def __init__(self):
+        self.order = 0
+        
     def __call__(self, target, source, env):
-        global built_it, built_target, built_source, built_args
+        global built_it, built_target, built_source, built_args, built_order
         built_it = 1
         built_target = target
         built_source = source
         built_args = env
+        built_order = built_order + 1
+        self.order = built_order
+        return 0
+
+class MyNonGlobalAction:
+    def __init__(self):
+        self.order = 0
+        self.built_it = None
+        self.built_target =  None
+        self.built_source =  None
+
+    def __call__(self, target, source, env):
+        # Okay, so not ENTIRELY non-global...
+        global built_order
+        self.built_it = 1
+        self.built_target = target
+        self.built_source = source
+        self.built_args = env
+        built_order = built_order + 1
+        self.order = built_order
         return 0
 
 class Builder:
@@ -116,7 +140,7 @@ class NodeTestCase(unittest.TestCase):
     def test_build(self):
         """Test building a node
         """
-        global built_it
+        global built_it, built_order
 
         # Make sure it doesn't blow up if no builder is set.
         node = MyNode("www")
@@ -159,7 +183,39 @@ class NodeTestCase(unittest.TestCase):
         ggg.path = "ggg"
         fff.sources = ["hhh", "iii"]
         ggg.sources = ["hhh", "iii"]
+        # [Charles C. 1/7/2002] Uhhh, why are there no asserts here?
 
+        built_it = None
+        built_order = 0
+        node = MyNode("xxx")
+        node.builder_set(Builder())
+        node.env_set(Environment())
+        node.sources = ["yyy", "zzz"]
+        pre1 = MyNonGlobalAction()
+        pre2 = MyNonGlobalAction()
+        post1 = MyNonGlobalAction()
+        post2 = MyNonGlobalAction()
+        node.add_pre_action(pre1)
+        node.add_pre_action(pre2)
+        node.add_post_action(post1)
+        node.add_post_action(post2)
+        node.build()
+        assert built_it
+        assert pre1.built_it
+        assert pre2.built_it
+        assert post1.built_it
+        assert post2.built_it
+        assert pre1.order == 1, pre1.order
+        assert pre2.order == 2, pre1.order
+        # The action of the builder itself is order 3...
+        assert post1.order == 4, pre1.order
+        assert post2.order == 5, pre1.order
+
+        for act in [ pre1, pre2, post1, post2 ]:
+            assert type(act.built_target[0]) == type(MyNode("bar")), type(act.built_target[0])
+            assert str(act.built_target[0]) == "xxx", str(act.built_target[0])
+            assert act.built_source == ["yyy", "zzz"], act.built_source
+            
     def test_depends_on(self):
         """Test the depends_on() method
         """
index 618a8fba0b149b4ad9d0dbeeccb78478ee9275e5..9641ea70cbb43aec0cd43106e4c3806d3b66942d 100644 (file)
@@ -102,6 +102,8 @@ class Node:
         self.attributes = self.Attrs() # Generic place to stick information about the Node.
         self.side_effect = 0 # true iff this node is a side effect
         self.side_effects = [] # the side effects of building this target
+        self.pre_actions = []
+        self.post_actions = []
 
     def generate_build_env(self):
         return self.env.Override(self.overrides)
@@ -115,7 +117,8 @@ class Node:
         """
         if not self.has_builder():
             return None
-        action_list = self.builder.get_actions()
+        action_list = self.pre_actions + self.builder.get_actions() + \
+                      self.post_actions
         if not action_list:
             return
         targets = self.builder.targets(self)
@@ -470,6 +473,16 @@ class Node:
         the command interpreter literally."""
         return 1
 
+    def add_pre_action(self, act):
+        """Adds an Action performed on this Node only before
+        building it."""
+        self.pre_actions.append(act)
+
+    def add_post_action(self, act):
+        """Adds and Action performed on this Node only after
+        building it."""
+        self.post_actions.append(act)
+
     def render_include_tree(self):
         """
         Return a text representation, suitable for displaying to the
index d6bba7d652032a1ed61c073a7b8629a522caa29a..c5cccb62dcb2846e974303af7de952db7b52fc74 100644 (file)
@@ -30,6 +30,7 @@ files.
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
+import SCons.Action
 import SCons.Builder
 import SCons.Defaults
 import SCons.Environment
@@ -336,6 +337,16 @@ def Clean(target, files):
     else:
         clean_targets[s] = nodes
 
+def AddPreAction(files, action):
+    nodes = SCons.Node.arg2nodes(files, SCons.Node.FS.default_fs.Entry)
+    for n in nodes:
+        n.add_pre_action(SCons.Action.Action(action))
+
+def AddPostAction(files, action):
+    nodes = SCons.Node.arg2nodes(files, SCons.Node.FS.default_fs.Entry)
+    for n in nodes:
+        n.add_post_action(SCons.Action.Action(action))
+
 def BuildDefaultGlobals():
     """
     Create a dictionary containing all the default globals for 
@@ -344,6 +355,8 @@ def BuildDefaultGlobals():
 
     globals = {}
     globals['Action']            = SCons.Action.Action
+    globals['AddPostAction']     = AddPostAction
+    globals['AddPreAction']      = AddPreAction
     globals['ARGUMENTS']         = arguments
     globals['BuildDir']          = BuildDir
     globals['Builder']           = SCons.Builder.Builder
diff --git a/test/append-action.py b/test/append-action.py
new file mode 100644 (file)
index 0000000..c8de281
--- /dev/null
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001, 2002 Steven Knight
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# 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.
+#
+# This test exercises the addition operator of Action objects.
+# Using Environment.Prepend() and Environment.Append(), you should be
+# able to add new actions to existing ones, effectively adding steps
+# to a build process.
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import stat
+import sys
+import TestSCons
+
+if sys.platform == 'win32':
+    _exe = '.exe'
+else:
+    _exe = ''
+
+test = TestSCons.TestSCons()
+
+test.write('foo.c', r"""
+#include <stdio.h>
+
+int main(void)
+{
+    printf("Foo\n");
+    return 0;
+}
+""")
+
+test.write('SConstruct', """
+import os.path
+
+env=Environment()
+
+def before(env, target, source):
+    f=open(str(target[0]), "wb")
+    f.write("Foo\\n")
+    f.close()
+    f=open("before.txt", "wb")
+    f.write("Bar\\n")
+    f.close()
+
+def after(env, target, source):
+    fin = open(str(target[0]), "rb")
+    fout = open("after%s", "wb")
+    fout.write(fin.read())
+    fout.close()
+    fin.close()
+
+env.Prepend(LINKCOM=Action(before))
+env.Append(LINKCOM=Action(after))
+env.Program(source='foo.c', target='foo')
+""" % _exe)
+
+after_exe = test.workpath('after' + _exe)
+
+test.run(arguments='.')
+test.fail_test(open('before.txt', 'rb').read() != "Bar\n")
+os.chmod(after_exe, os.stat(after_exe)[stat.ST_MODE] | stat.S_IXUSR)
+test.run(program=after_exe, stdout="Foo\n")
+test.pass_test()
diff --git a/test/pre-post-actions.py b/test/pre-post-actions.py
new file mode 100644 (file)
index 0000000..d3139bb
--- /dev/null
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001, 2002 Steven Knight
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# 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.
+#
+# This test exercises the AddPreAction() and AddPostAction() API
+# functions, which add pre-build and post-build actions to nodes.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os
+import stat
+import sys
+import TestSCons
+
+if sys.platform == 'win32':
+    _exe = '.exe'
+else:
+    _exe = ''
+
+test = TestSCons.TestSCons()
+
+test.write('foo.c', r"""
+#include <stdio.h>
+
+int main(void)
+{
+    printf("Foo\n");
+    return 0;
+}
+""")
+
+test.write('SConstruct', """
+import os.path
+
+env=Environment()
+
+def before(env, target, source):
+    f=open(str(target[0]), "wb")
+    f.write("Foo\\n")
+    f.close()
+    f=open("before.txt", "wb")
+    f.write("Bar\\n")
+    f.close()
+
+def after(env, target, source):
+    fin = open(str(target[0]), "rb")
+    fout = open("after%s", "wb")
+    fout.write(fin.read())
+    fout.close()
+    fin.close()
+
+foo = env.Program(source='foo.c', target='foo')
+AddPreAction(foo, before)
+AddPostAction('foo%s', after)
+""" % (_exe, _exe))
+
+after_exe = test.workpath('after' + _exe)
+
+test.run(arguments='.')
+test.fail_test(open('before.txt', 'rb').read() != "Bar\n")
+os.chmod(after_exe, os.stat(after_exe)[stat.ST_MODE] | stat.S_IXUSR)
+test.run(program=test.workpath(after_exe), stdout="Foo\n")
+test.pass_test()