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