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