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