1"""SCons.SConsign 2 3Writing and reading information to the .sconsign file or files. 4 5""" 6 7# 8# Copyright (c) 2001 - 2014 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 2014/07/05 09:42:21 garyo" 31 32import SCons.compat 33 34import os 35# compat layer imports "cPickle" for us if it's available. 36import pickle 37 38import SCons.dblite 39import SCons.Warnings 40 41def corrupt_dblite_warning(filename): 42 SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning, 43 "Ignoring corrupt .sconsign file: %s"%filename) 44 45SCons.dblite.ignore_corrupt_dbfiles = 1 46SCons.dblite.corruption_warning = corrupt_dblite_warning 47 48#XXX Get rid of the global array so this becomes re-entrant. 49sig_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). 57DataBase = {} 58DB_Module = SCons.dblite 59# Nuitka: Avoid collisions with newer Scons using newer pickle protocols 60DB_Name = ".sconsign2" 61DB_sync_list = [] 62 63def Get_DataBase(dir): 64 global DataBase, DB_Module, DB_Name 65 top = dir.fs.Top 66 if not os.path.isabs(DB_Name) and top.repositories: 67 mode = "c" 68 for d in [top] + top.repositories: 69 if dir.is_under(d): 70 try: 71 return DataBase[d], mode 72 except KeyError: 73 path = d.entry_abspath(DB_Name) 74 try: db = DataBase[d] = DB_Module.open(path, mode) 75 except (IOError, OSError): pass 76 else: 77 if mode != "r": 78 DB_sync_list.append(db) 79 return db, mode 80 mode = "r" 81 try: 82 return DataBase[top], "c" 83 except KeyError: 84 db = DataBase[top] = DB_Module.open(DB_Name, "c") 85 DB_sync_list.append(db) 86 return db, "c" 87 except TypeError: 88 print "DataBase =", DataBase 89 raise 90 91def Reset(): 92 """Reset global state. Used by unit tests that end up using 93 SConsign multiple times to get a clean slate for each test.""" 94 global sig_files, DB_sync_list 95 sig_files = [] 96 DB_sync_list = [] 97 98normcase = os.path.normcase 99 100def write(): 101 global sig_files 102 for sig_file in sig_files: 103 sig_file.write(sync=0) 104 for db in DB_sync_list: 105 try: 106 syncmethod = db.sync 107 except AttributeError: 108 pass # Not all dbm modules have sync() methods. 109 else: 110 syncmethod() 111 try: 112 closemethod = db.close 113 except AttributeError: 114 pass # Not all dbm modules have close() methods. 115 else: 116 closemethod() 117 118class SConsignEntry(object): 119 """ 120 Wrapper class for the generic entry in a .sconsign file. 121 The Node subclass populates it with attributes as it pleases. 122 123 XXX As coded below, we do expect a '.binfo' attribute to be added, 124 but we'll probably generalize this in the next refactorings. 125 """ 126 current_version_id = 1 127 def __init__(self): 128 # Create an object attribute from the class attribute so it ends up 129 # in the pickled data in the .sconsign file. 130 _version_id = self.current_version_id 131 def convert_to_sconsign(self): 132 self.binfo.convert_to_sconsign() 133 def convert_from_sconsign(self, dir, name): 134 self.binfo.convert_from_sconsign(dir, name) 135 136class Base(object): 137 """ 138 This is the controlling class for the signatures for the collection of 139 entries associated with a specific directory. The actual directory 140 association will be maintained by a subclass that is specific to 141 the underlying storage method. This class provides a common set of 142 methods for fetching and storing the individual bits of information 143 that make up signature entry. 144 """ 145 def __init__(self): 146 self.entries = {} 147 self.dirty = False 148 self.to_be_merged = {} 149 150 def get_entry(self, filename): 151 """ 152 Fetch the specified entry attribute. 153 """ 154 return self.entries[filename] 155 156 def set_entry(self, filename, obj): 157 """ 158 Set the entry. 159 """ 160 self.entries[filename] = obj 161 self.dirty = True 162 163 def do_not_set_entry(self, filename, obj): 164 pass 165 166 def store_info(self, filename, node): 167 entry = node.get_stored_info() 168 entry.binfo.merge(node.get_binfo()) 169 self.to_be_merged[filename] = node 170 self.dirty = True 171 172 def do_not_store_info(self, filename, node): 173 pass 174 175 def merge(self): 176 for key, node in self.to_be_merged.items(): 177 entry = node.get_stored_info() 178 try: 179 ninfo = entry.ninfo 180 except AttributeError: 181 # This happens with SConf Nodes, because the configuration 182 # subsystem takes direct control over how the build decision 183 # is made and its information stored. 184 pass 185 else: 186 ninfo.merge(node.get_ninfo()) 187 self.entries[key] = entry 188 self.to_be_merged = {} 189 190class DB(Base): 191 """ 192 A Base subclass that reads and writes signature information 193 from a global .sconsign.db* file--the actual file suffix is 194 determined by the database module. 195 """ 196 def __init__(self, dir): 197 Base.__init__(self) 198 199 self.dir = dir 200 201 db, mode = Get_DataBase(dir) 202 203 # Read using the path relative to the top of the Repository 204 # (self.dir.tpath) from which we're fetching the signature 205 # information. 206 path = normcase(dir.tpath) 207 try: 208 rawentries = db[path] 209 except KeyError: 210 pass 211 else: 212 try: 213 self.entries = pickle.loads(rawentries) 214 if not isinstance(self.entries, dict): 215 self.entries = {} 216 raise TypeError 217 except KeyboardInterrupt: 218 raise 219 except Exception, e: 220 SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning, 221 "Ignoring corrupt sconsign entry : %s (%s)\n"%(self.dir.tpath, e)) 222 for key, entry in self.entries.items(): 223 entry.convert_from_sconsign(dir, key) 224 225 if mode == "r": 226 # This directory is actually under a repository, which means 227 # likely they're reaching in directly for a dependency on 228 # a file there. Don't actually set any entry info, so we 229 # won't try to write to that .sconsign.dblite file. 230 self.set_entry = self.do_not_set_entry 231 self.store_info = self.do_not_store_info 232 233 global sig_files 234 sig_files.append(self) 235 236 def write(self, sync=1): 237 if not self.dirty: 238 return 239 240 self.merge() 241 242 db, mode = Get_DataBase(self.dir) 243 244 # Write using the path relative to the top of the SConstruct 245 # directory (self.dir.path), not relative to the top of 246 # the Repository; we only write to our own .sconsign file, 247 # not to .sconsign files in Repositories. 248 path = normcase(self.dir.path) 249 for key, entry in self.entries.items(): 250 entry.convert_to_sconsign() 251 db[path] = pickle.dumps(self.entries, 1) 252 253 if sync: 254 try: 255 syncmethod = db.sync 256 except AttributeError: 257 # Not all anydbm modules have sync() methods. 258 pass 259 else: 260 syncmethod() 261 262class Dir(Base): 263 def __init__(self, fp=None, dir=None): 264 """ 265 fp - file pointer to read entries from 266 """ 267 Base.__init__(self) 268 269 if not fp: 270 return 271 272 self.entries = pickle.load(fp) 273 if not isinstance(self.entries, dict): 274 self.entries = {} 275 raise TypeError 276 277 if dir: 278 for key, entry in self.entries.items(): 279 entry.convert_from_sconsign(dir, key) 280 281class DirFile(Dir): 282 """ 283 Encapsulates reading and writing a per-directory .sconsign file. 284 """ 285 def __init__(self, dir): 286 """ 287 dir - the directory for the file 288 """ 289 290 self.dir = dir 291 self.sconsign = os.path.join(dir.path, '.sconsign') 292 293 try: 294 fp = open(self.sconsign, 'rb') 295 except IOError: 296 fp = None 297 298 try: 299 Dir.__init__(self, fp, dir) 300 except KeyboardInterrupt: 301 raise 302 except: 303 SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning, 304 "Ignoring corrupt .sconsign file: %s"%self.sconsign) 305 306 global sig_files 307 sig_files.append(self) 308 309 def write(self, sync=1): 310 """ 311 Write the .sconsign file to disk. 312 313 Try to write to a temporary file first, and rename it if we 314 succeed. If we can't write to the temporary file, it's 315 probably because the directory isn't writable (and if so, 316 how did we build anything in this directory, anyway?), so 317 try to write directly to the .sconsign file as a backup. 318 If we can't rename, try to copy the temporary contents back 319 to the .sconsign file. Either way, always try to remove 320 the temporary file at the end. 321 """ 322 if not self.dirty: 323 return 324 325 self.merge() 326 327 temp = os.path.join(self.dir.path, '.scons%d' % os.getpid()) 328 try: 329 file = open(temp, 'wb') 330 fname = temp 331 except IOError: 332 try: 333 file = open(self.sconsign, 'wb') 334 fname = self.sconsign 335 except IOError: 336 return 337 for key, entry in self.entries.items(): 338 entry.convert_to_sconsign() 339 pickle.dump(self.entries, file, 1) 340 file.close() 341 if fname != self.sconsign: 342 try: 343 mode = os.stat(self.sconsign)[0] 344 os.chmod(self.sconsign, 0666) 345 os.unlink(self.sconsign) 346 except (IOError, OSError): 347 # Try to carry on in the face of either OSError 348 # (things like permission issues) or IOError (disk 349 # or network issues). If there's a really dangerous 350 # issue, it should get re-raised by the calls below. 351 pass 352 try: 353 os.rename(fname, self.sconsign) 354 except OSError: 355 # An OSError failure to rename may indicate something 356 # like the directory has no write permission, but 357 # the .sconsign file itself might still be writable, 358 # so try writing on top of it directly. An IOError 359 # here, or in any of the following calls, would get 360 # raised, indicating something like a potentially 361 # serious disk or network issue. 362 open(self.sconsign, 'wb').write(open(fname, 'rb').read()) 363 os.chmod(self.sconsign, mode) 364 try: 365 os.unlink(temp) 366 except (IOError, OSError): 367 pass 368 369ForDirectory = DB 370 371def File(name, dbm_module=None): 372 """ 373 Arrange for all signatures to be stored in a global .sconsign.db* 374 file. 375 """ 376 global ForDirectory, DB_Name, DB_Module 377 if name is None: 378 ForDirectory = DirFile 379 DB_Module = None 380 else: 381 ForDirectory = DB 382 DB_Name = name 383 if not dbm_module is None: 384 DB_Module = dbm_module 385 386# Local Variables: 387# tab-width:4 388# indent-tabs-mode:nil 389# End: 390# vim: set expandtab tabstop=4 shiftwidth=4: 391