http://scons.tigris.org/issues/show_bug.cgi?id=2329
[scons.git] / src / engine / SCons / SConsign.py
index a91817b3d33161546f6e821644723f6dba4166be..bd322780468bf0d37e6736599e8e898094176efb 100644 (file)
@@ -32,43 +32,97 @@ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
 import cPickle
 import os
 import os.path
-import time
 
-import SCons.Sig
-import SCons.Node
+import SCons.dblite
 import SCons.Warnings
 
+def corrupt_dblite_warning(filename):
+    SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
+                        "Ignoring corrupt .sconsign file: %s"%filename)
+
+SCons.dblite.ignore_corrupt_dbfiles = 1
+SCons.dblite.corruption_warning = corrupt_dblite_warning
+
 #XXX Get rid of the global array so this becomes re-entrant.
 sig_files = []
 
-database = None
+# Info for the database SConsign implementation (now the default):
+# "DataBase" is a dictionary that maps top-level SConstruct directories
+# to open database handles.
+# "DB_Module" is the Python database module to create the handles.
+# "DB_Name" is the base name of the database file (minus any
+# extension the underlying DB module will add).
+DataBase = {}
+DB_Module = SCons.dblite
+DB_Name = ".sconsign"
+DB_sync_list = []
+
+def Get_DataBase(dir):
+    global DataBase, DB_Module, DB_Name
+    top = dir.fs.Top
+    if not os.path.isabs(DB_Name) and top.repositories:
+        mode = "c"
+        for d in [top] + top.repositories:
+            if dir.is_under(d):
+                try:
+                    return DataBase[d], mode
+                except KeyError:
+                    path = d.entry_abspath(DB_Name)
+                    try: db = DataBase[d] = DB_Module.open(path, mode)
+                    except (IOError, OSError): pass
+                    else:
+                        if mode != "r":
+                            DB_sync_list.append(db)
+                        return db, mode
+            mode = "r"
+    try:
+        return DataBase[top], "c"
+    except KeyError:
+        db = DataBase[top] = DB_Module.open(DB_Name, "c")
+        DB_sync_list.append(db)
+        return db, "c"
+    except TypeError:
+        print "DataBase =", DataBase
+        raise
+
+def Reset():
+    """Reset global state.  Used by unit tests that end up using
+    SConsign multiple times to get a clean slate for each test."""
+    global sig_files, DB_sync_list
+    sig_files = []
+    DB_sync_list = []
+
+normcase = os.path.normcase
 
 def write():
     global sig_files
     for sig_file in sig_files:
-        sig_file.write()
-
-
-class Entry:
-
-    """Objects of this type are pickled to the .sconsign file, so it
-    should only contain simple builtin Python datatypes and no methods.
+        sig_file.write(sync=0)
+    for db in DB_sync_list:
+        try:
+            syncmethod = db.sync
+        except AttributeError:
+            pass # Not all anydbm modules have sync() methods.
+        else:
+            syncmethod()
 
-    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. """
+class SConsignEntry:
+    """
+    Wrapper class for the generic entry in a .sconsign file.
+    The Node subclass populates it with attributes as it pleases.
 
-    # 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
-    bkids = []
-    bkidsigs = []
-    bact = None
-    bactsig = None
+    XXX As coded below, we do expect a '.binfo' attribute to be added,
+    but we'll probably generalize this in the next refactorings.
+    """
+    current_version_id = 1
+    def __init__(self):
+        # Create an object attribute from the class attribute so it ends up
+        # in the pickled data in the .sconsign file.
+        _version_id = self.current_version_id
+    def convert_to_sconsign(self):
+        self.binfo.convert_to_sconsign()
+    def convert_from_sconsign(self, dir, name):
+        self.binfo.convert_from_sconsign(dir, name)
 
 class Base:
     """
@@ -79,168 +133,149 @@ class Base:
     methods for fetching and storing the individual bits of information
     that make up signature entry.
     """
-    def __init__(self, module=None):
-        """
-        module - the signature module being used
-        """
-
-        self.module = module or SCons.Sig.default_calc.module
+    def __init__(self):
         self.entries = {}
-        self.dirty = 0
-
-    # A null .sconsign entry.  We define this here so that it will
-    # be easy to keep this in sync if/whenever we change the type of
-    # information returned by the get() method, below.
-    null_siginfo = (None, None, None)
-
-    def get(self, filename):
-        """
-        Get the .sconsign entry for a file
-
-        filename - the filename whose signature will be returned
-        returns - (timestamp, bsig, csig)
-        """
-        entry = self.get_entry(filename)
-        return (entry.timestamp, entry.bsig, entry.csig)
+        self.dirty = False
+        self.to_be_merged = {}
 
     def get_entry(self, filename):
         """
-        Create an entry for the filename and return it, or if one already exists,
-        then return it.
+        Fetch the specified entry attribute.
         """
-        try:
-            return self.entries[filename]
-        except (KeyError, AttributeError):
-            return Entry()
+        return self.entries[filename]
 
-    def set_entry(self, filename, entry):
+    def set_entry(self, filename, obj):
         """
         Set the entry.
         """
-        self.entries[filename] = entry
-        self.dirty = 1
-
-    def set_csig(self, filename, csig):
-        """
-        Set the csig .sconsign entry for a file
-
-        filename - the filename whose signature will be set
-        csig - the file's content signature
-        """
-
-        entry = self.get_entry(filename)
-        entry.csig = csig
-        self.set_entry(filename, entry)
-
-    def set_binfo(self, filename, bsig, bkids, bkidsigs, bact, bactsig):
-        """
-        Set the build info .sconsign entry for a file
+        self.entries[filename] = obj
+        self.dirty = True
 
-        filename - the filename whose signature will be set
-        bsig - the file's built signature
-        """
-
-        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 do_not_set_entry(self, filename, obj):
+        pass
 
-    def set_timestamp(self, filename, timestamp):
-        """
-        Set the timestamp .sconsign entry for a file
+    def store_info(self, filename, node):
+        entry = node.get_stored_info()
+        entry.binfo.merge(node.get_binfo())
+        self.to_be_merged[filename] = node
+        self.dirty = True
 
-        filename - the filename whose signature will be set
-        timestamp - the file's timestamp
-        """
+    def do_not_store_info(self, filename, node):
+        pass
 
-        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 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
+    def merge(self):
+        for key, node in self.to_be_merged.items():
+            entry = node.get_stored_info()
+            try:
+                ninfo = entry.ninfo
+            except AttributeError:
+                # This happens with SConf Nodes, because the configuration
+                # subsystem takes direct control over how the build decision
+                # is made and its information stored.
+                pass
+            else:
+                ninfo.merge(node.get_ninfo())
+            self.entries[key] = entry
+        self.to_be_merged = {}
 
 class DB(Base):
     """
     A Base subclass that reads and writes signature information
-    from a global .sconsign.dbm file.
+    from a global .sconsign.db* file--the actual file suffix is
+    determined by the database module.
     """
-    def __init__(self, dir, module=None):
-        Base.__init__(self, module)
+    def __init__(self, dir):
+        Base.__init__(self)
 
         self.dir = dir
 
+        db, mode = Get_DataBase(dir)
+
+        # Read using the path relative to the top of the Repository
+        # (self.dir.tpath) from which we're fetching the signature
+        # information.
+        path = normcase(dir.tpath)
         try:
-            global database
-            rawentries = database[self.dir.path]
+            rawentries = db[path]
         except KeyError:
             pass
         else:
             try:
                 self.entries = cPickle.loads(rawentries)
-                if type(self.entries) is not type({}):
+                if not isinstance(self.entries, dict):
                     self.entries = {}
                     raise TypeError
             except KeyboardInterrupt:
                 raise
-            except:
+            except Exception, e:
                 SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
-                                    "Ignoring corrupt sconsign entry : %s"%self.dir.path)
+                                    "Ignoring corrupt sconsign entry : %s (%s)\n"%(self.dir.tpath, e))
+            for key, entry in self.entries.items():
+                entry.convert_from_sconsign(dir, key)
+
+        if mode == "r":
+            # This directory is actually under a repository, which means
+            # likely they're reaching in directly for a dependency on
+            # a file there.  Don't actually set any entry info, so we
+            # won't try to write to that .sconsign.dblite file.
+            self.set_entry = self.do_not_set_entry
+            self.store_info = self.do_not_store_info
 
         global sig_files
         sig_files.append(self)
 
-    def write(self):
-        if self.dirty:
-            global database
-            database[self.dir.path] = cPickle.dumps(self.entries, 1)
+    def write(self, sync=1):
+        if not self.dirty:
+            return
+
+        self.merge()
+
+        db, mode = Get_DataBase(self.dir)
+
+        # Write using the path relative to the top of the SConstruct
+        # directory (self.dir.path), not relative to the top of
+        # the Repository; we only write to our own .sconsign file,
+        # not to .sconsign files in Repositories.
+        path = normcase(self.dir.path)
+        for key, entry in self.entries.items():
+            entry.convert_to_sconsign()
+        db[path] = cPickle.dumps(self.entries, 1)
+
+        if sync:
             try:
-                database.sync()
+                syncmethod = db.sync
             except AttributeError:
                 # Not all anydbm modules have sync() methods.
                 pass
+            else:
+                syncmethod()
 
 class Dir(Base):
-    def __init__(self, fp=None, module=None):
+    def __init__(self, fp=None, dir=None):
         """
         fp - file pointer to read entries from
-        module - the signature module being used
         """
-        Base.__init__(self, module)
+        Base.__init__(self)
+
+        if not fp:
+            return
 
-        if fp:
-            self.entries = cPickle.load(fp)
-            if type(self.entries) is not type({}):
-                self.entries = {}
-                raise TypeError
+        self.entries = cPickle.load(fp)
+        if not isinstance(self.entries, dict):
+            self.entries = {}
+            raise TypeError
+
+        if dir:
+            for key, entry in self.entries.items():
+                entry.convert_from_sconsign(dir, key)
 
 class DirFile(Dir):
     """
     Encapsulates reading and writing a per-directory .sconsign file.
     """
-    def __init__(self, dir, module=None):
+    def __init__(self, dir):
         """
         dir - the directory for the file
-        module - the signature module being used
         """
 
         self.dir = dir
@@ -252,7 +287,7 @@ class DirFile(Dir):
             fp = None
 
         try:
-            Dir.__init__(self, fp, module)
+            Dir.__init__(self, fp, dir)
         except KeyboardInterrupt:
             raise
         except:
@@ -262,7 +297,7 @@ class DirFile(Dir):
         global sig_files
         sig_files.append(self)
 
-    def write(self):
+    def write(self, sync=1):
         """
         Write the .sconsign file to disk.
 
@@ -275,49 +310,72 @@ class DirFile(Dir):
         to the .sconsign file.  Either way, always try to remove
         the temporary file at the end.
         """
-        if self.dirty:
-            temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
+        if not self.dirty:
+            return
+
+        self.merge()
+
+        temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
+        try:
+            file = open(temp, 'wb')
+            fname = temp
+        except IOError:
             try:
-                file = open(temp, 'wb')
-                fname = temp
+                file = open(self.sconsign, 'wb')
+                fname = self.sconsign
             except IOError:
-                try:
-                    file = open(self.sconsign, 'wb')
-                    fname = self.sconsign
-                except IOError:
-                    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 OSError:
-                    pass
-                try:
-                    os.rename(fname, self.sconsign)
-                except OSError:
-                    open(self.sconsign, 'wb').write(open(fname, 'rb').read())
-                    os.chmod(self.sconsign, mode)
+                return
+        for key, entry in self.entries.items():
+            entry.convert_to_sconsign()
+        cPickle.dump(self.entries, file, 1)
+        file.close()
+        if fname != self.sconsign:
             try:
-                os.unlink(temp)
-            except OSError:
+                mode = os.stat(self.sconsign)[0]
+                os.chmod(self.sconsign, 0666)
+                os.unlink(self.sconsign)
+            except (IOError, OSError):
+                # Try to carry on in the face of either OSError
+                # (things like permission issues) or IOError (disk
+                # or network issues).  If there's a really dangerous
+                # issue, it should get re-raised by the calls below.
                 pass
+            try:
+                os.rename(fname, self.sconsign)
+            except OSError:
+                # An OSError failure to rename may indicate something
+                # like the directory has no write permission, but
+                # the .sconsign file itself might still be writable,
+                # so try writing on top of it directly.  An IOError
+                # here, or in any of the following calls, would get
+                # raised, indicating something like a potentially
+                # serious disk or network issue.
+                open(self.sconsign, 'wb').write(open(fname, 'rb').read())
+                os.chmod(self.sconsign, mode)
+        try:
+            os.unlink(temp)
+        except (IOError, OSError):
+            pass
 
-ForDirectory = DirFile
+ForDirectory = DB
 
 def File(name, dbm_module=None):
     """
-    Arrange for all signatures to be stored in a global .sconsign.dbm
+    Arrange for all signatures to be stored in a global .sconsign.db*
     file.
     """
-    global database
-    if database is None:
-        if dbm_module is None:
-            import SCons.dblite
-            dbm_module = SCons.dblite
-        database = dbm_module.open(name, "c")
-
-    global ForDirectory
-    ForDirectory = DB
+    global ForDirectory, DB_Name, DB_Module
+    if name is None:
+        ForDirectory = DirFile
+        DB_Module = None
+    else:
+        ForDirectory = DB
+        DB_Name = name
+        if not dbm_module is None:
+            DB_Module = dbm_module
+
+# Local Variables:
+# tab-width:4
+# indent-tabs-mode:nil
+# End:
+# vim: set expandtab tabstop=4 shiftwidth=4: