SCons.SConsign

Package SCons :: Module SConsign |
[hide private]
[frames] | no frames]

# Source Code for Module SCons.SConsign

      1  """SCons.SConsign
      2
      3  Writing and reading information to the .sconsign file or files.
      4
      5  """
      6
      7  #
      8  # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 The SCons Foundation
      9  #
     10  # Permission is hereby granted, free of charge, to any person obtaining
     11  # a copy of this software and associated documentation files (the
     12  # "Software"), to deal in the Software without restriction, including
     13  # without limitation the rights to use, copy, modify, merge, publish,
     14  # distribute, sublicense, and/or sell copies of the Software, and to
     15  # permit persons to whom the Software is furnished to do so, subject to
     16  # the following conditions:
     17  #
     18  # The above copyright notice and this permission notice shall be included
     19  # in all copies or substantial portions of the Software.
     20  #
     21  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
     22  # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
     23  # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
     24  # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
     25  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
     26  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
     27  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
     28  #
     29
     30  __revision__ = "src/engine/SCons/SConsign.py 2725 2008/03/31 12:52:02 knight"
     31
     32  import SCons.compat
     33
     34  import cPickle
     35  import os
     36  import os.path
     37
     38  import SCons.dblite
     39  import SCons.Warnings
     40
     41 -def corrupt_dblite_warning(filename):
     42      SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
     43                          "Ignoring corrupt .sconsign file: %s"%filename)
     44
     45  SCons.dblite.ignore_corrupt_dbfiles = 1
     46  SCons.dblite.corruption_warning = corrupt_dblite_warning
     47
     48  #XXX Get rid of the global array so this becomes re-entrant.
     49  sig_files = []
     50
     51  # Info for the database SConsign implementation (now the default):
     52  # "DataBase" is a dictionary that maps top-level SConstruct directories
     53  # to open database handles.
     54  # "DB_Module" is the Python database module to create the handles.
     55  # "DB_Name" is the base name of the database file (minus any
     56  # extension the underlying DB module will add).
     57  DataBase = {}
     58  DB_Module = SCons.dblite
     59  DB_Name = ".sconsign"
     60  DB_sync_list = []
     61
     62 -def Get_DataBase(dir):
     63      global DataBase, DB_Module, DB_Name
     64      top = dir.fs.Top
     65      if not os.path.isabs(DB_Name) and top.repositories:
     66          mode = "c"
     67          for d in [top] + top.repositories:
     68              if dir.is_under(d):
     69                  try:
     70                      return DataBase[d], mode
     71                  except KeyError:
     72                      path = d.entry_abspath(DB_Name)
     73                      try: db = DataBase[d] = DB_Module.open(path, mode)
     74                      except (IOError, OSError): pass
     75                      else:
     76                          if mode != "r":
     77                              DB_sync_list.append(db)
     78                          return db, mode
     79              mode = "r"
     80      try:
     81          return DataBase[top], "c"
     82      except KeyError:
     83          db = DataBase[top] = DB_Module.open(DB_Name, "c")
     84          DB_sync_list.append(db)
     85          return db, "c"
     86      except TypeError:
     87          print "DataBase =", DataBase
     88          raise
     89
     90 -def Reset():
     91      """Reset global state.  Used by unit tests that end up using
     92      SConsign multiple times to get a clean slate for each test."""
     93      global sig_files, DB_sync_list
     94      sig_files = []
     95      DB_sync_list = []
     96
     97  normcase = os.path.normcase
     98
     99 -def write():
    100      global sig_files
    101      for sig_file in sig_files:
    102          sig_file.write(sync=0)
    103      for db in DB_sync_list:
    104          try:
    105              syncmethod = db.sync
    106          except AttributeError:
    107              pass # Not all anydbm modules have sync() methods.
    108          else:
    109              syncmethod()
    110
    111 -class SConsignEntry:
    112      """
    113      Wrapper class for the generic entry in a .sconsign file.
    114      The Node subclass populates it with attributes as it pleases.
    115
    116      XXX As coded below, we do expect a '.binfo' attribute to be added,
    117      but we'll probably generalize this in the next refactorings.
    118      """
    119      current_version_id = 1
    120 -    def __init__(self):
    121          # Create an object attribute from the class attribute so it ends up
    122          # in the pickled data in the .sconsign file.
    123          _version_id = self.current_version_id
    124 -    def convert_to_sconsign(self):
    125          self.binfo.convert_to_sconsign()
    126 -    def convert_from_sconsign(self, dir, name):
    127          self.binfo.convert_from_sconsign(dir, name)
    128
    129 -class Base:
    130      """
    131      This is the controlling class for the signatures for the collection of
    132      entries associated with a specific directory.  The actual directory
    133      association will be maintained by a subclass that is specific to
    134      the underlying storage method.  This class provides a common set of
    135      methods for fetching and storing the individual bits of information
    136      that make up signature entry.
    137      """
    138 -    def __init__(self):
    139          self.entries = {}
    140          self.dirty = False
    141          self.to_be_merged = {}
    142
    143 -    def get_entry(self, filename):
    144          """
    145          Fetch the specified entry attribute.
    146          """
    147          return self.entries[filename]
    148
    149 -    def set_entry(self, filename, obj):
    150          """
    151          Set the entry.
    152          """
    153          self.entries[filename] = obj
    154          self.dirty = True
    155
    156 -    def do_not_set_entry(self, filename, obj):
    157          pass
    158
    159 -    def store_info(self, filename, node):
    160          entry = node.get_stored_info()
    161          entry.binfo.merge(node.get_binfo())
    162          self.to_be_merged[filename] = node
    163          self.dirty = True
    164
    165 -    def do_not_store_info(self, filename, node):
    166          pass
    167
    168 -    def merge(self):
    169          for key, node in self.to_be_merged.items():
    170              entry = node.get_stored_info()
    171              try:
    172                  ninfo = entry.ninfo
    173              except AttributeError:
    174                  # This happens with SConf Nodes, because the configuration
    175                  # subsystem takes direct control over how the build decision
    176                  # is made and its information stored.
    177                  pass
    178              else:
    179                  ninfo.merge(node.get_ninfo())
    180              self.entries[key] = entry
    181          self.to_be_merged = {}
    182
    183 -class DB(Base):
    184      """
    185      A Base subclass that reads and writes signature information
    186      from a global .sconsign.db* file--the actual file suffix is
    187      determined by the database module.
    188      """
    189 -    def __init__(self, dir):
    190          Base.__init__(self)
    191
    192          self.dir = dir
    193
    194          db, mode = Get_DataBase(dir)
    195
    196          # Read using the path relative to the top of the Repository
    197          # (self.dir.tpath) from which we're fetching the signature
    198          # information.
    199          path = normcase(dir.tpath)
    200          try:
    201              rawentries = db[path]
    202          except KeyError:
    203              pass
    204          else:
    205              try:
    206                  self.entries = cPickle.loads(rawentries)
    207                  if type(self.entries) is not type({}):
    208                      self.entries = {}
    209                      raise TypeError
    210              except KeyboardInterrupt:
    211                  raise
    212              except Exception, e:
    213                  SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
    214                                      "Ignoring corrupt sconsign entry : %s (%s)\n"%(self.dir.tpath, e))
    215              for key, entry in self.entries.items():
    216                  entry.convert_from_sconsign(dir, key)
    217
    218          if mode == "r":
    219              # This directory is actually under a repository, which means
    220              # likely they're reaching in directly for a dependency on
    221              # a file there.  Don't actually set any entry info, so we
    222              # won't try to write to that .sconsign.dblite file.
    223              self.set_entry = self.do_not_set_entry
    224              self.store_info = self.do_not_store_info
    225
    226          global sig_files
    227          sig_files.append(self)
    228
    229 -    def write(self, sync=1):
    230          if not self.dirty:
    231              return
    232
    233          self.merge()
    234
    235          db, mode = Get_DataBase(self.dir)
    236
    237          # Write using the path relative to the top of the SConstruct
    238          # directory (self.dir.path), not relative to the top of
    239          # the Repository; we only write to our own .sconsign file,
    240          # not to .sconsign files in Repositories.
    241          path = normcase(self.dir.path)
    242          for key, entry in self.entries.items():
    243              entry.convert_to_sconsign()
    244          db[path] = cPickle.dumps(self.entries, 1)
    245
    246          if sync:
    247              try:
    248                  syncmethod = db.sync
    249              except AttributeError:
    250                  # Not all anydbm modules have sync() methods.
    251                  pass
    252              else:
    253                  syncmethod()
    254
    255 -class Dir(Base):
    256 -    def __init__(self, fp=None, dir=None):
    257          """
    258          fp - file pointer to read entries from
    259          """
    260          Base.__init__(self)
    261
    262          if not fp:
    263              return
    264
    265          self.entries = cPickle.load(fp)
    266          if type(self.entries) is not type({}):
    267              self.entries = {}
    268              raise TypeError
    269
    270          if dir:
    271              for key, entry in self.entries.items():
    272                  entry.convert_from_sconsign(dir, key)
    273
    274 -class DirFile(Dir):
    275      """
    276      Encapsulates reading and writing a per-directory .sconsign file.
    277      """
    278 -    def __init__(self, dir):
    279          """
    280          dir - the directory for the file
    281          """
    282
    283          self.dir = dir
    284          self.sconsign = os.path.join(dir.path, '.sconsign')
    285
    286          try:
    287              fp = open(self.sconsign, 'rb')
    288          except IOError:
    289              fp = None
    290
    291          try:
    292              Dir.__init__(self, fp, dir)
    293          except KeyboardInterrupt:
    294              raise
    295          except:
    296              SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
    297                                  "Ignoring corrupt .sconsign file: %s"%self.sconsign)
    298
    299          global sig_files
    300          sig_files.append(self)
    301
    302 -    def write(self, sync=1):
    303          """
    304          Write the .sconsign file to disk.
    305
    306          Try to write to a temporary file first, and rename it if we
    307          succeed.  If we can't write to the temporary file, it's
    308          probably because the directory isn't writable (and if so,
    309          how did we build anything in this directory, anyway?), so
    310          try to write directly to the .sconsign file as a backup.
    311          If we can't rename, try to copy the temporary contents back
    312          to the .sconsign file.  Either way, always try to remove
    313          the temporary file at the end.
    314          """
    315          if not self.dirty:
    316              return
    317
    318          self.merge()
    319
    320          temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
    321          try:
    322              file = open(temp, 'wb')
    323              fname = temp
    324          except IOError:
    325              try:
    326                  file = open(self.sconsign, 'wb')
    327                  fname = self.sconsign
    328              except IOError:
    329                  return
    330          for key, entry in self.entries.items():
    331              entry.convert_to_sconsign()
    332          cPickle.dump(self.entries, file, 1)
    333          file.close()
    334          if fname != self.sconsign:
    335              try:
    336                  mode = os.stat(self.sconsign)[0]
    337                  os.chmod(self.sconsign, 0666)
    338                  os.unlink(self.sconsign)
    339              except (IOError, OSError):
    340                  # Try to carry on in the face of either OSError
    341                  # (things like permission issues) or IOError (disk
    342                  # or network issues).  If there's a really dangerous
    343                  # issue, it should get re-raised by the calls below.
    344                  pass
    345              try:
    346                  os.rename(fname, self.sconsign)
    347              except OSError:
    348                  # An OSError failure to rename may indicate something
    349                  # like the directory has no write permission, but
    350                  # the .sconsign file itself might still be writable,
    351                  # so try writing on top of it directly.  An IOError
    352                  # here, or in any of the following calls, would get
    353                  # raised, indicating something like a potentially
    354                  # serious disk or network issue.
    355                  open(self.sconsign, 'wb').write(open(fname, 'rb').read())
    356                  os.chmod(self.sconsign, mode)
    357          try:
    358              os.unlink(temp)
    359          except (IOError, OSError):
    360              pass
    361
    362  ForDirectory = DB
    363
    364 -def File(name, dbm_module=None):
    365      """
    366      Arrange for all signatures to be stored in a global .sconsign.db*
    367      file.
    368      """
    369      global ForDirectory, DB_Name, DB_Module
    370      if name is None:
    371          ForDirectory = DirFile
    372          DB_Module = None
    373      else:
    374          ForDirectory = DB
    375          DB_Name = name
    376          if not dbm_module is None:
    377              DB_Module = dbm_module
    378