Add the highly anticipated --debug=explain option to provide build reasoning.
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 17 Apr 2004 12:34:55 +0000 (12:34 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Sat, 17 Apr 2004 12:34:55 +0000 (12:34 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@957 fdb21ef1-2011-0410-befe-b5e4ea1792b1

12 files changed:
src/engine/SCons/Action.py
src/engine/SCons/ActionTests.py
src/engine/SCons/Executor.py
src/engine/SCons/ExecutorTests.py
src/engine/SCons/Node/FS.py
src/engine/SCons/Node/FSTests.py
src/engine/SCons/Node/NodeTests.py
src/engine/SCons/Node/__init__.py
src/engine/SCons/Script/__init__.py
src/engine/SCons/Sig/SigTests.py
src/engine/SCons/Sig/__init__.py
test/explain.py [new file with mode: 0644]

index f7024d344885529a7df74ff01eb4e7f51624e8a7..7397873527c0249f9016c866ad45e7775873077d 100644 (file)
@@ -29,6 +29,14 @@ other modules:
         This is what the Sig/*.py subsystem uses 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 compare the actions used to build a target last time and
+        this time.
+
 Subclasses also supply the following methods for internal use within
 this module:
 
@@ -215,6 +223,9 @@ class ActionBase:
                 (string.join(map(lambda x: str(x), target), ' and '),
                  string.join(lines, '\n  ')))
 
+    def genstring(self, target, source, env):
+        return str(self)
+
     def get_actions(self):
         return [self]
 
@@ -384,6 +395,9 @@ class CommandGeneratorAction(ActionBase):
         act = self.__generate([], [], env, 0)
         return str(act)
 
+    def genstring(self, target, source, env):
+        return str(self.__generate(target, source, env, 0))
+
     def _execute(self, target, source, env):
         if not SCons.Util.is_List(source):
             source = [source]
index 5672dac5aee1e35ae6a46baf7261ab79e538d931..a273607e5cb470d92ea6bf5a2d84fed7767355fa 100644 (file)
@@ -465,6 +465,44 @@ class CommandActionTestCase(unittest.TestCase):
         s = str(act)
         assert s == "['xyzzy', '$TARGET', '$SOURCE', '$TARGETS', '$SOURCES']", s
 
+    def test_genstring(self):
+        """Test the genstring() method for command Actions
+        """
+
+        env = Environment()
+        t1 = DummyNode('t1')
+        t2 = DummyNode('t2')
+        s1 = DummyNode('s1')
+        s2 = DummyNode('s2')
+        act = SCons.Action.CommandAction('xyzzy $TARGET $SOURCE')
+        expect = 'xyzzy $TARGET $SOURCE'
+        s = act.genstring([], [], env)
+        assert s == expect, s
+        s = act.genstring([t1], [s1], env)
+        assert s == expect, s
+        s = act.genstring([t1, t2], [s1, s2], env)
+        assert s == expect, s
+
+        act = SCons.Action.CommandAction('xyzzy $TARGETS $SOURCES')
+        expect = 'xyzzy $TARGETS $SOURCES'
+        s = act.genstring([], [], env)
+        assert s == expect, s
+        s = act.genstring([t1], [s1], env)
+        assert s == expect, s
+        s = act.genstring([t1, t2], [s1, s2], env)
+        assert s == expect, s
+
+        act = SCons.Action.CommandAction(['xyzzy',
+                                          '$TARGET', '$SOURCE',
+                                          '$TARGETS', '$SOURCES'])
+        expect = "['xyzzy', '$TARGET', '$SOURCE', '$TARGETS', '$SOURCES']"
+        s = act.genstring([], [], env)
+        assert s == expect, s
+        s = act.genstring([t1], [s1], env)
+        assert s == expect, s
+        s = act.genstring([t1, t2], [s1, s2], env)
+        assert s == expect, s
+
     def test_strfunction(self):
         """Test fetching the string representation of command Actions
         """
@@ -829,6 +867,19 @@ class CommandGeneratorActionTestCase(unittest.TestCase):
         s = str(a)
         assert s == 'FOO', s
 
+    def test_genstring(self):
+        """Test the command generator Action genstring() method
+        """
+        def f(target, source, env, for_signature, self=self):
+            dummy = env['dummy']
+            self.dummy = dummy
+            return "$FOO $TARGET $SOURCE $TARGETS $SOURCES"
+        a = SCons.Action.CommandGeneratorAction(f)
+        self.dummy = 0
+        s = a.genstring([], [], env=Environment(FOO='xyzzy', dummy=1))
+        assert self.dummy == 1, self.dummy
+        assert s == "$FOO $TARGET $SOURCE $TARGETS $SOURCES", s
+
     def test_strfunction(self):
         """Test the command generator Action string function
         """
@@ -1079,7 +1130,7 @@ class ListActionTestCase(unittest.TestCase):
         assert g == [l[1]], g
 
     def test___str__(self):
-        """Test the __str__() method ffor a list of subsidiary Actions
+        """Test the __str__() method for a list of subsidiary Actions
         """
         def f(target,source,env):
             pass
@@ -1089,6 +1140,17 @@ class ListActionTestCase(unittest.TestCase):
         s = str(a)
         assert s == "f(env, target, source)\ng(env, target, source)\nXXX\nf(env, target, source)", s
 
+    def test_genstring(self):
+        """Test the genstring() method for a list of subsidiary Actions
+        """
+        def f(target,source,env):
+            pass
+        def g(target,source,env):
+            pass
+        a = SCons.Action.ListAction([f, g, "XXX", f])
+        s = a.genstring([], [], Environment())
+        assert s == "f(env, target, source)\ng(env, target, source)\nXXX\nf(env, target, source)", s
+
     def test_strfunction(self):
         """Test the string function for a list of subsidiary Actions
         """
@@ -1173,6 +1235,15 @@ class LazyActionTestCase(unittest.TestCase):
         s = a.strfunction([], [], env=Environment(BAR=f, s=self))
         assert s == "f([], [])", s
 
+    def test_genstring(self):
+        """Test the lazy-evaluation Action genstring() method
+        """
+        def f(target, source, env):
+            pass
+        a = SCons.Action.Action('$BAR')
+        s = a.genstring([], [], env=Environment(BAR=f, s=self))
+        assert s == "f(env, target, source)", s
+
     def test_execute(self):
         """Test executing a lazy-evaluation Action
         """
index b636f60658dd56cdea8dfd45554a40ece7281124..ac209e0519932af81358607f4f462e6182041348 100644 (file)
@@ -133,6 +133,16 @@ class Executor:
         slist = filter(lambda x, s=self.sources: x not in s, sources)
         self.sources.extend(slist)
 
+    def __str__(self):
+        try:
+            return self.string
+        except AttributeError:
+            action = self.builder.action
+            self.string = action.genstring(self.targets,
+                                           self.sources,
+                                           self.get_build_env())
+            return self.string
+
     def get_raw_contents(self):
         """Fetch the raw signature contents.  This, along with
         get_contents(), is the real reason this class exists, so we can
index 9c640121ffdb8d01658440c1bcec4119bac8e63b..23b7719e7421bb1afa808331a45a4d2ea8d4b874 100644 (file)
@@ -47,6 +47,8 @@ class MyAction:
     actions = ['action1', 'action2']
     def get_actions(self):
         return self.actions
+    def genstring(self, target, source, env):
+        return string.join(['GENSTRING'] + self.actions + target + source)
     def get_raw_contents(self, target, source, env):
         return string.join(['RAW'] + self.actions + target + source)
     def get_contents(self, target, source, env):
@@ -165,6 +167,14 @@ class ExecutorTestCase(unittest.TestCase):
         x.add_sources(['s3', 's1', 's4'])
         assert x.sources == ['s1', 's2', 's3', 's4'], x.sources
 
+    def test___str__(self):
+        """Test the __str__() method"""
+        env = MyEnvironment(S='string')
+
+        x = SCons.Executor.Executor(MyBuilder(env, {}), None, {}, ['t'], ['s'])
+        c = str(x)
+        assert c == 'GENSTRING action1 action2 t s', c
+
     def test_get_raw_contents(self):
         """Test fetching the raw signatures contents"""
         env = MyEnvironment(RC='raw contents')
index bb13afcff92835c250b94ba59d645dc87e7f1438..a349f774a0aa35822d6527ff319586fedd2b853b 100644 (file)
@@ -1225,13 +1225,13 @@ class Dir(Base):
         """A directory does not get scanned."""
         return None
 
-    def set_bsig(self, bsig):
+    def set_binfo(self, bsig, bkids, bkidsigs, bact, bactsig):
         """A directory has no signature."""
-        bsig = None
+        pass
 
     def set_csig(self, csig):
         """A directory has no signature."""
-        csig = None
+        pass
 
     def get_contents(self):
         """Return aggregate contents of all our children."""
@@ -1363,8 +1363,12 @@ class File(Base):
     def store_csig(self):
         self.dir.sconsign().set_csig(self.name, self.get_csig())
 
-    def store_bsig(self):
-        self.dir.sconsign().set_bsig(self.name, self.get_bsig())
+    def store_binfo(self):
+        binfo = self.get_binfo()
+        apply(self.dir.sconsign().set_binfo, (self.name,) + binfo)
+
+    def get_stored_binfo(self):
+        return self.dir.sconsign().get_binfo(self.name)
 
     def store_implicit(self):
         self.dir.sconsign().set_implicit(self.name, self.implicit)
@@ -1613,7 +1617,7 @@ class File(Base):
                         # ...and they'd like a local copy.
                         LocalCopy(self, r, None)
                         self.set_bsig(bsig)
-                        self.store_bsig()
+                        self.store_binfo()
                     return 1
             self._rfile = self
             return None
index b1999e08376d1c0a50532f496d37898dd0c1c6f2..6fbcfdc81382b3e93b3390ef8a64205d88cc3d04 100644 (file)
@@ -815,7 +815,7 @@ class FSTestCase(unittest.TestCase):
         e8 = fs.Entry("e8")
         assert e8.get_bsig() is None, e8.get_bsig()
         assert e8.get_csig() is None, e8.get_csig()
-        e8.set_bsig('xxx')
+        e8.set_binfo('xxx', [], [], [], [])
         e8.set_csig('yyy')
         assert e8.get_bsig() == 'xxx', e8.get_bsig()
         assert e8.get_csig() == 'yyy', e8.get_csig()
@@ -823,7 +823,7 @@ class FSTestCase(unittest.TestCase):
         f9 = fs.File("f9")
         assert f9.get_bsig() is None, f9.get_bsig()
         assert f9.get_csig() is None, f9.get_csig()
-        f9.set_bsig('xxx')
+        f9.set_binfo('xxx', [], [], [], [])
         f9.set_csig('yyy')
         assert f9.get_bsig() == 'xxx', f9.get_bsig()
         assert f9.get_csig() == 'yyy', f9.get_csig()
@@ -831,7 +831,7 @@ class FSTestCase(unittest.TestCase):
         d10 = fs.Dir("d10")
         assert d10.get_bsig() is None, d10.get_bsig()
         assert d10.get_csig() is None, d10.get_csig()
-        d10.set_bsig('xxx')
+        d10.set_binfo('xxx', [], [], [], [])
         d10.set_csig('yyy')
         assert d10.get_bsig() is None, d10.get_bsig()
         assert d10.get_csig() is None, d10.get_csig()
@@ -1651,7 +1651,7 @@ class CacheDirTestCase(unittest.TestCase):
         SCons.Sig.MD5.collect = my_collect
         try:
             f5 = fs.File("cd.f5")
-            f5.set_bsig('a_fake_bsig')
+            f5.set_binfo('a_fake_bsig', [], [], [], [])
             cp = f5.cachepath()
             dirname = os.path.join('cache', 'A')
             filename = os.path.join(dirname, 'a_fake_bsig')
@@ -1661,7 +1661,7 @@ class CacheDirTestCase(unittest.TestCase):
 
         # Verify that no bsig raises an InternalERror
         f6 = fs.File("cd.f6")
-        f6.set_bsig(None)
+        f6.set_binfo(None, [], [], [], [])
         exc_caught = 0
         try:
             cp = f6.cachepath()
index 63b945ff26b327fc1fbe5f91486b6cf0e3423291..e9d779c235589317efb5c3ab3ae14c8ad1576c69 100644 (file)
@@ -344,18 +344,34 @@ class NodeTestCase(unittest.TestCase):
         a = node.builder.get_actions()
         assert isinstance(a[0], MyAction), a[0]
 
-    def test_set_bsig(self):
-        """Test setting a Node's signature
+    def test_set_binfo(self):
+        """Test setting a Node's build information
+        """
+        node = SCons.Node.Node()
+        node.set_binfo('www', ['w1'], ['w2'], 'w act', 'w actsig')
+        assert node.bsig == 'www', node.bsig
+        assert node.bkids == ['w1'], node.bkdids
+        assert node.bkidsigs == ['w2'], node.bkidsigs
+        assert node.bact == 'w act', node.bkdid
+        assert node.bactsig == 'w actsig', node.bkidsig
+
+    def test_get_binfo(self):
+        """Test fetching a Node's build information
         """
         node = SCons.Node.Node()
-        node.set_bsig('www')
-        assert node.bsig == 'www'
+        node.set_binfo('yyy', ['y1'], ['y2'], 'y act', 'y actsig')
+        bsig, bkids, bkidsigs, bact, bactsig = node.get_binfo()
+        assert bsig == 'yyy', bsig
+        assert bkids == ['y1'], bkdids
+        assert bkidsigs == ['y2'], bkidsigs
+        assert bact == 'y act', bkdid
+        assert bactsig == 'y actsig', bkidsig
 
     def test_get_bsig(self):
         """Test fetching a Node's signature
         """
         node = SCons.Node.Node()
-        node.set_bsig('xxx')
+        node.set_binfo('xxx', ['x1'], ['x2'], 'x act', 'x actsig')
         assert node.get_bsig() == 'xxx'
 
     def test_set_csig(self):
@@ -372,11 +388,11 @@ class NodeTestCase(unittest.TestCase):
         node.set_csig('zzz')
         assert node.get_csig() == 'zzz'
 
-    def test_store_bsig(self):
-        """Test calling the method to store a build signature
+    def test_store_binfo(self):
+        """Test calling the method to store build information
         """
         node = SCons.Node.Node()
-        node.store_bsig()
+        node.store_binfo()
 
     def test_store_csig(self):
         """Test calling the method to store a content signature
@@ -907,7 +923,7 @@ class NodeTestCase(unittest.TestCase):
         n = SCons.Node.Node()
 
         n.set_state(3)
-        n.set_bsig('bsig')
+        n.set_binfo('bbb', ['b1'], ['b2'], 'b act', 'b actsig')
         n.set_csig('csig')
         n.includes = 'testincludes'
         n.found_include = {'testkey':'testvalue'}
@@ -917,6 +933,10 @@ class NodeTestCase(unittest.TestCase):
 
         assert n.get_state() is None, n.get_state()
         assert not hasattr(n, 'bsig'), n.bsig
+        assert not hasattr(n, 'bkids'), n.bkids
+        assert not hasattr(n, 'bkidsigs'), n.bkidsigs
+        assert not hasattr(n, 'bact'), n.bact
+        assert not hasattr(n, 'bactsig'), n.bactsig
         assert not hasattr(n, 'csig'), n.csig
         assert n.includes is None, n.includes
         assert n.found_includes == {}, n.found_includes
index 861b5540f393339e492adc3a96fa82d3f406e724..a9581e4065e921d34fe97cf4b1138a5d1802fcc8 100644 (file)
@@ -205,7 +205,7 @@ class Node:
 
     def built(self):
         """Called just after this node is sucessfully built."""
-        self.store_bsig()
+        self.store_binfo()
 
         # Clear out the implicit dependency caches:
         # XXX this really should somehow be made more general and put
@@ -217,7 +217,7 @@ class Node:
             def get_parents(node, parent): return node.get_parents()
             def clear_cache(node, parent):
                 node.implicit = None
-                node.del_bsig()
+                node.del_binfo()
             w = Walker(self, get_parents, ignore_cycle, clear_cache)
             while w.next(): pass
 
@@ -241,7 +241,7 @@ class Node:
         builds).
         """
         self.set_state(None)
-        self.del_bsig()
+        self.del_binfo()
         self.del_csig()
         try:
             delattr(self, '_calculated_sig')
@@ -402,7 +402,7 @@ class Node:
                     self.implicit = []
                     self.implicit_dict = {}
                     self._children_reset()
-                    self.del_bsig()
+                    self.del_binfo()
 
         build_env = self.get_build_env()
 
@@ -495,22 +495,47 @@ class Node:
         """Set the node's build signature (based on the signatures
         of its dependency files and build information)."""
         self.bsig = bsig
+
+    def get_binfo(self):
+        """Get the node's build signature (based on the signatures
+        of its dependency files and build information)."""
+        result = []
+        for attr in ['bsig', 'bkids', 'bkidsigs', 'bact', 'bactsig']:
+            try:
+                r = getattr(self, attr)
+            except AttributeError:
+                r = None
+            result.append(r)
+        return tuple(result)
+
+    def set_binfo(self, bsig, bkids, bkidsigs, bact, bactsig):
+        """Set the node's build signature (based on the signatures
+        of its dependency files and build information)."""
+        self.bsig = bsig
+        self.bkids = bkids
+        self.bkidsigs = bkidsigs
+        self.bact = bact
+        self.bactsig = bactsig
         try:
             delattr(self, '_tempbsig')
         except AttributeError:
             pass
 
-    def store_bsig(self):
+    def store_binfo(self):
         """Make the build signature permanent (that is, store it in the
         .sconsign file or equivalent)."""
         pass
 
-    def del_bsig(self):
+    def get_stored_binfo(self):
+        return (None, None, None, None, None)
+
+    def del_binfo(self):
         """Delete the bsig from this node."""
-        try:
-            delattr(self, 'bsig')
-        except AttributeError:
-            pass
+        for attr in ['bsig', 'bkids', 'bkidsigs', 'bact', 'bactsig']:
+            try:
+                delattr(self, attr)
+            except AttributeError:
+                pass
 
     def get_csig(self):
         """Get the signature of the node's content."""
index cac2fcfadd945cf4f07592b0bb61e5e046a09bf8..245fea973c2798698ac6d70d1697a26736bd360f 100644 (file)
@@ -69,6 +69,18 @@ import SCons.Taskmaster
 import SCons.Util
 import SCons.Warnings
 
+#
+import __builtin__
+try:
+    __builtin__.zip
+except AttributeError:
+    def zip(l1, l2):
+        result = []
+        for i in xrange(len(l1)):
+           result.append((l1[i], l2[i]))
+        return result
+    __builtin__.zip = zip
+
 #
 display = SCons.Util.display
 progress_display = SCons.Util.DisplayEngine()
@@ -175,6 +187,58 @@ class BuildTask(SCons.Taskmaster.Task):
 
         self.do_failed(status)
 
+    def make_ready(self):
+        """Make a task ready for execution"""
+        SCons.Taskmaster.Task.make_ready(self)
+        if self.out_of_date and print_explanations:
+            node = self.out_of_date[0]
+            if not node.exists():
+                sys.stdout.write("scons: building `%s' because it doesn't exist\n" % node)
+                return
+
+            oldbsig, oldkids, oldsigs, oldact, oldactsig = node.get_stored_binfo()
+            if oldkids is None:
+                return
+
+            def dictify(kids, sigs):
+                result = {}
+                for k, s in zip(kids, sigs):
+                    result[k] = s
+                return result
+
+            osig = dictify(oldkids, oldsigs)
+
+            newkids, newsigs = map(str, node.bkids), node.bkidsigs
+            nsig = dictify(newkids, newsigs)
+
+            lines = map(lambda x: "`%s' is no longer a dependency\n" % x,
+                        filter(lambda x, nk=newkids: not x in nk, oldkids))
+
+            for k in newkids:
+                if not k in oldkids:
+                    lines.append("`%s' is a new dependency\n" % k)
+                elif osig[k] != nsig[k]:
+                    lines.append("`%s' changed\n" % k)
+
+            if len(lines) == 0:
+                newact, newactsig = node.bact, node.bactsig
+                if oldact != newact:
+                    lines.append("the build action changed:\n" +
+                                 "%sold: %s\n" % (' '*15, oldact) +
+                                 "%snew: %s\n" % (' '*15, newact))
+
+            if len(lines) == 0:
+                lines.append("the dependency order changed:\n" +
+                             "%sold: %s\n" % (' '*15, oldkids) +
+                             "%snew: %s\n" % (' '*15, newkids))
+
+            preamble = "scons: rebuilding `%s' because" % node
+            if len(lines) == 1:
+                sys.stdout.write("%s %s"  % (preamble, lines[0]))
+            else:
+                lines = ["%s:\n" % preamble] + lines
+                sys.stdout.write(string.join(lines, ' '*11))
+
 class CleanTask(SCons.Taskmaster.Task):
     """An SCons clean task."""
     def show(self):
@@ -225,6 +289,7 @@ class QuestionTask(SCons.Taskmaster.Task):
 keep_going_on_error = 0
 print_count = 0
 print_dtree = 0
+print_explanations = 0
 print_includes = 0
 print_objects = 0
 print_time = 0
@@ -389,7 +454,8 @@ def _SConstruct_exists(dirname=''):
 
 def _set_globals(options):
     global repositories, keep_going_on_error, ignore_errors
-    global print_count, print_dtree, print_includes
+    global print_count, print_dtree
+    global print_explanations, print_includes
     global print_objects, print_time, print_tree
     global memory_outf, memory_stats
 
@@ -402,6 +468,8 @@ def _set_globals(options):
                 print_count = 1
             elif options.debug == "dtree":
                 print_dtree = 1
+            elif options.debug == "explain":
+                print_explanations = 1
             elif options.debug == "includes":
                 print_includes = 1
             elif options.debug == "memory":
@@ -493,7 +561,8 @@ class OptParser(OptionParser):
                         help="Search up directory tree for SConstruct,       "
                              "build all Default() targets.")
 
-        debug_options = ["count", "dtree", "includes", "memory", "objects",
+        debug_options = ["count", "dtree", "explain",
+                         "includes", "memory", "objects",
                          "pdb", "presub", "time", "tree"]
 
         def opt_debug(option, opt, value, parser, debug_options=debug_options):
index f297464d876d55a74deb44e71b2f252ebeb62054..98465fffe9983a1b07f7ad65f9e721b98b6fb45f 100644 (file)
@@ -106,8 +106,12 @@ class DummyNode:
         else:
             return calc.csig(self)
 
-    def set_bsig(self, bsig):
+    def set_binfo(self, bsig, bkids, bkidsigs, bact, bactsig):
         self.bsig = bsig
+        self.bkids = bkids
+        self.bkidsigs = bkidsigs
+        self.bact = bact
+        self.bactsig = bactsig
 
     def get_bsig(self):
         return self.bsig
@@ -129,12 +133,6 @@ class DummyNode:
 
     def get_stored_implicit(self):
         return None
-
-    def store_csig(self):
-        pass
-
-    def store_bsig(self):
-        pass
     
     def store_timestamp(self):
         pass
@@ -327,8 +325,12 @@ class CalcTestCase(unittest.TestCase):
                 return 1
             def get_bsig(self):
                 return self.bsig
-            def set_bsig(self, bsig):
+            def set_binfo(self, bsig, bkids, bkidsig, bact, bactsig):
                 self.bsig = bsig
+                self.bkids = bkids
+                self.bkidsigs = bkidsigs
+                self.bact = bact
+                self.bactsig = bactsig
             def get_csig(self):
                 return self.csig
             def set_csig(self, csig):
@@ -414,7 +416,7 @@ class _SConsignTestCase(unittest.TestCase):
             path = 'not_a_valid_path'
 
         f = SCons.Sig._SConsign()
-        f.set_bsig('foo', 1)
+        f.set_binfo('foo', 1, ['f1'], ['f2'], 'foo act', 'foo actsig')
         assert f.get('foo') == (None, 1, None)
         f.set_csig('foo', 2)
         assert f.get('foo') == (None, 1, 2)
@@ -425,7 +427,7 @@ class _SConsignTestCase(unittest.TestCase):
         assert f.get_implicit('foo') == ['bar']
 
         f = SCons.Sig._SConsign(DummyModule())
-        f.set_bsig('foo', 1)
+        f.set_binfo('foo', 1, ['f1'], ['f2'], 'foo act', 'foo actsig')
         assert f.get('foo') == (None, 1, None)
         f.set_csig('foo', 2)
         assert f.get('foo') == (None, 1, 2)
@@ -446,20 +448,20 @@ class SConsignDBTestCase(unittest.TestCase):
         try:
             d1 = SCons.Sig.SConsignDB(DummyNode('dir1'))
             d1.set_timestamp('foo', 1)
-            d1.set_bsig('foo', 2)
+            d1.set_binfo('foo', 2, ['f1'], ['f2'], 'foo act', 'foo actsig')
             d1.set_csig('foo', 3)
             d1.set_timestamp('bar', 4)
-            d1.set_bsig('bar', 5)
+            d1.set_binfo('bar', 5, ['b1'], ['b2'], 'bar act', 'bar actsig')
             d1.set_csig('bar', 6)
             assert d1.get('foo') == (1, 2, 3)
             assert d1.get('bar') == (4, 5, 6)
 
             d2 = SCons.Sig.SConsignDB(DummyNode('dir1'))
             d2.set_timestamp('foo', 7)
-            d2.set_bsig('foo', 8)
+            d2.set_binfo('foo', 8, ['f3'], ['f4'], 'foo act', 'foo actsig')
             d2.set_csig('foo', 9)
             d2.set_timestamp('bar', 10)
-            d2.set_bsig('bar', 11)
+            d2.set_binfo('bar', 11, ['b3'], ['b4'], 'bar act', 'bar actsig')
             d2.set_csig('bar', 12)
             assert d2.get('foo') == (7, 8, 9)
             assert d2.get('bar') == (10, 11, 12)
@@ -480,7 +482,7 @@ class SConsignDirFileTestCase(unittest.TestCase):
             path = 'not_a_valid_path'
 
         f = SCons.Sig.SConsignDirFile(DummyNode(), DummyModule())
-        f.set_bsig('foo', 1)
+        f.set_binfo('foo', 1, ['f1'], ['f2'], 'foo act', 'foo actsig')
         assert f.get('foo') == (None, 1, None)
         f.set_csig('foo', 2)
         assert f.get('foo') == (None, 1, 2)
index a2ebd5cc32b4cff27f5d1fcb4ee3c915f32d9d0b..d70319467a573496ca2e016233c8b83fad6f4cb2 100644 (file)
@@ -73,6 +73,10 @@ class SConsignEntry:
     bsig = None
     csig = None
     implicit = None
+    bkids = []
+    bkidsigs = []
+    bact = None
+    bactsig = None
 
 class _SConsign:
     """
@@ -139,9 +143,9 @@ class _SConsign:
         entry.csig = csig
         self.set_entry(filename, entry)
 
-    def set_bsig(self, filename, bsig):
+    def set_binfo(self, filename, bsig, bkids, bkidsigs, bact, bactsig):
         """
-        Set the csig .sconsign entry for a file
+        Set the build info .sconsign entry for a file
 
         filename - the filename whose signature will be set
         bsig - the file's built signature
@@ -149,11 +153,15 @@ class _SConsign:
 
         entry = self.get_entry(filename)
         entry.bsig = bsig
+        entry.bkids = bkids
+        entry.bkidsigs = bkidsigs
+        entry.bact = bact
+        entry.bactsig = bactsig
         self.set_entry(filename, entry)
 
     def set_timestamp(self, filename, timestamp):
         """
-        Set the csig .sconsign entry for a file
+        Set the timestamp .sconsign entry for a file
 
         filename - the filename whose signature will be set
         timestamp - the file's timestamp
@@ -171,12 +179,17 @@ class _SConsign:
     def set_implicit(self, filename, implicit):
         """Cache the implicit dependencies for 'filename'."""
         entry = self.get_entry(filename)
-        if SCons.Util.is_String(implicit):
+        if not SCons.Util.is_List(implicit):
             implicit = [implicit]
         implicit = map(str, implicit)
         entry.implicit = implicit
         self.set_entry(filename, entry)
 
+    def get_binfo(self, filename):
+        """Fetch the cached implicit dependencies for 'filename'"""
+        entry = self.get_entry(filename)
+        return entry.bsig, entry.bkids, entry.bkidsigs, entry.bact, entry.bactsig
+
 class SConsignDB(_SConsign):
     """
     A _SConsign subclass that reads and writes signature information
@@ -360,20 +373,28 @@ class Calculator:
             return bsig
 
         children = node.children()
+        bkids = map(str, children)
 
-        # double check bsig, because the call to childre() above may
+        # double check bsig, because the call to children() above may
         # have set it:
         bsig = cache.get_bsig()
         if bsig is not None:
             return bsig
 
         sigs = map(lambda n, c=self: n.calc_signature(c), children)
+
         if node.has_builder():
-            sigs.append(self.module.signature(node.get_executor()))
+            executor = node.get_executor()
+            bact = str(executor)
+            bactsig = self.module.signature(executor)
+            sigs.append(bactsig)
+        else:
+            bact = ""
+            bactsig = ""
 
-        bsig = self.module.collect(filter(lambda x: not x is None, sigs))
+        bsig = self.module.collect(filter(None, sigs))
 
-        cache.set_bsig(bsig)
+        cache.set_binfo(bsig, bkids, sigs, bact, bactsig)
 
         # don't store the bsig here, because it isn't accurate until
         # the node is actually built.
diff --git a/test/explain.py b/test/explain.py
new file mode 100644 (file)
index 0000000..d46d410
--- /dev/null
@@ -0,0 +1,295 @@
+#!/usr/bin/env python
+#
+# __COPYRIGHT__
+#
+# 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__"
+
+"""
+Test the --debug=explain option.
+"""
+
+import sys
+import TestSCons
+
+python = TestSCons.python
+
+test = TestSCons.TestSCons()
+
+test.subdir('src')
+
+test.write(['src', 'cat.py'], r"""
+import sys
+
+def process(outfp, infp):
+    for line in infp.readlines():
+        if line[:8] == 'include ':
+            file = line[8:-1]
+            try:
+                fp = open(file, 'rb')
+            except IOError:
+                import os
+                print "os.getcwd() =", os.getcwd()
+                raise
+            process(outfp, fp)
+        else:
+            outfp.write(line)
+
+outfp = open(sys.argv[1], 'wb')
+for f in sys.argv[2:]:
+    if f != '-':
+        process(outfp, open(f, 'rb'))
+
+sys.exit(0)
+""")
+
+test.write(['src', 'SConstruct'], """
+import re
+
+include_re = re.compile(r'^include\s+(\S+)$', re.M)
+
+def kfile_scan(node, env, target, arg):
+    contents = node.get_contents()
+    includes = include_re.findall(contents)
+    return includes
+
+kscan = Scanner(name = 'kfile',
+                function = kfile_scan,
+                argument = None,
+                skeys = ['.k'])
+
+cat = Builder(action = r"%s cat.py $TARGET $SOURCES")
+
+env = Environment()
+env.Append(BUILDERS = {'Cat':cat},
+           SCANNERS = kscan)
+
+Export("env")
+SConscript('SConscript')
+env.Install('../inc', 'aaa')
+env.InstallAs('../inc/bbb.k', 'bbb.k')
+env.Install('../inc', 'ddd')
+env.InstallAs('../inc/eee', 'eee.in')
+""" % (python,))
+
+test.write(['src', 'SConscript'], """\
+Import("env")
+env.Cat('file1', 'file1.in')
+env.Cat('file2', 'file2.k')
+env.Cat('file3', ['xxx', 'yyy', 'zzz'])
+env.Command('file4', 'file4.in', r"%s cat.py $TARGET - $SOURCES")
+env.Cat('file5', 'file5.k')
+""" % (python,))
+
+test.write(['src', 'aaa'], "aaa 1\n")
+test.write(['src', 'bbb.k'], """\
+bbb.k 1
+include ccc
+include ../inc/ddd
+include ../inc/eee
+""")
+test.write(['src', 'ccc'], "ccc 1\n")
+test.write(['src', 'ddd'], "ddd 1\n")
+test.write(['src', 'eee.in'], "eee.in 1\n")
+
+test.write(['src', 'file1.in'], "file1.in 1\n")
+
+test.write(['src', 'file2.k'], """\
+file2.k 1 line 1
+include xxx
+include yyy
+file2.k 1 line 4
+""")
+
+test.write(['src', 'file4.in'], "file4.in 1\n")
+
+test.write(['src', 'xxx'], "xxx 1\n")
+test.write(['src', 'yyy'], "yyy 1\n")
+test.write(['src', 'zzz'], "zzz 1\n")
+
+test.write(['src', 'file5.k'], """\
+file5.k 1 line 1
+include ../inc/aaa
+include ../inc/bbb.k
+file5.k 1 line 4
+""")
+
+args = '--debug=explain .'
+
+#
+test.run(chdir='src', arguments=args, stdout=test.wrap_stdout("""\
+scons: building `file1' because it doesn't exist
+%s cat.py file1 file1.in
+scons: building `file2' because it doesn't exist
+%s cat.py file2 file2.k
+scons: building `file3' because it doesn't exist
+%s cat.py file3 xxx yyy zzz
+scons: building `file4' because it doesn't exist
+%s cat.py file4 - file4.in
+scons: building `%s' because it doesn't exist
+Install file: "aaa" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "ddd" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "eee.in" as "%s"
+scons: building `%s' because it doesn't exist
+Install file: "bbb.k" as "%s"
+scons: building `file5' because it doesn't exist
+%s cat.py file5 file5.k
+""" % (python,
+       python,
+       python,
+       python,
+       test.workpath('inc', 'aaa'),
+       test.workpath('inc', 'aaa'),
+       test.workpath('inc', 'ddd'),
+       test.workpath('inc', 'ddd'),
+       test.workpath('inc', 'eee'),
+       test.workpath('inc', 'eee'),
+       test.workpath('inc', 'bbb.k'),
+       test.workpath('inc', 'bbb.k'),
+       python,)))
+
+test.must_match(['src', 'file1'], "file1.in 1\n")
+test.must_match(['src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 1
+file2.k 1 line 4
+""")
+test.must_match(['src', 'file3'], "xxx 1\nyyy 1\nzzz 1\n")
+test.must_match(['src', 'file4'], "file4.in 1\n")
+test.must_match(['src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 1
+ccc 1
+ddd 1
+eee.in 1
+file5.k 1 line 4
+""")
+
+#
+test.write(['src', 'file1.in'], "file1.in 2\n")
+test.write(['src', 'yyy'], "yyy 2\n")
+test.write(['src', 'zzz'], "zzz 2\n")
+test.write(['src', 'bbb.k'], "bbb.k 2\ninclude ccc\n")
+
+test.run(chdir='src', arguments=args, stdout=test.wrap_stdout("""\
+scons: rebuilding `file1' because `file1.in' changed
+%s cat.py file1 file1.in
+scons: rebuilding `file2' because `yyy' changed
+%s cat.py file2 file2.k
+scons: rebuilding `file3' because:
+           `yyy' changed
+           `zzz' changed
+%s cat.py file3 xxx yyy zzz
+scons: rebuilding `%s' because:
+           `%s' is no longer a dependency
+           `%s' is no longer a dependency
+           `bbb.k' changed
+Install file: "bbb.k" as "%s"
+scons: rebuilding `file5' because `%s' changed
+%s cat.py file5 file5.k
+""" % (python,
+       python,
+       python,
+       test.workpath('inc', 'bbb.k'),
+       test.workpath('inc', 'ddd'),
+       test.workpath('inc', 'eee'),
+       test.workpath('inc', 'bbb.k'),
+       test.workpath('inc', 'bbb.k'),
+       python)))
+
+test.must_match(['src', 'file1'], "file1.in 2\n")
+test.must_match(['src', 'file2'], """\
+file2.k 1 line 1
+xxx 1
+yyy 2
+file2.k 1 line 4
+""")
+test.must_match(['src', 'file3'], "xxx 1\nyyy 2\nzzz 2\n")
+test.must_match(['src', 'file5'], """\
+file5.k 1 line 1
+aaa 1
+bbb.k 2
+ccc 1
+file5.k 1 line 4
+""")
+
+#
+test.write(['src', 'SConscript'], """\
+Import("env")
+env.Cat('file3', ['xxx', 'yyy'])
+""")
+
+test.run(chdir='src', arguments=args, stdout=test.wrap_stdout("""\
+scons: rebuilding `file3' because `zzz' is no longer a dependency
+%s cat.py file3 xxx yyy
+""" % (python,)))
+
+test.must_match(['src', 'file3'], "xxx 1\nyyy 2\n")
+
+#
+test.write(['src', 'SConscript'], """\
+Import("env")
+env.Cat('file3', ['xxx', 'yyy', 'zzz'])
+""")
+
+test.run(chdir='src', arguments=args, stdout=test.wrap_stdout("""\
+scons: rebuilding `file3' because `zzz' is a new dependency
+%s cat.py file3 xxx yyy zzz
+""" % (python,)))
+
+test.must_match(['src', 'file3'], "xxx 1\nyyy 2\nzzz 2\n")
+
+#
+test.write(['src', 'SConscript'], """\
+Import("env")
+env.Cat('file3', ['zzz', 'yyy', 'xxx'])
+""")
+
+test.run(chdir='src', arguments=args, stdout=test.wrap_stdout("""\
+scons: rebuilding `file3' because the dependency order changed:
+               old: ['xxx', 'yyy', 'zzz']
+               new: ['zzz', 'yyy', 'xxx']
+%s cat.py file3 zzz yyy xxx
+""" % (python,)))
+
+test.must_match(['src', 'file3'], "zzz 2\nyyy 2\nxxx 1\n")
+
+#
+test.write(['src', 'SConscript'], """\
+Import("env")
+env.Command('file4', 'file4.in', r"%s cat.py $TARGET $SOURCES")
+""" % (python,))
+
+test.run(chdir='src',arguments=args, stdout=test.wrap_stdout("""\
+scons: rebuilding `file4' because the build action changed:
+               old: %s cat.py $TARGET - $SOURCES
+               new: %s cat.py $TARGET $SOURCES
+%s cat.py file4 file4.in
+""" % (python, python, python)))
+
+test.must_match(['src', 'file4'], "file4.in 1\n")
+
+test.pass_test()