Add command generator function support. (Anthony Roach)
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Wed, 20 Feb 2002 19:35:45 +0000 (19:35 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Wed, 20 Feb 2002 19:35:45 +0000 (19:35 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@270 fdb21ef1-2011-0410-befe-b5e4ea1792b1

doc/man/scons.1
src/engine/SCons/Action.py
src/engine/SCons/ActionTests.py
src/engine/SCons/Builder.py
src/engine/SCons/BuilderTests.py
test/CommandGenerator.py [new file with mode: 0644]

index 4d1ea77267428ed50187d23afc3fe171adfd94d8..78bf54b51257c4cd05cf449b2c2e08d9535898ce 100644 (file)
@@ -1170,6 +1170,31 @@ Specifies a builder to use when a source file name suffix does not match
 any of the suffixes of the builder. Using this argument produces a
 multi-stage builder.
 
+.IP generator
+A function that returns a list of command lines that will be executed to build
+the target(s) from the source(s). The command lines must be returned as
+lists, where the first element of the list is the executable, and the other
+elements in the list are the arguments that will be passed to the
+executable. The 
+.I generator
+and
+.I action
+arguments must not both be used for the same Builder. The generator function
+should take 3 arguments:
+.I source 
+- a list of source nodes, 
+.I target
+- a list of target nodes,
+.I env
+- the construction environment. Example:
+
+.ES
+def g(env, source, target):
+       return [["gcc", "-c", "-o"] + target + source] 
+
+b = Builder(name="Object", generator=g)
+.EE
+
 .SS Action Objects
 
 The Builder function will turn its
index 19ec05cd57290eea727ec0fb7e0162ad190bef36..7bee0828ba049f474c94702d1c140d901bb36422 100644 (file)
@@ -163,10 +163,21 @@ def SetCommandHandler(func):
     global spawn
     spawn = func
 
+class CommandGenerator:
+    """
+    Wrappes a command generator function so the Action() factory
+    function can tell a generator function from a function action.
+    """
+    def __init__(self, generator):
+        self.generator = generator
+
+
 def Action(act):
     """A factory for action objects."""
     if isinstance(act, ActionBase):
         return act
+    elif isinstance(act, CommandGenerator):
+        return CommandGeneratorAction(act.generator)
     elif callable(act):
         return FunctionAction(act)
     elif SCons.Util.is_String(act):
@@ -175,7 +186,7 @@ def Action(act):
         return ListAction(act)
     else:
         return None
-    
+
 class ActionBase:
     """Base class for actions that create output objects."""
     def __cmp__(self, other):
@@ -301,6 +312,51 @@ class CommandAction(ActionBase):
         """
         return SCons.Util.scons_subst(self.command, self._sig_dict(kw), {}, _remove)
 
+class CommandGeneratorAction(ActionBase):
+    """Class for command-generator actions."""
+    def __init__(self, generator):
+        self.generator = generator
+
+    def execute(self, **kw):
+        # ensure that target is a list, to make it easier to write
+        # generator functions:
+        import SCons.Util
+        if kw.has_key("target") and not SCons.Util.is_List(kw["target"]):
+            kw["target"] = [kw["target"]]
+
+        cmd_list = apply(self.generator, (), kw)
+
+        cmd_list = map(lambda x: map(str, x), cmd_list)
+        for cmd_line in cmd_list:
+            if print_actions:
+                self.show(cmd_line)
+            if execute_actions:
+                try:
+                    ENV = kw['env']['ENV']
+                except:
+                    import SCons.Defaults
+                    ENV = SCons.Defaults.ConstructionEnvironment['ENV']
+                ret = spawn(cmd_line[0], cmd_line, ENV)
+                if ret:
+                    return ret
+
+        return 0
+
+    def get_contents(self, **kw):
+        """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.
+        """
+       kw['source'] = ["__s1__", "__s2__"]
+       kw['target'] = ["__t1__", "__t2__"]
+       cmd_list = apply(self.generator, (), kw)
+       cmd_list = map(lambda x: map(str, x), cmd_list)
+       cmd_list = map(lambda x: string.join(x,  "\0"), cmd_list)
+       cmd_list = map(lambda x: _remove.sub('', x), cmd_list)
+       cmd_list = map(lambda x: filter(lambda y: y, string.split(x, "\0")), cmd_list)
+       return cmd_list
+
 class FunctionAction(ActionBase):
     """Class for Python function actions."""
     def __init__(self, function):
index f872a1d599067ba55414d943c1312f1d1ca2dfe4..461da6e2126d7297afcd1b33d64335731eee4523 100644 (file)
@@ -142,6 +142,41 @@ class CommandActionTestCase(unittest.TestCase):
         c = a.get_contents(foo = 'FFF', bar = 'BBB')
         assert c == "| FFF BBB |"
 
+class CommandGeneratorActionTestCase(unittest.TestCase):
+
+    def test_init(self):
+        """Test creation of a command generator Action
+        """
+        def f(target, source, env):
+            pass
+        a = SCons.Action.CommandGeneratorAction(f)
+        assert a.generator == f
+
+    def test_execute(self):
+        """Test executing a command generator Action
+        """
+
+        def f(dummy, self=self):
+            self.dummy = dummy
+            return [[""]]
+
+        a = SCons.Action.CommandGeneratorAction(f)
+        self.dummy = 0
+        a.execute(dummy=1)
+        assert self.dummy == 1
+        del self.dummy
+
+    def test_get_contents(self):
+        """Test fetching the contents of a command generator Action
+        """
+        def f(target, source, foo, bar):
+            return [["guux", foo, "$(", "ignore", "$)", bar]]
+
+        a = SCons.Action.CommandGeneratorAction(f)
+        c = a.get_contents(foo = 'FFF', bar = 'BBB')
+        assert c == [["guux", 'FFF', 'BBB']], c
+
+
 class FunctionActionTestCase(unittest.TestCase):
 
     def test_init(self):
@@ -209,6 +244,7 @@ if __name__ == "__main__":
     suite.addTest(ActionBaseTestCase("test_cmp"))
     suite.addTest(ActionBaseTestCase("test_subst_dict"))
     for tclass in [CommandActionTestCase,
+                   CommandGeneratorActionTestCase,
                    FunctionActionTestCase,
                    ListActionTestCase]:
         for func in ["test_init", "test_execute", "test_get_contents"]:
index 347a6525058a8ce19acb596ec20cb7d70fdf553b..0b8fc494bb4d5fd95f7745e62a1f8089d28cde67 100644 (file)
@@ -43,6 +43,13 @@ import SCons.Util
 
 def Builder(**kw):
     """A factory for builder objects."""
+    
+    if kw.has_key('generator'):
+        if kw.has_key('action'):
+            raise UserError, "You must not specify both an action and a generator."
+        kw['action'] = SCons.Action.CommandGenerator(kw['generator'])
+        del kw['generator']
+
     if kw.has_key('action') and SCons.Util.is_Dict(kw['action']):
         return apply(CompositeBuilder, (), kw)
     elif kw.has_key('src_builder'):
index a1223d7dee1c4585e0d27c53e00af9804d94a122..68810b4bb51b5ed93a29de0121f0a24f5fee4941 100644 (file)
@@ -155,6 +155,15 @@ class BuilderTestCase(unittest.TestCase):
        builder = SCons.Builder.Builder(name="builder", action="foo")
        assert builder.action.command == "foo"
 
+    def test_generator(self):
+        """Test Builder creation given a generator function."""
+
+        def generator():
+            pass
+
+        builder = SCons.Builder.Builder(name="builder", generator=generator)
+        assert builder.action.generator == generator
+
     def test_cmp(self):
        """Test simple comparisons of Builder objects
        """
diff --git a/test/CommandGenerator.py b/test/CommandGenerator.py
new file mode 100644 (file)
index 0000000..c1413d6
--- /dev/null
@@ -0,0 +1,70 @@
+#!/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.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os.path
+import sys
+import TestSCons
+
+python = sys.executable
+
+test = TestSCons.TestSCons()
+
+test.write('build.py', r"""
+import sys
+contents = open(sys.argv[2], 'rb').read()
+file = open(sys.argv[1], 'wb')
+file.write(contents)
+file.close()
+sys.exit(0)
+""")
+
+test.write('SConstruct', """
+def g(source, target, env):
+    import sys
+    python = sys.executable
+    return [[python, "build.py", ".temp"] + source,
+            [python, "build.py"] + target + [".temp"]]
+
+b = Builder(name = 'b', generator=g)
+env = Environment(BUILDERS = [b])
+env.b(target = 'foo1.out', source = 'foo1.in')
+env.b(target = 'foo2.out', source = 'foo2.in')
+env.b(target = 'foo3.out', source = 'foo3.in')
+""")
+
+test.write('foo1.in', "foo1.in\n")
+
+test.write('foo2.in', "foo2.in\n")
+
+test.write('foo3.in', "foo3.in\n")
+
+test.run(arguments = 'foo1.out foo2.out foo3.out')
+
+test.fail_test(test.read(test.workpath('foo1.out')) != "foo1.in\n")
+test.fail_test(test.read(test.workpath('foo2.out')) != "foo2.in\n")
+test.fail_test(test.read(test.workpath('foo3.out')) != "foo3.in\n")
+
+test.pass_test()