6bdfe099e28d90cb9589858981cb6ecb4305170e
[scons.git] / src / engine / SCons / Sig / __init__.py
1 """SCons.Sig
2
3 The Signature package for the scons software construction utility.
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 time
36
37 import SCons.Node
38 import SCons.Warnings
39
40 try:
41     import MD5
42     default_module = MD5
43 except ImportError:
44     import TimeStamp
45     default_module = TimeStamp
46
47 default_max_drift = 2*24*60*60
48
49 #XXX Get rid of the global array so this becomes re-entrant.
50 sig_files = []
51
52 SConsign_db = None
53
54 def write():
55     global sig_files
56     for sig_file in sig_files:
57         sig_file.write()
58
59
60 class SConsignEntry:
61
62     """Objects of this type are pickled to the .sconsign file, so it
63     should only contain simple builtin Python datatypes and no methods.
64
65     This class is used to store cache information about nodes between
66     scons runs for efficiency, and to store the build signature for
67     nodes so that scons can determine if they are out of date. """
68
69     # setup the default value for various attributes:
70     # (We make the class variables so the default values won't get pickled
71     # with the instances, which would waste a lot of space)
72     timestamp = None
73     bsig = None
74     csig = None
75     implicit = None
76     bkids = []
77     bkidsigs = []
78     bact = None
79     bactsig = None
80
81 class _SConsign:
82     """
83     This is the controlling class for the signatures for the collection of
84     entries associated with a specific directory.  The actual directory
85     association will be maintained by a subclass that is specific to
86     the underlying storage method.  This class provides a common set of
87     methods for fetching and storing the individual bits of information
88     that make up signature entry.
89     """
90     def __init__(self, module=None):
91         """
92         module - the signature module being used
93         """
94
95         if module is None:
96             self.module = default_calc.module
97         else:
98             self.module = module
99         self.entries = {}
100         self.dirty = 0
101
102     # A null .sconsign entry.  We define this here so that it will
103     # be easy to keep this in sync if/whenever we change the type of
104     # information returned by the get() method, below.
105     null_siginfo = (None, None, None)
106
107     def get(self, filename):
108         """
109         Get the .sconsign entry for a file
110
111         filename - the filename whose signature will be returned
112         returns - (timestamp, bsig, csig)
113         """
114         entry = self.get_entry(filename)
115         return (entry.timestamp, entry.bsig, entry.csig)
116
117     def get_entry(self, filename):
118         """
119         Create an entry for the filename and return it, or if one already exists,
120         then return it.
121         """
122         try:
123             return self.entries[filename]
124         except (KeyError, AttributeError):
125             return SConsignEntry()
126
127     def set_entry(self, filename, entry):
128         """
129         Set the entry.
130         """
131         self.entries[filename] = entry
132         self.dirty = 1
133
134     def set_csig(self, filename, csig):
135         """
136         Set the csig .sconsign entry for a file
137
138         filename - the filename whose signature will be set
139         csig - the file's content signature
140         """
141
142         entry = self.get_entry(filename)
143         entry.csig = csig
144         self.set_entry(filename, entry)
145
146     def set_binfo(self, filename, bsig, bkids, bkidsigs, bact, bactsig):
147         """
148         Set the build info .sconsign entry for a file
149
150         filename - the filename whose signature will be set
151         bsig - the file's built signature
152         """
153
154         entry = self.get_entry(filename)
155         entry.bsig = bsig
156         entry.bkids = bkids
157         entry.bkidsigs = bkidsigs
158         entry.bact = bact
159         entry.bactsig = bactsig
160         self.set_entry(filename, entry)
161
162     def set_timestamp(self, filename, timestamp):
163         """
164         Set the timestamp .sconsign entry for a file
165
166         filename - the filename whose signature will be set
167         timestamp - the file's timestamp
168         """
169
170         entry = self.get_entry(filename)
171         entry.timestamp = timestamp
172         self.set_entry(filename, entry)
173
174     def get_implicit(self, filename):
175         """Fetch the cached implicit dependencies for 'filename'"""
176         entry = self.get_entry(filename)
177         return entry.implicit
178
179     def set_implicit(self, filename, implicit):
180         """Cache the implicit dependencies for 'filename'."""
181         entry = self.get_entry(filename)
182         if not SCons.Util.is_List(implicit):
183             implicit = [implicit]
184         implicit = map(str, implicit)
185         entry.implicit = implicit
186         self.set_entry(filename, entry)
187
188     def get_binfo(self, filename):
189         """Fetch the cached implicit dependencies for 'filename'"""
190         entry = self.get_entry(filename)
191         return entry.bsig, entry.bkids, entry.bkidsigs, entry.bact, entry.bactsig
192
193 class SConsignDB(_SConsign):
194     """
195     A _SConsign subclass that reads and writes signature information
196     from a global .sconsign.dbm file.
197     """
198     def __init__(self, dir, module=None):
199         _SConsign.__init__(self, module)
200
201         self.dir = dir
202
203         try:
204             global SConsign_db
205             rawentries = SConsign_db[self.dir.path]
206         except KeyError:
207             pass
208         else:
209             try:
210                 self.entries = cPickle.loads(rawentries)
211                 if type(self.entries) is not type({}):
212                     self.entries = {}
213                     raise TypeError
214             except KeyboardInterrupt:
215                 raise
216             except:
217                 SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
218                                     "Ignoring corrupt sconsign entry : %s"%self.dir.path)
219
220         global sig_files
221         sig_files.append(self)
222
223     def write(self):
224         if self.dirty:
225             global SConsign_db
226             SConsign_db[self.dir.path] = cPickle.dumps(self.entries, 1)
227             try:
228                 SConsign_db.sync()
229             except AttributeError:
230                 # Not all anydbm modules have sync() methods.
231                 pass
232
233 class SConsignDir(_SConsign):
234     def __init__(self, fp=None, module=None):
235         """
236         fp - file pointer to read entries from
237         module - the signature module being used
238         """
239         _SConsign.__init__(self, module)
240
241         if fp:
242             self.entries = cPickle.load(fp)
243             if type(self.entries) is not type({}):
244                 self.entries = {}
245                 raise TypeError
246
247 class SConsignDirFile(SConsignDir):
248     """
249     Encapsulates reading and writing a per-directory .sconsign file.
250     """
251     def __init__(self, dir, module=None):
252         """
253         dir - the directory for the file
254         module - the signature module being used
255         """
256
257         self.dir = dir
258         self.sconsign = os.path.join(dir.path, '.sconsign')
259
260         try:
261             fp = open(self.sconsign, 'rb')
262         except IOError:
263             fp = None
264
265         try:
266             SConsignDir.__init__(self, fp, module)
267         except KeyboardInterrupt:
268             raise
269         except:
270             SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
271                                 "Ignoring corrupt .sconsign file: %s"%self.sconsign)
272
273         global sig_files
274         sig_files.append(self)
275
276     def write(self):
277         """
278         Write the .sconsign file to disk.
279
280         Try to write to a temporary file first, and rename it if we
281         succeed.  If we can't write to the temporary file, it's
282         probably because the directory isn't writable (and if so,
283         how did we build anything in this directory, anyway?), so
284         try to write directly to the .sconsign file as a backup.
285         If we can't rename, try to copy the temporary contents back
286         to the .sconsign file.  Either way, always try to remove
287         the temporary file at the end.
288         """
289         if self.dirty:
290             temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
291             try:
292                 file = open(temp, 'wb')
293                 fname = temp
294             except IOError:
295                 try:
296                     file = open(self.sconsign, 'wb')
297                     fname = self.sconsign
298                 except IOError:
299                     return
300             cPickle.dump(self.entries, file, 1)
301             file.close()
302             if fname != self.sconsign:
303                 try:
304                     mode = os.stat(self.sconsign)[0]
305                     os.chmod(self.sconsign, 0666)
306                     os.unlink(self.sconsign)
307                 except OSError:
308                     pass
309                 try:
310                     os.rename(fname, self.sconsign)
311                 except OSError:
312                     open(self.sconsign, 'wb').write(open(fname, 'rb').read())
313                     os.chmod(self.sconsign, mode)
314             try:
315                 os.unlink(temp)
316             except OSError:
317                 pass
318
319 SConsignForDirectory = SConsignDirFile
320
321 def SConsignFile(name, dbm_module=None):
322     """
323     Arrange for all signatures to be stored in a global .sconsign.dbm
324     file.
325     """
326     global SConsign_db
327     if SConsign_db is None:
328         if dbm_module is None:
329             import SCons.dblite
330             dbm_module = SCons.dblite
331         SConsign_db = dbm_module.open(name, "c")
332
333     global SConsignForDirectory
334     SConsignForDirectory = SConsignDB
335
336 class Calculator:
337     """
338     Encapsulates signature calculations and .sconsign file generating
339     for the build engine.
340     """
341
342     def __init__(self, module=default_module, max_drift=default_max_drift):
343         """
344         Initialize the calculator.
345
346         module - the signature module to use for signature calculations
347         max_drift - the maximum system clock drift used to determine when to
348           cache content signatures. A negative value means to never cache
349           content signatures. (defaults to 2 days)
350         """
351         self.module = module
352         self.max_drift = max_drift
353
354     def bsig(self, node, cache=None):
355         """
356         Generate a node's build signature, the digested signatures
357         of its dependency files and build information.
358
359         node - the node whose sources will be collected
360         cache - alternate node to use for the signature cache
361         returns - the build signature
362
363         This no longer handles the recursive descent of the
364         node's children's signatures.  We expect that they're
365         already built and updated by someone else, if that's
366         what's wanted.
367         """
368
369         if cache is None: cache = node
370
371         bsig = cache.get_bsig()
372         if bsig is not None:
373             return bsig
374
375         children = node.children()
376         bkids = map(str, children)
377
378         # double check bsig, because the call to children() above may
379         # have set it:
380         bsig = cache.get_bsig()
381         if bsig is not None:
382             return bsig
383
384         sigs = map(lambda n, c=self: n.calc_signature(c), children)
385
386         if node.has_builder():
387             executor = node.get_executor()
388             bact = str(executor)
389             bactsig = self.module.signature(executor)
390             sigs.append(bactsig)
391         else:
392             bact = ""
393             bactsig = ""
394
395         bsig = self.module.collect(filter(None, sigs))
396
397         cache.set_binfo(bsig, bkids, sigs, bact, bactsig)
398
399         # don't store the bsig here, because it isn't accurate until
400         # the node is actually built.
401
402         return bsig
403
404     def csig(self, node, cache=None):
405         """
406         Generate a node's content signature, the digested signature
407         of its content.
408
409         node - the node
410         cache - alternate node to use for the signature cache
411         returns - the content signature
412         """
413
414         if cache is None: cache = node
415
416         csig = cache.get_csig()
417         if csig is not None:
418             return csig
419         
420         if self.max_drift >= 0:
421             oldtime, oldbsig, oldcsig = node.get_prevsiginfo()
422         else:
423             oldtime, oldbsig, oldcsig = _SConsign.null_siginfo
424
425         mtime = node.get_timestamp()
426
427         if (oldtime and oldcsig and oldtime == mtime):
428             # use the signature stored in the .sconsign file
429             csig = oldcsig
430             # Set the csig here so it doesn't get recalculated unnecessarily
431             # and so it's set when the .sconsign file gets written
432             cache.set_csig(csig)
433         else:
434             csig = self.module.signature(node)
435             # Set the csig here so it doesn't get recalculated unnecessarily
436             # and so it's set when the .sconsign file gets written
437             cache.set_csig(csig)
438
439             if self.max_drift >= 0 and (time.time() - mtime) > self.max_drift:
440                 node.store_csig()
441                 node.store_timestamp()
442
443         return csig
444
445     def current(self, node, newsig):
446         """
447         Check if a signature is up to date with respect to a node.
448
449         node - the node whose signature will be checked
450         newsig - the (presumably current) signature of the file
451
452         returns - 1 if the file is current with the specified signature,
453         0 if it isn't
454         """
455
456         if node.always_build:
457             return 0
458         
459         oldtime, oldbsig, oldcsig = node.get_prevsiginfo()
460
461         if not node.has_builder() and node.get_timestamp() == oldtime:
462             return 1
463
464         return self.module.current(newsig, oldbsig)
465
466
467 default_calc = Calculator()