Change the way implicit deps are stored to make it easier to maintain backward compat...
[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 (c) 2001, 2002 Steven Knight
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 os
33 import os.path
34 import string
35 import SCons.Node
36 import time
37
38 #XXX Get rid of the global array so this becomes re-entrant.
39 sig_files = []
40
41 def write():
42     global sig_files
43     for sig_file in sig_files:
44         sig_file.write()
45
46 class SConsignEntry:
47     def __init__(self, module, entry=None):
48
49         self.timestamp = self.csig = self.bsig = self.implicit = None
50
51         if not entry is None:
52             arr = map(string.strip, string.split(entry, " ", 3))
53
54             try:
55                 if arr[0] == '-': self.timestamp = None
56                 else:             self.timestamp = int(arr[0])
57
58                 if arr[1] == '-': self.bsig = None
59                 else:             self.bsig = module.from_string(arr[1])
60
61                 if arr[2] == '-': self.csig = None
62                 else:             self.csig = module.from_string(arr[2])
63
64                 if len(arr) < 4:        self.implicit = None # pre-0.07 format
65                 elif arr[3] == '':      self.implicit = '' # pre-0.08 format
66                 elif arr[3] == '-':     self.implicit = None
67                 else:                   self.implicit = string.replace(arr[3], '\0\0', '')
68             except IndexError:
69                 pass
70
71     def render(self, module):
72         if self.timestamp is None: timestamp = '-'
73         else:                      timestamp = "%d"%self.timestamp
74
75         if self.bsig is None: bsig = '-'
76         else:                 bsig = module.to_string(self.bsig)
77
78         if self.csig is None: csig = '-'
79         else:                 csig = module.to_string(self.csig)
80
81         if self.implicit is None: implicit = '-'
82         else:                     implicit = '\0\0%s\0\0'%self.implicit
83
84         return '%s %s %s %s' % (timestamp, bsig, csig, implicit)
85
86     def get_implicit(self):
87         if self.implicit is None:
88             return None
89         elif self.implicit == '':
90             return []
91         else:
92             return string.split(self.implicit, '\0')
93
94     def set_implicit(self, implicit):
95         if implicit is None:
96             self.implicit = None
97         else:
98             if SCons.Util.is_String(implicit):
99                 implicit = [implicit]
100             self.implicit = string.join(map(str, implicit), '\0')
101
102
103 class SConsignFile:
104     """
105     Encapsulates reading and writing a .sconsign file.
106     """
107
108     def __init__(self, dir, module=None):
109         """
110         dir - the directory for the file
111         module - the signature module being used
112         """
113
114         self.dir = dir
115
116         if module is None:
117             self.module = default_calc.module
118         else:
119             self.module = module
120         self.sconsign = os.path.join(dir.path, '.sconsign')
121         self.entries = {}
122         self.dirty = None
123
124         try:
125             file = open(self.sconsign, 'rt')
126         except:
127             pass
128         else:
129             for line in file.readlines():
130                 filename, rest = map(string.strip, string.split(line, ":", 1))
131                 self.entries[filename] = SConsignEntry(self.module, rest)
132
133         global sig_files
134         sig_files.append(self)
135
136     def get(self, filename):
137         """
138         Get the .sconsign entry for a file
139
140         filename - the filename whose signature will be returned
141         returns - (timestamp, bsig, csig)
142         """
143         try:
144             entry = self.entries[filename]
145             return (entry.timestamp, entry.bsig, entry.csig)
146         except KeyError:
147             return (None, None, None)
148     
149     def create_entry(self, filename):
150         """
151         Create an entry for the filename and return it, or if one already exists,
152         then return it.
153         """
154         try:
155             entry = self.entries[filename]
156         except KeyError:
157             entry = SConsignEntry(self.module)
158             self.entries[filename] = entry
159             
160         return entry
161
162     def set_csig(self, filename, csig):
163         """
164         Set the csig .sconsign entry for a file
165
166         filename - the filename whose signature will be set
167         csig - the file's content signature
168         """
169
170         entry = self.create_entry(filename)
171         entry.csig = csig
172         self.dirty = 1
173
174     def set_bsig(self, filename, bsig):
175         """
176         Set the csig .sconsign entry for a file
177
178         filename - the filename whose signature will be set
179         bsig - the file's built signature
180         """
181
182         entry = self.create_entry(filename)
183         entry.bsig = bsig
184         self.dirty = 1
185
186     def set_timestamp(self, filename, timestamp):
187         """
188         Set the csig .sconsign entry for a file
189
190         filename - the filename whose signature will be set
191         timestamp - the file's timestamp
192         """
193
194         entry = self.create_entry(filename)
195         entry.timestamp = timestamp
196         self.dirty = 1
197
198     def get_implicit(self, filename):
199         """Fetch the cached implicit dependencies for 'filename'"""
200         try:
201             entry = self.entries[filename]
202             return entry.get_implicit()
203         except KeyError:
204             return None
205
206     def set_implicit(self, filename, implicit):
207         """Cache the implicit dependencies for 'filename'."""
208         entry = self.create_entry(filename)
209         entry.set_implicit(implicit)
210         self.dirty = 1
211
212     def write(self):
213         """
214         Write the .sconsign file to disk.
215
216         Try to write to a temporary file first, and rename it if we
217         succeed.  If we can't write to the temporary file, it's
218         probably because the directory isn't writable (and if so,
219         how did we build anything in this directory, anyway?), so
220         try to write directly to the .sconsign file as a backup.
221         If we can't rename, try to copy the temporary contents back
222         to the .sconsign file.  Either way, always try to remove
223         the temporary file at the end.
224         """
225         if self.dirty:
226             temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
227             try:
228                 file = open(temp, 'wt')
229                 fname = temp
230             except:
231                 try:
232                     file = open(self.sconsign, 'wt')
233                     fname = self.sconsign
234                 except:
235                     return
236             keys = self.entries.keys()
237             keys.sort()
238             for name in keys:
239                 file.write("%s: %s\n" % (name, self.entries[name].render(self.module)))
240             file.close()
241             if fname != self.sconsign:
242                 try:
243                     mode = os.stat(self.sconsign)[0]
244                     os.chmod(self.sconsign, 0666)
245                     os.unlink(self.sconsign)
246                 except:
247                     pass
248                 try:
249                     os.rename(fname, self.sconsign)
250                 except:
251                     open(self.sconsign, 'wb').write(open(fname, 'rb').read())
252                     os.chmod(self.sconsign, mode)
253             try:
254                 os.unlink(temp)
255             except:
256                 pass
257
258
259 class Calculator:
260     """
261     Encapsulates signature calculations and .sconsign file generating
262     for the build engine.
263     """
264
265     def __init__(self, module=None, max_drift=2*24*60*60):
266         """
267         Initialize the calculator.
268
269         module - the signature module to use for signature calculations
270         max_drift - the maximum system clock drift used to determine when to
271           cache content signatures. A negative value means to never cache
272           content signatures. (defaults to 2 days)
273         """
274         if module is None:
275             try:
276                 import MD5
277                 self.module = MD5
278             except ImportError:
279                 # fallback on timestamp signatures if MD5 is not available
280                 # XXX add a warning message here
281                 import TimeStamp
282                 self.module = TimeStamp
283         else:
284             self.module = module
285         self.max_drift = max_drift
286
287     def bsig(self, node):
288         """
289         Generate a node's build signature, the digested signatures
290         of its dependency files and build information.
291
292         node - the node whose sources will be collected
293         returns - the build signature
294
295         This no longer handles the recursive descent of the
296         node's children's signatures.  We expect that they're
297         already built and updated by someone else, if that's
298         what's wanted.
299         """
300         if not node.use_signature:
301             return None
302
303         bsig = node.get_bsig()
304         if not bsig is None:
305             return bsig
306
307         sigs = map(self.get_signature, node.children())
308         if node.builder:
309             sigs.append(self.module.signature(node.builder_sig_adapter()))
310
311         bsig = self.module.collect(filter(lambda x: not x is None, sigs))
312
313         node.set_bsig(bsig)
314
315         # don't store the bsig here, because it isn't accurate until
316         # the node is actually built.
317
318         return bsig
319
320     def csig(self, node):
321         """
322         Generate a node's content signature, the digested signature
323         of its content.
324
325         node - the node
326         returns - the content signature
327         """
328         if not node.use_signature:
329             return None
330
331         csig = node.get_csig()
332         if not csig is None:
333             return csig
334
335         if self.max_drift >= 0:
336             info = node.get_prevsiginfo()
337         else:
338             info = None
339
340         mtime = node.get_timestamp()
341
342         if (info and info[0] and info[2] and info[0] == mtime):
343             # use the signature stored in the .sconsign file
344             csig = info[2]
345             # Set the csig here so it doesn't get recalculated unnecessarily
346             # and so it's set when the .sconsign file gets written
347             node.set_csig(csig)
348         else:
349             csig = self.module.signature(node)
350             # Set the csig here so it doesn't get recalculated unnecessarily
351             # and so it's set when the .sconsign file gets written
352             node.set_csig(csig)
353
354             if self.max_drift >= 0 and (time.time() - mtime) > self.max_drift:
355                 node.store_csig()
356                 node.store_timestamp()
357
358         return csig
359
360     def get_signature(self, node):
361         """
362         Get the appropriate signature for a node.
363
364         node - the node
365         returns - the signature or None if the signature could not
366         be computed.
367
368         This method does not store the signature in the node and
369         in the .sconsign file.
370         """
371
372         if not node.use_signature:
373             # This node type doesn't use a signature (e.g. a
374             # directory) so bail right away.
375             return None
376         elif node.builder:
377             return self.bsig(node)
378         elif not node.exists():
379             return None
380         else:
381             return self.csig(node)
382
383     def current(self, node, newsig):
384         """
385         Check if a signature is up to date with respect to a node.
386
387         node - the node whose signature will be checked
388         newsig - the (presumably current) signature of the file
389
390         returns - 1 if the file is current with the specified signature,
391         0 if it isn't
392         """
393
394         c = node.current()
395         if not c is None:
396             # The node itself has told us whether or not it's
397             # current without checking the signature.  The
398             # canonical uses here are a "0" return for a file
399             # that doesn't exist, or a directory.
400             return c
401
402         oldtime, oldbsig, oldcsig = node.get_prevsiginfo()
403
404         if not node.builder and node.get_timestamp() == oldtime:
405             return 1
406
407         return self.module.current(newsig, oldbsig)
408
409
410 default_calc = Calculator()