67b2affab7c81d824c26e9d9a260bf85960147d6
[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 import string
36 import time
37
38 import SCons.dblite
39 import SCons.Sig
40 import SCons.Warnings
41
42 def corrupt_dblite_warning(filename):
43     SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
44                         "Ignoring corrupt .sconsign file: %s"%filename)
45
46 SCons.dblite.ignore_corrupt_dbfiles = 1
47 SCons.dblite.corruption_warning = corrupt_dblite_warning
48
49 #XXX Get rid of the global array so this becomes re-entrant.
50 sig_files = []
51
52 # Info for the database SConsign implementation (now the default):
53 # "DataBase" is a dictionary that maps top-level SConstruct directories
54 # to open database handles.
55 # "DB_Module" is the Python database module to create the handles.
56 # "DB_Name" is the base name of the database file (minus any
57 # extension the underlying DB module will add).
58 DataBase = {}
59 DB_Module = SCons.dblite
60 DB_Name = ".sconsign"
61 DB_sync_list = []
62
63 def 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
91 def 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
98 normcase = os.path.normcase
99
100 def 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 anydbm modules have sync() methods.
109         else:
110             syncmethod()
111
112 class Base:
113     """
114     This is the controlling class for the signatures for the collection of
115     entries associated with a specific directory.  The actual directory
116     association will be maintained by a subclass that is specific to
117     the underlying storage method.  This class provides a common set of
118     methods for fetching and storing the individual bits of information
119     that make up signature entry.
120     """
121     def __init__(self, module=None):
122         """
123         module - the signature module being used
124         """
125
126         self.module = module or SCons.Sig.default_calc.module
127         self.entries = {}
128         self.dirty = 0
129
130     def get_entry(self, filename):
131         """
132         Fetch the specified entry attribute.
133         """
134         return self.entries[filename]
135
136     def set_entry(self, filename, obj):
137         """
138         Set the entry.
139         """
140         self.entries[filename] = obj
141         self.dirty = 1
142
143     def do_not_set_entry(self, filename, obj):
144         pass
145
146 class DB(Base):
147     """
148     A Base subclass that reads and writes signature information
149     from a global .sconsign.db* file--the actual file suffix is
150     determined by the specified database module.
151     """
152     def __init__(self, dir, module=None):
153         Base.__init__(self, module)
154
155         self.dir = dir
156
157         db, mode = Get_DataBase(dir)
158
159         # Read using the path relative to the top of the Repository
160         # (self.dir.tpath) from which we're fetching the signature
161         # information.
162         path = normcase(dir.tpath)
163         try:
164             rawentries = db[path]
165         except KeyError:
166             pass
167         else:
168             try:
169                 self.entries = cPickle.loads(rawentries)
170                 if type(self.entries) is not type({}):
171                     self.entries = {}
172                     raise TypeError
173             except KeyboardInterrupt:
174                 raise
175             except Exception, e:
176                 SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
177                                     "Ignoring corrupt sconsign entry : %s (%s)\n"%(self.dir.tpath, e))
178             for key, entry in self.entries.items():
179                 entry.convert_from_sconsign(dir, key)
180
181         if mode == "r":
182             # This directory is actually under a repository, which means
183             # likely they're reaching in directly for a dependency on
184             # a file there.  Don't actually set any entry info, so we
185             # won't try to write to that .sconsign.dblite file.
186             self.set_entry = self.do_not_set_entry
187
188         global sig_files
189         sig_files.append(self)
190
191     def write(self, sync=1):
192         if not self.dirty:
193             return
194
195         db, mode = Get_DataBase(self.dir)
196
197         # Write using the path relative to the top of the SConstruct
198         # directory (self.dir.path), not relative to the top of
199         # the Repository; we only write to our own .sconsign file,
200         # not to .sconsign files in Repositories.
201         path = normcase(self.dir.path)
202         for key, entry in self.entries.items():
203             entry.convert_to_sconsign()
204         db[path] = cPickle.dumps(self.entries, 1)
205
206         if sync:
207             try:
208                 syncmethod = db.sync
209             except AttributeError:
210                 # Not all anydbm modules have sync() methods.
211                 pass
212             else:
213                 syncmethod()
214
215 class Dir(Base):
216     def __init__(self, fp=None, module=None):
217         """
218         fp - file pointer to read entries from
219         module - the signature module being used
220         """
221         Base.__init__(self, module)
222
223         if fp:
224             self.entries = cPickle.load(fp)
225             if type(self.entries) is not type({}):
226                 self.entries = {}
227                 raise TypeError
228
229 class DirFile(Dir):
230     """
231     Encapsulates reading and writing a per-directory .sconsign file.
232     """
233     def __init__(self, dir, module=None):
234         """
235         dir - the directory for the file
236         module - the signature module being used
237         """
238
239         self.dir = dir
240         self.sconsign = os.path.join(dir.path, '.sconsign')
241
242         try:
243             fp = open(self.sconsign, 'rb')
244         except IOError:
245             fp = None
246
247         try:
248             Dir.__init__(self, fp, module)
249         except KeyboardInterrupt:
250             raise
251         except:
252             SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
253                                 "Ignoring corrupt .sconsign file: %s"%self.sconsign)
254
255         global sig_files
256         sig_files.append(self)
257
258     def get_entry(self, filename):
259         """
260         Fetch the specified entry attribute, converting from .sconsign
261         format to in-memory format.
262         """
263         entry = Dir.get_entry(self, filename)
264         entry.convert_from_sconsign(self.dir, filename)
265         return entry
266
267     def write(self, sync=1):
268         """
269         Write the .sconsign file to disk.
270
271         Try to write to a temporary file first, and rename it if we
272         succeed.  If we can't write to the temporary file, it's
273         probably because the directory isn't writable (and if so,
274         how did we build anything in this directory, anyway?), so
275         try to write directly to the .sconsign file as a backup.
276         If we can't rename, try to copy the temporary contents back
277         to the .sconsign file.  Either way, always try to remove
278         the temporary file at the end.
279         """
280         if self.dirty:
281             temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
282             try:
283                 file = open(temp, 'wb')
284                 fname = temp
285             except IOError:
286                 try:
287                     file = open(self.sconsign, 'wb')
288                     fname = self.sconsign
289                 except IOError:
290                     return
291             for key, entry in self.entries.items():
292                 entry.convert_to_sconsign()
293             cPickle.dump(self.entries, file, 1)
294             file.close()
295             if fname != self.sconsign:
296                 try:
297                     mode = os.stat(self.sconsign)[0]
298                     os.chmod(self.sconsign, 0666)
299                     os.unlink(self.sconsign)
300                 except (IOError, OSError):
301                     # Try to carry on in the face of either OSError
302                     # (things like permission issues) or IOError (disk
303                     # or network issues).  If there's a really dangerous
304                     # issue, it should get re-raised by the calls below.
305                     pass
306                 try:
307                     os.rename(fname, self.sconsign)
308                 except OSError:
309                     # An OSError failure to rename may indicate something
310                     # like the directory has no write permission, but
311                     # the .sconsign file itself might still be writable,
312                     # so try writing on top of it directly.  An IOError
313                     # here, or in any of the following calls, would get
314                     # raised, indicating something like a potentially
315                     # serious disk or network issue.
316                     open(self.sconsign, 'wb').write(open(fname, 'rb').read())
317                     os.chmod(self.sconsign, mode)
318             try:
319                 os.unlink(temp)
320             except (IOError, OSError):
321                 pass
322
323 ForDirectory = DB
324
325 def File(name, dbm_module=None):
326     """
327     Arrange for all signatures to be stored in a global .sconsign.db*
328     file.
329     """
330     global ForDirectory, DB_Name, DB_Module
331     if name is None:
332         ForDirectory = DirFile
333         DB_Module = None
334     else:
335         ForDirectory = DB
336         DB_Name = name
337         if not dbm_module is None:
338             DB_Module = dbm_module