Rebuild in response to a changed build command.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Fri, 2 Nov 2001 03:12:07 +0000 (03:12 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Fri, 2 Nov 2001 03:12:07 +0000 (03:12 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@114 fdb21ef1-2011-0410-befe-b5e4ea1792b1

src/engine/SCons/Builder.py
src/engine/SCons/BuilderTests.py
src/engine/SCons/Node/NodeTests.py
src/engine/SCons/Node/__init__.py
src/engine/SCons/Sig/MD5.py
src/engine/SCons/Sig/MD5Tests.py
src/engine/SCons/Sig/SigTests.py
src/engine/SCons/Sig/__init__.py
test/CCFLAGS.py
test/actions.py [new file with mode: 0644]

index 4023530931c4693fef1f4a955fc9818f096587b8..3bb17da3f456de450450da73fb77cf52d824e987 100644 (file)
@@ -188,6 +188,12 @@ class BuilderBase:
        """
        return apply(self.action.execute, (), kw)
 
+    def get_contents(self, **kw):
+        """Fetch the "contents" of the builder's action
+        (for signature calculation).
+        """
+        return apply(self.action.get_contents, (), kw)
+
 class MultiStepBuilder(BuilderBase):
     """This is a builder subclass that can build targets in
     multiple steps.  The src_builder parameter to the constructor
@@ -301,47 +307,85 @@ class ActionBase:
     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
+
+            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 construction
+                     variable
+
+        Any other keyword arguments are copied into the
+        dictionary."""
+
+        dict = {}
+        if kw.has_key('env'):
+            dict.update(kw['env'])
+            del kw['env']
+
+        if kw.has_key('target'):
+            t = kw['target']
+            del kw['target']
+            if type(t) is type(""):
+                t = [t]
+            dict['TARGETS'] = PathList(map(os.path.normpath, t))
+            dict['TARGET'] = dict['TARGETS'][0]
+        if kw.has_key('source'):
+            s = kw['source']
+            del kw['source']
+            if type(s) is type(""):
+                s = [s]
+            dict['SOURCES'] = PathList(map(os.path.normpath, s))
+
+        dict.update(kw)
+
+        return dict
+
 class CommandAction(ActionBase):
     """Class for command-execution actions."""
     def __init__(self, string):
-       self.command = string
+        self.command = string
 
     def execute(self, **kw):
-       loc = {}
-       if kw.has_key('target'):
-           t = kw['target']
-           if type(t) is type(""):
-               t = [t]
-           loc['TARGETS'] = PathList(map(os.path.normpath, t))
-           loc['TARGET'] = loc['TARGETS'][0]
-       if kw.has_key('source'):
-           s = kw['source']
-           if type(s) is type(""):
-               s = [s]
-            loc['SOURCES'] = PathList(map(os.path.normpath, s))
-
-       glob = {}
-       if kw.has_key('env'):
-           glob = kw['env']
-
-       cmd_str = scons_subst(self.command, loc, glob)
+        dict = apply(self.subst_dict, (), kw)
+        cmd_str = scons_subst(self.command, dict, {})
         for cmd in string.split(cmd_str, '\n'):
             if print_actions:
                 self.show(cmd)
             if execute_actions:
                 args = string.split(cmd)
                 try:
-                    ENV = glob['ENV']
+                    ENV = kw['env']['ENV']
                 except:
                     import SCons.Defaults
                     ENV = SCons.Defaults.ConstructionEnvironment['ENV']
                 ret = spawn(args[0], args, ENV)
                 if ret:
-                    #XXX This doesn't account for ignoring errors (-i)
                     return ret
         return 0
 
+    def get_contents(self, **kw):
+        """Return the signature contents of this action's command line.
 
+        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.
+        """
+        kw['target'] = ['__t1__', '__t2__']
+        kw['source'] = ['__s1__', '__s2__']
+        dict = apply(self.subst_dict, (), kw)
+        return scons_subst(self.command, dict, {})
 
 class FunctionAction(ActionBase):
     """Class for Python function actions."""
@@ -352,7 +396,24 @@ class FunctionAction(ActionBase):
        # if print_actions:
        # XXX:  WHAT SHOULD WE PRINT HERE?
        if execute_actions:
-           return self.function(kw)
+            dict = apply(self.subst_dict, (), kw)
+            return apply(self.function, (), dict)
+
+    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
+        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)
 
 class ListAction(ActionBase):
     """Class for lists of other actions."""
@@ -365,3 +426,11 @@ class ListAction(ActionBase):
            if r != 0:
                return r
        return 0
+
+    def get_contents(self, **kw):
+        """Return the signature contents of this action list.
+
+        Simple concatenation of the signatures of the elements.
+        """
+
+        return reduce(lambda x, y: x + str(y.get_contents()), self.list, "")
index e24784d38c35cce7b47dd66b7c11aaf4b878d135..89ba1fc1fe78ab15c394ca5fbc3e7668c413543d 100644 (file)
@@ -161,7 +161,7 @@ class BuilderTestCase(unittest.TestCase):
        c = test.read(outfile, 'r')
        assert c == "act.py: out5 XYZZY\nact.py: xyzzy\n", c
 
-       def function1(kw):
+       def function1(**kw):
            open(kw['out'], 'w').write("function1\n")
            return 1
 
@@ -172,7 +172,7 @@ class BuilderTestCase(unittest.TestCase):
        assert c == "function1\n", c
 
        class class1a:
-           def __init__(self, kw):
+           def __init__(self, **kw):
                open(kw['out'], 'w').write("class1a\n")
 
        builder = SCons.Builder.Builder(action = class1a)
@@ -182,7 +182,7 @@ class BuilderTestCase(unittest.TestCase):
        assert c == "class1a\n", c
 
        class class1b:
-           def __call__(self, kw):
+           def __call__(self, **kw):
                open(kw['out'], 'w').write("class1b\n")
                return 2
 
@@ -194,17 +194,17 @@ class BuilderTestCase(unittest.TestCase):
 
        cmd2 = r'%s %s %s syzygy' % (python, act_py, outfile)
 
-       def function2(kw):
+       def function2(**kw):
            open(kw['out'], 'a').write("function2\n")
            return 0
 
        class class2a:
-           def __call__(self, kw):
+           def __call__(self, **kw):
                open(kw['out'], 'a').write("class2a\n")
                return 0
 
        class class2b:
-           def __init__(self, kw):
+           def __init__(self, **kw):
                open(kw['out'], 'a').write("class2b\n")
 
        builder = SCons.Builder.Builder(action = [cmd2, function2, class2a(), class2b])
@@ -213,6 +213,25 @@ class BuilderTestCase(unittest.TestCase):
        c = test.read(outfile, 'r')
        assert c == "act.py: syzygy\nfunction2\nclass2a\nclass2b\n", c
 
+    def test_get_contents(self):
+        """Test returning the signature contents of a Builder
+        """
+
+        b1 = SCons.Builder.Builder(action = "foo")
+        contents = b1.get_contents()
+        assert contents == "foo", contents
+
+        def func():
+            pass
+
+        b2 = SCons.Builder.Builder(action = func)
+        contents = b2.get_contents()
+        assert contents == "\177\340\0\177\341\0d\0\0S", contents
+
+        b3 = SCons.Builder.Builder(action = ["foo", func, "bar"])
+        contents = b3.get_contents()
+        assert contents == "foo\177\340\0\177\341\0d\0\0Sbar", contents
+
     def test_name(self):
        """Test Builder creation with a specified name
        """
index 035bc90bbc47d5aa7d82287c982e279a7f2f9744..24ad9558209b904effb53be3b1fc748f4ade3511 100644 (file)
@@ -39,6 +39,8 @@ class Builder:
        global built_it
        built_it = 1
         return 0
+    def get_contents(self, env):
+        return 7
 
 class FailBuilder:
     def execute(self, **kw):
@@ -89,6 +91,15 @@ class NodeTestCase(unittest.TestCase):
        node.builder_set(b)
        assert node.builder == b
 
+    def test_builder_sig_adapter(self):
+        """Test the node's adapter for builder signatures
+        """
+        node = SCons.Node.Node()
+        node.builder_set(Builder())
+        node.env_set(Environment())
+        c = node.builder_sig_adapter().get_contents()
+        assert c == 7, c
+
     def test_current(self):
         """Test the default current() method
         """
index 0e1a8d970b1cbebbd1f52a6efd55b029e9d5c5e5..513b2604acf5a69a7a733298d144663ce32d2241 100644 (file)
@@ -79,6 +79,23 @@ class Node:
     def builder_set(self, builder):
        self.builder = builder
 
+    def builder_sig_adapter(self):
+        """Create an adapter for calculating a builder's signature.
+
+        The underlying signature class will call get_contents()
+        to fetch the signature of a builder, but the actual
+        content of that signature depends on the node and the
+        environment (for construction variable substitution),
+        so this adapter provides the right glue between the two.
+        """
+        class Adapter:
+            def __init__(self, node):
+                self.node = node
+            def get_contents(self):
+                env = self.node.env.Dictionary()
+                return self.node.builder.get_contents(env = env)
+        return Adapter(self)
+
     def env_set(self, env):
        self.env = env
 
index e13669eeed26058b72c6d55f717c6a69d735b2da..bd5643fa1981b0861987a5b9a392e128ce3c4b0d 100644 (file)
@@ -79,9 +79,10 @@ def signature(obj):
     """Generate a signature for an object
     """
     try:
-        contents = obj.get_contents()
-    except AttributeError:
-        raise AttributeError, "unable to fetch contents of '%s'" % str(obj)
+        contents = str(obj.get_contents())
+    except AttributeError, e:
+        raise AttributeError, \
+             "unable to fetch contents of '%s': %s" % (str(obj), e)
     return hexdigest(md5.new(contents).digest())
 
 def to_string(signature):
index f9cf61fa67518241a42a059a11a41dbde99b1abb..2d42fc0a5d238556b91d6c3c03b9c377b8113614 100644 (file)
@@ -73,12 +73,17 @@ class MD5TestCase(unittest.TestCase):
     def test_signature(self):
         """Test generating a signature"""
        o1 = my_obj(value = '111')
-        assert '698d51a19d8a121ce581499d7b701668' == signature(o1)
+        s = signature(o1)
+        assert '698d51a19d8a121ce581499d7b701668' == s, s
+
+        o2 = my_obj(value = 222)
+        s = signature(o2)
+        assert 'bcbe3365e6ac95ea2c0343a2395834dd' == s, s
 
         try:
             signature('string')
         except AttributeError, e:
-            assert str(e) == "unable to fetch contents of 'string'"
+            assert str(e) == "unable to fetch contents of 'string': 'string' object has no attribute 'get_contents'", e
         else:
             raise AttributeError, "unexpected get_contents() attribute"
 
index 95789e8f9eafd1a9b65ff8a76131a4641eaa633b..0870c939f51ba78c40c78642f41e7b4c5dd42da2 100644 (file)
@@ -87,9 +87,23 @@ class DummyNode:
     def get_bsig(self):
         return self.bsig
 
+    def set_csig(self, csig):
+        self.csig = csig
+
+    def get_csig(self):
+        return self.bsig
+
     def get_prevsiginfo(self):
         return (self.oldtime, self.oldbsig, self.oldcsig)
 
+    def builder_sig_adapter(self):
+        class Adapter:
+            def get_contents(self):
+                return 111
+            def get_timestamp(self):
+                return 222
+        return Adapter()
+
 
 def create_files(test):
     args  = [(test.workpath('f1.c'), 'blah blah', 111, 0),     #0
@@ -269,6 +283,13 @@ class CalcTestCase(unittest.TestCase):
                 return 0, self.bsig, self.csig
             def get_timestamp(self):
                 return 1
+            def builder_sig_adapter(self):
+                class MyAdapter:
+                    def get_csig(self):
+                        return 333
+                    def get_timestamp(self):
+                        return 444
+                return MyAdapter()
 
         self.module = MySigModule()
         self.nodeclass = MyNode
@@ -318,7 +339,7 @@ class CalcTestCase(unittest.TestCase):
         n4 = self.nodeclass('n4', None, None)
         n4.builder = 1
         n4.kids = [n2, n3]
-        assert self.calc.get_signature(n4) == 57
+        assert self.calc.get_signature(n4) == 390
 
         n5 = NE('n5', 55, 56)
         assert self.calc.get_signature(n5) is None
index 40957926331010e6c37971cf8fbc78e924d30118..43e3b8aba4565e3b9a044213a1bf5fe78b963c3c 100644 (file)
@@ -150,9 +150,13 @@ class Calculator:
         already built and updated by someone else, if that's
         what's wanted.
         """
+        if not node.use_signature:
+            return None
         #XXX If configured, use the content signatures from the
         #XXX .sconsign file if the timestamps match.
         sigs = map(lambda n,s=self: s.get_signature(n), node.children())
+        if node.builder:
+            sigs.append(self.module.signature(node.builder_sig_adapter()))
         return self.module.collect(filter(lambda x: not x is None, sigs))
 
     def csig(self, node):
@@ -163,6 +167,8 @@ class Calculator:
         node - the node
         returns - the content signature
         """
+        if not node.use_signature:
+            return None
         #XXX If configured, use the content signatures from the
         #XXX .sconsign file if the timestamps match.
         return self.module.signature(node)
index cc6ad737d14669ebea831e65a8ff55b82d5e5772..8b0e5a8bc883cf8b35db0d48bd86e0385ede86e4 100644 (file)
@@ -53,9 +53,22 @@ main(int argc, char *argv[])
 """)
 
 
-test.run(arguments = 'foo bar')
+test.run(arguments = '.')
 
 test.run(program = test.workpath('foo'), stdout = "prog.c:  FOO\n")
 test.run(program = test.workpath('bar'), stdout = "prog.c:  BAR\n")
 
+test.write('SConstruct', """
+bar = Environment(CCFLAGS = '-DBAR')
+bar.Object(target = 'foo.o', source = 'prog.c')
+bar.Object(target = 'bar.o', source = 'prog.c')
+bar.Program(target = 'foo', source = 'foo.o')
+bar.Program(target = 'bar', source = 'bar.o')
+""")
+
+test.run(arguments = '.')
+
+test.run(program = test.workpath('foo'), stdout = "prog.c:  BAR\n")
+test.run(program = test.workpath('bar'), stdout = "prog.c:  BAR\n")
+
 test.pass_test()
diff --git a/test/actions.py b/test/actions.py
new file mode 100644 (file)
index 0000000..cee067e
--- /dev/null
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2001 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 sys
+import TestSCons
+
+python = sys.executable
+
+test = TestSCons.TestSCons()
+
+test.write('build.py', r"""
+import sys
+file = open(sys.argv[1], 'wb')
+file.write(sys.argv[2] + "\n")
+file.write(open(sys.argv[3], 'rb').read())
+file.close
+sys.exit(0)
+""")
+
+test.write('SConstruct', """
+B = Builder(name = 'B', action = r'%s build.py $TARGET 1 $SOURCES')
+env = Environment(BUILDERS = [B])
+env.B(target = 'foo.out', source = 'foo.in')
+""" % python)
+
+test.write('foo.in', "foo.in\n")
+
+test.run(arguments = '.')
+
+test.fail_test(test.read('foo.out') != "1\nfoo.in\n")
+
+test.up_to_date(arguments = '.')
+
+test.write('SConstruct', """
+B = Builder(name = 'B', action = r'%s build.py $TARGET 2 $SOURCES')
+env = Environment(BUILDERS = [B])
+env.B(target = 'foo.out', source = 'foo.in')
+""" % python)
+
+test.run(arguments = '.')
+
+test.fail_test(test.read('foo.out') != "2\nfoo.in\n")
+
+test.up_to_date(arguments = '.')
+
+test.write('SConstruct', """
+import os
+import SCons.Util
+def func(**kw):
+    cmd = SCons.Util.scons_subst(r'%s build.py $TARGET 3 $SOURCES', kw, {})
+    return os.system(cmd)
+B = Builder(name = 'B', action = func)
+env = Environment(BUILDERS = [B])
+env.B(target = 'foo.out', source = 'foo.in')
+""" % python)
+
+test.run(arguments = '.')
+
+test.fail_test(test.read('foo.out') != "3\nfoo.in\n")
+
+test.up_to_date(arguments = '.')
+
+test.write('SConstruct', """
+import os
+import SCons.Util
+class bld:
+    def __init__(self):
+        self.cmd = r'%s build.py $TARGET 4 $SOURCES'
+    def __call__(self, **kw):
+        cmd = SCons.Util.scons_subst(self.cmd, kw, {})
+        return os.system(cmd)
+    def get_contents(self, **kw):
+        cmd = SCons.Util.scons_subst(self.cmd, kw, {})
+        return cmd
+B = Builder(name = 'B', action = bld())
+env = Environment(BUILDERS = [B])
+env.B(target = 'foo.out', source = 'foo.in')
+""" % python)
+
+test.run(arguments = '.')
+
+test.fail_test(test.read('foo.out') != "4\nfoo.in\n")
+
+test.up_to_date(arguments = '.')
+
+test.pass_test()