Expanded SCons.Scanner.LaTeX.comment_re to not break on \%
[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 SCons.compat
33
34 import os
35 # compat layer imports "cPickle" for us if it's available.
36 import pickle
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 = pickle.loads(rawentries)
207                 if not isinstance(self.entries, dict):
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] = pickle.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 = pickle.load(fp)
266         if not isinstance(self.entries, dict):
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         pickle.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
379 # Local Variables:
380 # tab-width:4
381 # indent-tabs-mode:nil
382 # End:
383 # vim: set expandtab tabstop=4 shiftwidth=4: