Refactor how actions get executed to eliminate a lot of redundant signature calcualat...
[scons.git] / src / engine / SCons / Sig / __init__.py
index d1e546f07d0ed75fbaa6da880aeb1971bd88a1f1..cf7a86f05be409828f2f98b1527d871da8db32a2 100644 (file)
@@ -5,7 +5,7 @@ The Signature package for the scons software construction utility.
 """
 
 #
-# Copyright (c) 2001, 2002 Steven Knight
+# __COPYRIGHT__
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -29,44 +29,86 @@ The Signature package for the scons software construction utility.
 
 __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 
+import cPickle
 import os
 import os.path
-import string
+import time
+
 import SCons.Node
+import SCons.Warnings
+
+try:
+    import MD5
+    default_module = MD5
+except ImportError:
+    import TimeStamp
+    default_module = TimeStamp
+
+default_max_drift = 2*24*60*60
 
 #XXX Get rid of the global array so this becomes re-entrant.
 sig_files = []
 
+# 1 means use build signature for derived source files
+# 0 means use content signature for derived source files
+build_signature = 1
+
 def write():
     global sig_files
     for sig_file in sig_files:
         sig_file.write()
 
+class SConsignEntry:
+
+    """Objects of this type are pickled to the .sconsign file, so it
+    should only contain simple builtin Python datatypes and no methods.
+
+    This class is used to store cache information about nodes between
+    scons runs for efficiency, and to store the build signature for
+    nodes so that scons can determine if they are out of date. """
+
+    # setup the default value for various attributes:
+    # (We make the class variables so the default values won't get pickled
+    # with the instances, which would waste a lot of space)
+    timestamp = None
+    bsig = None
+    csig = None
+    implicit = None
+
 class SConsignFile:
     """
     Encapsulates reading and writing a .sconsign file.
     """
 
-    def __init__(self, dir, module):
+    def __init__(self, dir, module=None):
         """
         dir - the directory for the file
         module - the signature module being used
         """
-        
+
         self.dir = dir
-        self.module = module
+
+        if module is None:
+            self.module = default_calc.module
+        else:
+            self.module = module
         self.sconsign = os.path.join(dir.path, '.sconsign')
         self.entries = {}
-        self.dirty = None
-                    
+        self.dirty = 0
+
         try:
-            file = open(self.sconsign, 'rt')
+            file = open(self.sconsign, 'rb')
         except:
             pass
         else:
-            for line in file.readlines():
-                filename, rest = map(string.strip, string.split(line, ":"))
-                self.entries[filename] = rest
+            try:
+                self.entries = cPickle.load(file)
+                if type(self.entries) is not type({}):
+                    self.entries = {}
+                    raise TypeError
+            except:
+                SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
+                                    "Ignoring corrupt .sconsign file: %s"%self.sconsign)
 
         global sig_files
         sig_files.append(self)
@@ -78,39 +120,75 @@ class SConsignFile:
         filename - the filename whose signature will be returned
         returns - (timestamp, bsig, csig)
         """
-        
-        try:
-            arr = map(string.strip, string.split(self.entries[filename], " "))
-        except KeyError:
-            return (None, None, None)
-        try:
-            if arr[1] == '-': bsig = None
-            else:             bsig = self.module.from_string(arr[1])
-        except IndexError:
-            bsig = None
+        entry = self.get_entry(filename)
+        return (entry.timestamp, entry.bsig, entry.csig)
+
+    def get_entry(self, filename):
+        """
+        Create an entry for the filename and return it, or if one already exists,
+        then return it.
+        """
         try:
-            if arr[2] == '-': csig = None
-            else:             csig = self.module.from_string(arr[2])
-        except IndexError:
-            csig = None
-        return (int(arr[0]), bsig, csig)
+            return self.entries[filename]
+        except:
+            return SConsignEntry()
+
+    def set_entry(self, filename, entry):
+        """
+        Set the entry.
+        """
+        self.entries[filename] = entry
+        self.dirty = 1
 
-    def set(self, filename, timestamp, bsig = None, csig = None):
+    def set_csig(self, filename, csig):
         """
-        Set the .sconsign entry for a file
+        Set the csig .sconsign entry for a file
 
         filename - the filename whose signature will be set
-        timestamp - the timestamp
-        module - the signature module being used
-        bsig - the file's build signature
         csig - the file's content signature
         """
-        if bsig is None: bsig = '-'
-        else:            bsig = self.module.to_string(bsig)
-        if csig is None: csig = ''
-        else:            csig = ' ' + self.module.to_string(csig)
-        self.entries[filename] = "%d %s%s" % (timestamp, bsig, csig)
-        self.dirty = 1
+
+        entry = self.get_entry(filename)
+        entry.csig = csig
+        self.set_entry(filename, entry)
+
+    def set_bsig(self, filename, bsig):
+        """
+        Set the csig .sconsign entry for a file
+
+        filename - the filename whose signature will be set
+        bsig - the file's built signature
+        """
+
+        entry = self.get_entry(filename)
+        entry.bsig = bsig
+        self.set_entry(filename, entry)
+
+    def set_timestamp(self, filename, timestamp):
+        """
+        Set the csig .sconsign entry for a file
+
+        filename - the filename whose signature will be set
+        timestamp - the file's timestamp
+        """
+
+        entry = self.get_entry(filename)
+        entry.timestamp = timestamp
+        self.set_entry(filename, entry)
+
+    def get_implicit(self, filename):
+        """Fetch the cached implicit dependencies for 'filename'"""
+        entry = self.get_entry(filename)
+        return entry.implicit
+
+    def set_implicit(self, filename, implicit):
+        """Cache the implicit dependencies for 'filename'."""
+        entry = self.get_entry(filename)
+        if SCons.Util.is_String(implicit):
+            implicit = [implicit]
+        implicit = map(str, implicit)
+        entry.implicit = implicit
+        self.set_entry(filename, entry)
 
     def write(self):
         """
@@ -128,47 +206,58 @@ class SConsignFile:
         if self.dirty:
             temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
             try:
-                file = open(temp, 'wt')
+                file = open(temp, 'wb')
                 fname = temp
             except:
-                file = open(self.sconsign, 'wt')
-                fname = self.sconsign
-            keys = self.entries.keys()
-            keys.sort()
-            for name in keys:
-                file.write("%s: %s\n" % (name, self.entries[name]))
-            file.close
+                try:
+                    file = open(self.sconsign, 'wb')
+                    fname = self.sconsign
+                except:
+                    return
+            cPickle.dump(self.entries, file, 1)
+            file.close()
             if fname != self.sconsign:
+                try:
+                    mode = os.stat(self.sconsign)[0]
+                    os.chmod(self.sconsign, 0666)
+                    os.unlink(self.sconsign)
+                except:
+                    pass
                 try:
                     os.rename(fname, self.sconsign)
                 except:
                     open(self.sconsign, 'wb').write(open(fname, 'rb').read())
+                    os.chmod(self.sconsign, mode)
             try:
                 os.unlink(temp)
             except:
                 pass
 
-
 class Calculator:
     """
     Encapsulates signature calculations and .sconsign file generating
     for the build engine.
     """
 
-    def __init__(self, module):
+    def __init__(self, module=default_module, max_drift=default_max_drift):
         """
         Initialize the calculator.
 
         module - the signature module to use for signature calculations
+        max_drift - the maximum system clock drift used to determine when to
+          cache content signatures. A negative value means to never cache
+          content signatures. (defaults to 2 days)
         """
         self.module = module
+        self.max_drift = max_drift
 
-    def bsig(self, node):
+    def bsig(self, node, cache=None):
         """
         Generate a node's build signature, the digested signatures
         of its dependency files and build information.
 
         node - the node whose sources will be collected
+        cache - alternate node to use for the signature cache
         returns - the build signature
 
         This no longer handles the recursive descent of the
@@ -176,83 +265,74 @@ class Calculator:
         already built and updated by someone else, if that's
         what's wanted.
         """
-        if not node.use_signature:
-            return None
-        #XXX If configured, use the content signatures from the
-        #XXX .sconsign file if the timestamps match.
 
-        bsig = node.get_bsig()
-        if not bsig is None:
+        if cache is None: cache = node
+
+        bsig = cache.get_bsig()
+        if bsig is not None:
             return bsig
 
-        # Collect the signatures for ALL the nodes that this
-        # node depends on. Just collecting the direct
-        # dependants is not good enough, because
-        # the signature of a non-derived file does
-        # not include the signatures of its psuedo-sources
-        # (e.g. the signature for a .c file does not include
-        # the signatures of the .h files that it includes).
-
-        # However, we do NOT want to walk dependencies of non-
-        # derived files, because calling get_signature() on the
-        # derived nodes will in turn call bsig() again and do that
-        # for us.  Hence:
-        def walk_non_derived(n, myself=node):
-            if not n.builder or n is myself:
-                return n.children()
-            return []
-        walker = SCons.Node.Walker(node, walk_non_derived)
-        sigs = []
-        while 1:
-            child = walker.next()
-            if child is None: break
-            if child is node: continue # skip the node itself
-            sigs.append(self.get_signature(child))
-
-        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):
+        children = node.children()
+
+        # double check bsig, because the call to childre() 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()))
+
+        bsig = self.module.collect(filter(lambda x: not x is None, sigs))
+
+        cache.set_bsig(bsig)
+
+        # don't store the bsig here, because it isn't accurate until
+        # the node is actually built.
+
+        return bsig
+
+    def csig(self, node, cache=None):
         """
         Generate a node's content signature, the digested signature
         of its content.
 
         node - the node
+        cache - alternate node to use for the signature cache
         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.
-        csig = node.get_csig()
-        if not csig is None:
+
+        if cache is None: cache = node
+
+        csig = cache.get_csig()
+        if csig is not None:
             return csig
         
-        return self.module.signature(node)
+        if self.max_drift >= 0:
+            info = node.get_prevsiginfo()
+        else:
+            info = None
 
-    def get_signature(self, node):
-        """
-        Get the appropriate signature for a node.
+        mtime = node.get_timestamp()
 
-        node - the node
-        returns - the signature or None if the signature could not
-        be computed.
+        if (info and info[0] and info[2] and info[0] == mtime):
+            # use the signature stored in the .sconsign file
+            csig = info[2]
+            # Set the csig here so it doesn't get recalculated unnecessarily
+            # and so it's set when the .sconsign file gets written
+            cache.set_csig(csig)
+        else:
+            csig = self.module.signature(node)
+            # Set the csig here so it doesn't get recalculated unnecessarily
+            # and so it's set when the .sconsign file gets written
+            cache.set_csig(csig)
 
-        This method does not store the signature in the node and
-        in the .sconsign file.
-        """
+            if self.max_drift >= 0 and (time.time() - mtime) > self.max_drift:
+                node.store_csig()
+                node.store_timestamp()
 
-        if not node.use_signature:
-            # This node type doesn't use a signature (e.g. a
-            # directory) so bail right away.
-            return None
-        elif node.builder:
-            return self.bsig(node)
-        elif not node.exists():
-            return None
-        else:
-            return self.csig(node)
+        return csig
 
     def current(self, node, newsig):
         """
@@ -264,18 +344,12 @@ class Calculator:
         returns - 1 if the file is current with the specified signature,
         0 if it isn't
         """
-
-        c = node.current()
-        if not c is None:
-            # The node itself has told us whether or not it's
-            # current without checking the signature.  The
-            # canonical uses here are a "0" return for a file
-            # that doesn't exist, or a directory.
-            return c
-
         oldtime, oldbsig, oldcsig = node.get_prevsiginfo()
 
-        if not node.builder and node.get_timestamp() == oldtime:
+        if not node.has_builder() and node.get_timestamp() == oldtime:
             return 1
-        
+
         return self.module.current(newsig, oldbsig)
+
+
+default_calc = Calculator()