summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteven Knight <knight@baldmt.com>2004-04-17 12:34:55 (GMT)
committerSteven Knight <knight@baldmt.com>2004-04-17 12:34:55 (GMT)
commit6afc74928b529087a998c8cb036f471f0a2497c9 (patch)
treee89b0387d2448f33c7a1dcf1cc47d12f584787fa
parentcf66aef7008b81abfb4e126e99cef44a29dbda10 (diff)
downloadSCons-6afc74928b529087a998c8cb036f471f0a2497c9.zip
SCons-6afc74928b529087a998c8cb036f471f0a2497c9.tar.gz
SCons-6afc74928b529087a998c8cb036f471f0a2497c9.tar.bz2
Add the highly anticipated --debug=explain option to provide build reasoning.
-rw-r--r--src/engine/SCons/Action.py14
-rw-r--r--src/engine/SCons/ActionTests.py73
-rw-r--r--src/engine/SCons/Executor.py10
-rw-r--r--src/engine/SCons/ExecutorTests.py10
-rw-r--r--src/engine/SCons/Node/FS.py16
-rw-r--r--src/engine/SCons/Node/FSTests.py10
-rw-r--r--src/engine/SCons/Node/NodeTests.py38
-rw-r--r--src/engine/SCons/Node/__init__.py45
-rw-r--r--src/engine/SCons/Script/__init__.py73
-rw-r--r--src/engine/SCons/Sig/SigTests.py32
-rw-r--r--src/engine/SCons/Sig/__init__.py37
-rw-r--r--test/explain.py295
12 files changed, 597 insertions, 56 deletions
diff --git a/src/engine/SCons/Action.py b/src/engine/SCons/Action.py
index f7024d3..7397873 100644
--- a/src/engine/SCons/Action.py
+++ b/src/engine/SCons/Action.py
@@ -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]
diff --git a/src/engine/SCons/ActionTests.py b/src/engine/SCons/ActionTests.py
index 5672dac..a273607 100644
--- a/src/engine/SCons/ActionTests.py
+++ b/src/engine/SCons/ActionTests.py
@@ -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
"""
diff --git a/src/engine/SCons/Executor.py b/src/engine/SCons/Executor.py
index b636f60..ac209e0 100644
--- a/src/engine/SCons/Executor.py
+++ b/src/engine/SCons/Executor.py
@@ -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
diff --git a/src/engine/SCons/ExecutorTests.py b/src/engine/SCons/ExecutorTests.py
index 9c64012..23b7719 100644
--- a/src/engine/SCons/ExecutorTests.py
+++ b/src/engine/SCons/ExecutorTests.py
@@ -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')
diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py
index bb13afc..a349f77 100644
--- a/src/engine/SCons/Node/FS.py
+++ b/src/engine/SCons/Node/FS.py
@@ -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
diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py
index b1999e0..6fbcfdc 100644
--- a/src/engine/SCons/Node/FSTests.py
+++ b/src/engine/SCons/Node/FSTests.py
@@ -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()
diff --git a/src/engine/SCons/Node/NodeTests.py b/src/engine/SCons/Node/NodeTests.py
index 63b945f..e9d779c 100644
--- a/src/engine/SCons/Node/NodeTests.py
+++ b/src/engine/SCons/Node/NodeTests.py
@@ -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
diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py
index 861b554..a9581e4 100644
--- a/src/engine/SCons/Node/__init__.py
+++ b/src/engine/SCons/Node/__init__.py
@@ -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."""
diff --git a/src/engine/SCons/Script/__init__.py b/src/engine/SCons/Script/__init__.py
index cac2fcf..245fea9 100644
--- a/src/engine/SCons/Script/__init__.py
+++ b/src/engine/SCons/Script/__init__.py
@@ -70,6 +70,18 @@ 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):
diff --git a/src/engine/SCons/Sig/SigTests.py b/src/engine/SCons/Sig/SigTests.py
index f297464..98465ff 100644
--- a/src/engine/SCons/Sig/SigTests.py
+++ b/src/engine/SCons/Sig/SigTests.py
@@ -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)
diff --git a/src/engine/SCons/Sig/__init__.py b/src/engine/SCons/Sig/__init__.py
index a2ebd5c..d703194 100644
--- a/src/engine/SCons/Sig/__init__.py
+++ b/src/engine/SCons/Sig/__init__.py
@@ -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
index 0000000..d46d410
--- /dev/null
+++ b/test/explain.py
@@ -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()