import os
import SCons.Node.FS
+import string
import types
node = self.node_factory(target)
node.builder_set(self)
node.env_set(self)
- node.sources = source # XXX REACHING INTO ANOTHER OBJECT
+
+ # XXX REACHING INTO ANOTHER OBJECT (this is only temporary):
+ assert type(source) is type("")
+ node.sources = source
+ node.derived = 1
+ sources = string.split(source, " ")
+ sources = filter(lambda x: x, sources)
+ source_nodes = []
+ for source in sources:
+ source_node = self.node_factory(source)
+ source_node.derived = 0
+ source_node.source_nodes = []
+ source_nodes.append(source_node)
+ node.source_nodes = source_nodes
+
return node
def execute(self, **kw):
def root(self):
return self.parent.root()
+ def get_contents(self):
+ return open(self.path, "r").read()
+
+ def get_timestamp(self):
+ return os.path.getmtime(self.path)
+
+ def exists(self):
+ return os.path.exists(self.path)
default_fs = FS()
def env_set(self, env):
self.env = env
+
+ def has_signature(self):
+ return hasattr(self, "signature")
+
+ def set_signature(self, signature):
+ self.signature = signature
+
+ def get_signature(self):
+ return self.signature
+
import md5
import string
-
+def current(obj, sig):
+ """Return whether a given object is up-to-date with the
+ specified signature.
+ """
+ return obj.get_signature() == sig
def hexdigest(s):
"""Return a signature as a string of hex characters.
r = r + h[(i >> 4) & 0xF] + h[i & 0xF]
return r
-
-
-def _init():
- pass # XXX
-
-def _end():
- pass # XXX
-
-def current(obj, sig):
- """Return whether a given object is up-to-date with the
- specified signature.
+def collect(signatures):
"""
- return obj.signature() == sig
-
-def set():
- pass # XXX
-
-def invalidate():
- pass # XXX
+ Collect a list of signatures into an aggregate signature.
-def collect(*objects):
- """Collect signatures from a list of objects, returning the
- aggregate signature of the list.
+ signatures - a list of signatures
+ returns - the aggregate signature
"""
- if len(objects) == 1:
- sig = objects[0].signature()
+ if len(signatures) == 1:
+ return signatures[0]
else:
- contents = string.join(map(lambda o: o.signature(), objects), ', ')
- sig = signature(contents)
-# if debug:
-# pass
- return sig
+ contents = string.join(signatures, ', ')
+ return hexdigest(md5.new(contents).digest())
-def signature(contents):
- """Generate a signature for a byte string.
+def signature(obj):
+ """Generate a signature for an object
"""
- return hexdigest(md5.new(contents).digest())
+ return hexdigest(md5.new(obj.get_contents()).digest())
-def cmdsig():
- pass # XXX
+def to_string(signature):
+ """Convert a signature to a string"""
+ return signature
-def srcsig():
- pass # XXX
+def from_string(string):
+ """Convert a string to a signature"""
+ return string
import sys
import unittest
-import SCons.Sig.MD5
+from SCons.Sig.MD5 import current, collect, signature, to_string, from_string
+
def __init__(self, value = ""):
self.value = value
- self.sig = None
-
- def signature(self):
- if not self.sig:
- self.sig = SCons.Sig.MD5.signature(self.value)
+
+ def get_signature(self):
+ if not hasattr(self, "sig"):
+ self.sig = signature(self)
return self.sig
- def current(self, sig):
- return SCons.Sig.MD5.current(self, sig)
+ def get_contents(self):
+ return self.value
class MD5TestCase(unittest.TestCase):
- def test__init(self):
- pass # XXX
-
- def test__end(self):
- pass # XXX
-
def test_current(self):
"""Test deciding if an object is up-to-date
Simple comparison of different "signature" values.
"""
- o111 = my_obj(value = '111')
- assert not o111.current(SCons.Sig.MD5.signature('110'))
- assert o111.current(SCons.Sig.MD5.signature('111'))
- assert not o111.current(SCons.Sig.MD5.signature('112'))
-
- def test_set(self):
- pass # XXX
-
- def test_invalidate(self):
- pass # XXX
+ obj = my_obj('111')
+ assert not current(obj, signature(my_obj('110')))
+ assert current(obj, signature(my_obj('111')))
+ assert not current(obj, signature(my_obj('112')))
def test_collect(self):
"""Test collecting a list of signatures into a new signature value
"""
- o1 = my_obj(value = '111')
- o2 = my_obj(value = '222')
- o3 = my_obj(value = '333')
- assert '698d51a19d8a121ce581499d7b701668' == SCons.Sig.MD5.collect(o1)
- assert '8980c988edc2c78cc43ccb718c06efd5' == SCons.Sig.MD5.collect(o1, o2)
- assert '53fd88c84ff8a285eb6e0a687e55b8c7' == SCons.Sig.MD5.collect(o1, o2, o3)
+ s = map(signature, map(my_obj, ('111', '222', '333')))
+
+ assert '698d51a19d8a121ce581499d7b701668' == collect(s[0:1])
+ assert '8980c988edc2c78cc43ccb718c06efd5' == collect(s[0:2])
+ assert '53fd88c84ff8a285eb6e0a687e55b8c7' == collect(s)
def test_signature(self):
- pass # XXX
-
- def test_cmdsig(self):
- pass # XXX
+ """Test generating a signature"""
+ o1 = my_obj(value = '111')
+ assert '698d51a19d8a121ce581499d7b701668' == signature(o1)
- def test_srcsig(self):
- pass # XXX
+ def test_to_string(self):
+ assert '698d51a19d8a121ce581499d7b701668' == to_string('698d51a19d8a121ce581499d7b701668')
+ def test_from_string(self):
+ assert '698d51a19d8a121ce581499d7b701668' == from_string('698d51a19d8a121ce581499d7b701668')
if __name__ == "__main__":
suite = unittest.makeSuite(MD5TestCase, 'test_')
--- /dev/null
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import unittest
+import TestCmd
+import SCons.Sig
+import SCons.Sig.MD5
+import SCons.Sig.TimeStamp
+import sys
+
+
+class DummyFile:
+ """A class that simulates a file for testing purposes"""
+ def __init__(self, path, contents, timestamp, derived):
+ self.path = path
+ self.contents = contents
+ self.timestamp = timestamp
+ self.derived = derived
+
+ def modify(self, contents, timestamp):
+ self.contents = contents
+ self.timestamp = timestamp
+
+class DummyNode:
+ """A node workalike for testing purposes"""
+
+ def __init__(self, file):
+ self.file = file
+ self.path = file.path
+ self.derived = file.derived
+
+ def get_contents(self):
+ # a file that doesn't exist has no contents:
+ assert self.exists()
+
+ return self.file.contents
+
+ def get_timestamp(self):
+ # a file that doesn't exist has no timestamp:
+ assert self.exists()
+
+ return self.file.timestamp
+
+ def exists(self):
+ return not self.file.contents is None
+
+ def has_signature(self):
+ return hasattr(self, "sig")
+
+ def set_signature(self, sig):
+ self.sig = sig
+
+ def get_signature(self):
+ return self.sig
+
+
+def create_files(test):
+ args = [(test.workpath('f1.c'), 'blah blah', 111, 0), #0
+ (test.workpath('f1'), None, 0, 1), #1
+ (test.workpath('d1/f1.c'), 'blah blah', 111, 0), #2
+ (test.workpath('d1/f1.h'), 'blah blah', 111, 0), #3
+ (test.workpath('d1/f2.c'), 'blah blah', 111, 0), #4
+ (test.workpath('d1/f3.h'), 'blah blah', 111, 0), #5
+ (test.workpath('d1/f4.h'), 'blah blah', 111, 0), #6
+ (test.workpath('d1/f1'), None, 0, 1), #7
+ (test.workpath('d1/test.c'), 'blah blah', 111, 0),#8
+ (test.workpath('d1/test.o'), None, 0, 1), #9
+ (test.workpath('d1/test'), None, 0, 1)] #10
+
+ files = map(lambda x: apply(DummyFile, x), args)
+
+ return files
+
+def create_nodes(files):
+ nodes = map(DummyNode, files)
+
+ nodes[0].source_nodes = []
+ nodes[1].source_nodes = [nodes[0]]
+ nodes[2].source_nodes = [nodes[3]]
+ nodes[3].source_nodes = []
+ nodes[4].source_nodes = [nodes[5]]
+ nodes[5].source_nodes = [nodes[6]]
+ nodes[6].source_nodes = [nodes[5]]
+ nodes[7].source_nodes = [nodes[2], nodes[4]]
+ nodes[8].source_nodes = []
+ nodes[9].source_nodes = [nodes[8]]
+ nodes[10].source_nodes = [nodes[9]]
+
+ return nodes
+
+
+class SigTestBase:
+
+ def runTest(self):
+
+ test = TestCmd.TestCmd(workdir = '')
+ test.subdir('d1')
+
+ self.files = create_files(test)
+ self.test_initial()
+ self.test_built()
+ self.test_modify()
+ self.test_delete()
+
+ def test_initial(self):
+
+ nodes = create_nodes(self.files)
+ calc = SCons.Sig.Calculator(self.module)
+
+ for node in nodes:
+ self.failUnless(not calc.current(node), "none of the nodes should be current")
+
+ # simulate a build:
+ self.files[1].modify('built', 222)
+ self.files[7].modify('built', 222)
+ self.files[9].modify('built', 222)
+ self.files[10].modify('built', 222)
+
+ calc.write(nodes)
+
+ def test_built(self):
+
+ nodes = create_nodes(self.files)
+
+ calc = SCons.Sig.Calculator(self.module)
+
+ for node in nodes:
+ self.failUnless(calc.current(node), "all of the nodes should be current")
+
+ calc.write(nodes)
+
+ def test_modify(self):
+
+ nodes = create_nodes(self.files)
+
+ #simulate a modification of some files
+ self.files[0].modify('blah blah blah', 333)
+ self.files[3].modify('blah blah blah', 333)
+ self.files[6].modify('blah blah blah', 333)
+ self.files[8].modify('blah blah blah', 333)
+
+ calc = SCons.Sig.Calculator(self.module)
+
+ self.failUnless(not calc.current(nodes[0]), "modified directly")
+ self.failUnless(not calc.current(nodes[1]), "direct source modified")
+ self.failUnless(calc.current(nodes[2]))
+ self.failUnless(not calc.current(nodes[3]), "modified directly")
+ self.failUnless(calc.current(nodes[4]))
+ self.failUnless(calc.current(nodes[5]))
+ self.failUnless(not calc.current(nodes[6]), "modified directly")
+ self.failUnless(not calc.current(nodes[7]), "indirect source modified")
+ self.failUnless(not calc.current(nodes[8]), "modified directory")
+ self.failUnless(not calc.current(nodes[9]), "direct source modified")
+ self.failUnless(not calc.current(nodes[10]), "indirect source modified")
+
+ calc.write(nodes)
+
+ def test_delete(self):
+
+ nodes = create_nodes(self.files)
+
+ #simulate the deletion of some files
+ self.files[1].modify(None, 0)
+ self.files[7].modify(None, 0)
+ self.files[9].modify(None, 0)
+
+ calc = SCons.Sig.Calculator(self.module)
+
+ self.failUnless(calc.current(nodes[0]))
+ self.failUnless(not calc.current(nodes[1]), "deleted")
+ self.failUnless(calc.current(nodes[2]))
+ self.failUnless(calc.current(nodes[3]))
+ self.failUnless(calc.current(nodes[4]))
+ self.failUnless(calc.current(nodes[5]))
+ self.failUnless(calc.current(nodes[6]))
+ self.failUnless(not calc.current(nodes[7]), "deleted")
+ self.failUnless(calc.current(nodes[8]))
+ self.failUnless(not calc.current(nodes[9]), "deleted")
+ self.failUnless(calc.current(nodes[10]),
+ "current even though it's source was deleted")
+
+ calc.write(nodes)
+
+class MD5TestCase(unittest.TestCase, SigTestBase):
+ """Test MD5 signatures"""
+
+ module = SCons.Sig.MD5
+
+class TimeStampTestCase(unittest.TestCase, SigTestBase):
+ """Test timestamp signatures"""
+
+ module = SCons.Sig.TimeStamp
+
+def suite():
+ suite = unittest.TestSuite()
+ suite.addTest(MD5TestCase())
+ suite.addTest(TimeStampTestCase())
+ return suite
+
+if __name__ == "__main__":
+ runner = unittest.TextTestRunner()
+ result = runner.run(suite())
+ if not result.wasSuccessful():
+ sys.exit(1)
+
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
-def _init():
- pass # XXX
-
-def _end():
- pass # XXX
-
def current(obj, sig):
- """Return whether the object's timestamp is up-to-date.
+ """Return whether the objects timestamp is up-to-date.
"""
- return obj.signature() >= sig
+ return obj.get_signature() >= sig
-def set():
- pass # XXX
-
-def invalidate():
- pass # XXX
+def collect(signatures):
+ """
+ Collect a list of timestamps, returning
+ the most-recent timestamp from the list
-def collect(*objects):
- """Collect timestamps from a list of objects, returning
- the most-recent timestamp from the list.
+ signatures - a list of timestamps
+ returns - the most recent timestamp
"""
- r = 0
- for obj in objects:
- s = obj.signature()
- if s > r:
- r = s
- return r
-
-def signature(contents):
+
+ if len(signatures) == 0:
+ return 0
+ elif len(signatures) == 1:
+ return signatures[0]
+ else:
+ return max(signatures)
+
+def signature(obj):
"""Generate a timestamp.
"""
- pass # XXX
-# return md5.new(contents).hexdigest() # 2.0
- return hexdigest(md5.new(contents).digest())
+ return obj.get_timestamp()
+
+def to_string(signature):
+ """Convert a timestamp to a string"""
+ return str(signature)
+
+def from_string(string):
+ """Convert a string to a timestamp"""
+ return int(string)
-def cmdsig():
- pass # XXX
-def srcsig():
- pass # XXX
import sys
import unittest
-import SCons.Sig.TimeStamp
+from SCons.Sig.TimeStamp import current, collect, signature, to_string, from_string
requirements of the TimeStamp class.
"""
- def __init__(self, value = ""):
+ def __init__(self, value = 0):
self.value = value
- def signature(self):
+ def get_signature(self):
return self.value
+ def get_timestamp(self):
+ return self.value
class TimeStampTestCase(unittest.TestCase):
- def test__init(self):
- pass # XXX
-
- def test__init(self):
- pass # XXX
-
- def test__end(self):
- pass # XXX
-
def test_current(self):
"""Test deciding if an object is up-to-date
Simple comparison of different timestamp values.
"""
o1 = my_obj(value = 111)
- assert SCons.Sig.TimeStamp.current(o1, 110)
- assert SCons.Sig.TimeStamp.current(o1, 111)
- assert not SCons.Sig.TimeStamp.current(o1, 112)
-
- def test_set(self):
- pass # XXX
-
- def test_invalidate(self):
- pass # XXX
+ assert current(o1, 110)
+ assert current(o1, 111)
+ assert not current(o1, 112)
def test_collect(self):
"""Test collecting a list of signatures into a new signature value
into a new timestamp value.
"""
- o1 = my_obj(value = 111)
- o2 = my_obj(value = 222)
- o3 = my_obj(value = 333)
- assert 111 == SCons.Sig.TimeStamp.collect(o1)
- assert 222 == SCons.Sig.TimeStamp.collect(o1, o2)
- assert 333 == SCons.Sig.TimeStamp.collect(o1, o2, o3)
+
+ assert 111 == collect((111,))
+ assert 222 == collect((111, 222))
+ assert 333 == collect((333, 222, 111))
def test_signature(self):
- pass # XXX
+ """Test generating a signature"""
+ o1 = my_obj(value = 111)
+ assert 111 == signature(o1)
- def test_cmdsig(self):
- pass # XXX
+ def test_to_string(self):
+ assert '111' == to_string(111)
- def test_srcsig(self):
- pass # XXX
+ def test_from_string(self):
+ assert 111 == from_string('111')
if __name__ == "__main__":
"""SCons.Sig
-The Signature package for the SCons software construction utility.
+The Signature package for the scons software construction utility.
"""
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+import os.path
+import string
+
+class SConsignFile:
+ """
+ Encapsulates reading and writing a .sconsign file.
+ """
+
+ def __init__(self, dir, module):
+ """
+ dir - the directory for the file
+ module - the signature module being used
+ """
+
+ self.path = os.path.join(dir, '.sconsign')
+ self.entries = {}
+
+ try:
+ file = open(self.path, 'rt')
+ except:
+ pass
+ else:
+ for line in file.readlines():
+ filename, rest = map(string.strip, string.split(line, ":"))
+ time, signature = map(string.strip, string.split(rest, " "))
+ self.entries[filename] = (int(time), module.from_string(signature))
+
+ def get(self, filename):
+ """
+ Get the signature for a file
+
+ filename - the filename whose signature will be returned
+ returns - (timestamp, signature)
+ """
+
+ try:
+ return self.entries[filename]
+ except KeyError:
+ return (0, None)
+
+ def set(self, filename, timestamp, signature, module):
+ """
+ Set the signature for a file
+
+ filename - the filename whose signature will be set
+ timestamp - the timestamp
+ signature - the signature
+ module - the signature module being used
+ """
+ self.entries[filename] = (timestamp, module.to_string(signature))
+
+ def write(self):
+ """
+ Write the .sconsign file to disk.
+ """
+
+ file = open(self.path, 'wt')
+ for item in self.entries.items():
+ file.write("%s: %d %s\n" % (item[0], item[1][0], item[1][1]))
+
+
+class Calculator:
+ """
+ Encapsulates signature calculations and .sconsign file generating
+ for the build engine.
+ """
+
+ def __init__(self, module):
+ """
+ Initialize the calculator.
+
+ module - the signature module to use for signature calculations
+ """
+ self.module = module
+ self.sig_files = {}
+
+
+ def collect(self, node, signatures):
+ """
+ Collect the signatures of the node's sources.
+
+ node - the node whose sources will be collected
+ signatures - the dictionary that the signatures will be
+ gathered into.
+ """
+ for source_node in node.source_nodes:
+ if not signatures.has_key(source_node):
+ signature = self.signature(source_node)
+ signatures[source_node] = signature
+ self.collect(source_node, signatures)
+
+ def get_sig_file(self, dir):
+ """
+ Get a sconsign file from the cache, or add it to the cache.
+
+ dir - the dir for the sconsign file
+ returns - the sconsign file
+ """
+ if self.sig_files.has_key(dir):
+ return self.sig_files[dir]
+ else:
+ self.sig_files[dir] = SConsignFile(dir, self.module)
+ return self.sig_files[dir]
+
+ def signature(self, node):
+ """
+ Get the signature for a node.
+
+ node - the node
+ returns - the signature or None if the signature could not
+ be computed.
+
+ This method also stores the signature in the node and
+ in the .sconsign file.
+ """
+
+ if node.has_signature():
+ sig = node.get_signature()
+ elif node.derived:
+ signatures = {}
+ self.collect(node, signatures)
+ signatures = filter(lambda x: not x is None, signatures.values())
+ sig = self.module.collect(signatures)
+ else:
+ if not node.exists():
+ return None
+
+ # XXX handle nodes that are not under the source root
+ sig = self.module.signature(node)
+
+ node.set_signature(sig)
+
+ dir, filename = os.path.split(node.path)
+ if node.exists():
+ timestamp = node.get_timestamp()
+ else:
+ timestamp = 0
+
+ self.get_sig_file(dir).set(filename,
+ timestamp,
+ sig,
+ self.module)
+
+ return sig
+
+ def current(self, node):
+ """
+ Check if a node is up to date.
+
+ node - the node whose signature will be checked
+
+ returns - 0 if the signature has changed since the last invocation,
+ and 1 if it hasn't
+ """
+
+ if not node.exists():
+ return 0
+
+ dir, filename = os.path.split(node.path)
+ oldtime, oldsig = self.get_sig_file(dir).get(filename)
+
+ newtime = node.get_timestamp()
+
+ if not node.derived and newtime == oldtime:
+ newsig = oldsig
+ else:
+ newsig = self.signature(node)
+
+ return newsig == oldsig
+
+ def write(self, nodes):
+ """
+ Write out all of the signature files.
+
+ nodes - the nodes whose signatures may have changed durring
+ the build
+ """
+
+ # make sure all the signatures have been calculated:
+ for node in nodes:
+ self.signature(node)
+
+ for sig_file in self.sig_files.values():
+ sig_file.write()
+
import SCons.Node.FS
import SCons.Job
from SCons.Errors import *
+import SCons.Sig
+import SCons.Sig.MD5
#
# Modules and classes that we don't use directly in this script, but
if not targets:
targets = default_targets
- taskmaster = Taskmaster(map(
- lambda x: SCons.Node.FS.default_fs.File(x),
- targets))
+ calc = SCons.Sig.Calculator(SCons.Sig.MD5)
+ nodes = map(lambda x: SCons.Node.FS.default_fs.File(x), targets)
+ nodes = filter(lambda x, calc=calc: not calc.current(x), nodes)
+
+ taskmaster = Taskmaster(nodes)
jobs = SCons.Job.Jobs(num_jobs, taskmaster)
jobs.start()
jobs.wait()
+ calc.write(nodes)
+
if __name__ == "__main__":
try:
main()
__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
import TestSCons
+import os.path
+import time
#XXX Future: be able to interpolate
#XXXtest.up_to_date(arguments = '.')
+# make sure the programs don't get rebuilt, because nothing changed:
+oldtime1 = os.path.getmtime(test.workpath('foo1'))
+oldtime2 = os.path.getmtime(test.workpath('foo2'))
+time.sleep(1) # introduce a small delay, to make the test valid
+test.run(arguments = 'foo1 foo2')
+test.fail_test(not (oldtime1 == os.path.getmtime(test.workpath('foo1'))))
+test.fail_test(not (oldtime2 == os.path.getmtime(test.workpath('foo2'))))
+
test.pass_test()
env.MyBuild(target = 'f2', source = 'f2.in')
""")
-def RunTest(args):
- test.write('f1.in', 'f1.in')
- test.write('f2.in', 'f2.in')
+def RunTest(args, extra):
+ """extra is used to make scons rebuild the output file"""
+ test.write('f1.in', 'f1.in'+extra)
+ test.write('f2.in', 'f2.in'+extra)
test.run(arguments = args)
return start2, finish1
-start2, finish1 = RunTest('-j 2 f1 f2')
+start2, finish1 = RunTest('-j 2 f1 f2', "first")
# fail if the second file was not started
# before the first one was finished
test.fail_test(not (start2 < finish1))
-start2, finish1 = RunTest('f1 f2')
+s2, f1 = RunTest('-j 2 f1 f2', "first")
+
+# re-run the test with the same input, fail if we don't
+# get back the same times, which would indicate that
+# SCons rebuilt the files even though nothing changed
+test.fail_test(start2 != s2)
+test.fail_test(finish1 != f1)
+
+start2, finish1 = RunTest('f1 f2', "second")
# fail if the second file was started
# before the first one was finished