Commits

Anonymous committed 7290b9f

Rebuild in response to a changed build command.

Comments (0)

Files changed (10)

src/engine/SCons/Builder.py

 	"""
 	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
     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."""
 	# 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."""
 	    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, "")

src/engine/SCons/BuilderTests.py

 	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
 
 	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)
 	assert c == "class1a\n", c
 
 	class class1b:
-	    def __call__(self, kw):
+	    def __call__(self, **kw):
 		open(kw['out'], 'w').write("class1b\n")
 		return 2
 
 
 	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])
 	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
 	"""

src/engine/SCons/Node/NodeTests.py

 	global built_it
 	built_it = 1
         return 0
+    def get_contents(self, env):
+        return 7
 
 class FailBuilder:
     def execute(self, **kw):
 	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
         """

src/engine/SCons/Node/__init__.py

     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
 

src/engine/SCons/Sig/MD5.py

     """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):

src/engine/SCons/Sig/MD5Tests.py

     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"
 

src/engine/SCons/Sig/SigTests.py

     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
                 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
         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

src/engine/SCons/Sig/__init__.py

         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):
         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)
 """)
 
 
-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()
+#!/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()