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