Use pickle for .sconsign. (Anthony Roach)
[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 import SCons.Warnings
38 import cPickle
39
40 try:
41     import MD5
42     default_module = MD5
43 except ImportError:
44     import TimeStamp
45     default_module = TimeStamp
46
47 #XXX Get rid of the global array so this becomes re-entrant.
48 sig_files = []
49
50 # 1 means use build signature for derived source files
51 # 0 means use content signature for derived source files
52 build_signature = 1
53
54 def write():
55     global sig_files
56     for sig_file in sig_files:
57         sig_file.write()
58
59 class SConsignEntry:
60
61     """Objects of this type are pickled to the .sconsign file, so it
62     should only contain simple builtin Python datatypes and no methods.
63
64     This class is used to store cache information about nodes between
65     scons runs for efficiency, and to store the build signature for
66     nodes so that scons can determine if they are out of date. """
67
68     # setup the default value for various attributes:
69     # (We make the class variables so the default values won't get pickled
70     # with the instances, which would waste a lot of space)
71     timestamp = None
72     bsig = None
73     csig = None
74     implicit = None
75
76 class SConsignFile:
77     """
78     Encapsulates reading and writing a .sconsign file.
79     """
80
81     def __init__(self, dir, module=None):
82         """
83         dir - the directory for the file
84         module - the signature module being used
85         """
86
87         self.dir = dir
88
89         if module is None:
90             self.module = default_calc.module
91         else:
92             self.module = module
93         self.sconsign = os.path.join(dir.path, '.sconsign')
94         self.entries = {}
95         self.dirty = 0
96
97         try:
98             file = open(self.sconsign, 'rb')
99         except:
100             pass
101         else:
102             try:
103                 self.entries = cPickle.load(file)
104                 if type(self.entries) is not type({}):
105                     self.entries = {}
106                     raise TypeError
107             except:
108                 SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
109                                     "Ignoring corrupt .sconsign file: %s"%self.sconsign)
110
111         global sig_files
112         sig_files.append(self)
113
114     def get(self, filename):
115         """
116         Get the .sconsign entry for a file
117
118         filename - the filename whose signature will be returned
119         returns - (timestamp, bsig, csig)
120         """
121         entry = self.get_entry(filename)
122         return (entry.timestamp, entry.bsig, entry.csig)
123
124     def get_entry(self, filename):
125         """
126         Create an entry for the filename and return it, or if one already exists,
127         then return it.
128         """
129         try:
130             return self.entries[filename]
131         except:
132             return SConsignEntry()
133
134     def set_entry(self, filename, entry):
135         """
136         Set the entry.
137         """
138         self.entries[filename] = entry
139         self.dirty = 1
140
141     def set_csig(self, filename, csig):
142         """
143         Set the csig .sconsign entry for a file
144
145         filename - the filename whose signature will be set
146         csig - the file's content signature
147         """
148
149         entry = self.get_entry(filename)
150         entry.csig = csig
151         self.set_entry(filename, entry)
152
153     def set_bsig(self, filename, bsig):
154         """
155         Set the csig .sconsign entry for a file
156
157         filename - the filename whose signature will be set
158         bsig - the file's built signature
159         """
160
161         entry = self.get_entry(filename)
162         entry.bsig = bsig
163         self.set_entry(filename, entry)
164
165     def set_timestamp(self, filename, timestamp):
166         """
167         Set the csig .sconsign entry for a file
168
169         filename - the filename whose signature will be set
170         timestamp - the file's timestamp
171         """
172
173         entry = self.get_entry(filename)
174         entry.timestamp = timestamp
175         self.set_entry(filename, entry)
176
177     def get_implicit(self, filename):
178         """Fetch the cached implicit dependencies for 'filename'"""
179         entry = self.get_entry(filename)
180         return entry.implicit
181
182     def set_implicit(self, filename, implicit):
183         """Cache the implicit dependencies for 'filename'."""
184         entry = self.get_entry(filename)
185         if SCons.Util.is_String(implicit):
186             implicit = [implicit]
187         implicit = map(str, implicit)
188         entry.implicit = implicit
189         self.set_entry(filename, entry)
190
191     def write(self):
192         """
193         Write the .sconsign file to disk.
194
195         Try to write to a temporary file first, and rename it if we
196         succeed.  If we can't write to the temporary file, it's
197         probably because the directory isn't writable (and if so,
198         how did we build anything in this directory, anyway?), so
199         try to write directly to the .sconsign file as a backup.
200         If we can't rename, try to copy the temporary contents back
201         to the .sconsign file.  Either way, always try to remove
202         the temporary file at the end.
203         """
204         if self.dirty:
205             temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
206             try:
207                 file = open(temp, 'wb')
208                 fname = temp
209             except:
210                 try:
211                     file = open(self.sconsign, 'wb')
212                     fname = self.sconsign
213                 except:
214                     return
215             cPickle.dump(self.entries, file, 1)
216             file.close()
217             if fname != self.sconsign:
218                 try:
219                     mode = os.stat(self.sconsign)[0]
220                     os.chmod(self.sconsign, 0666)
221                     os.unlink(self.sconsign)
222                 except:
223                     pass
224                 try:
225                     os.rename(fname, self.sconsign)
226                 except:
227                     open(self.sconsign, 'wb').write(open(fname, 'rb').read())
228                     os.chmod(self.sconsign, mode)
229             try:
230                 os.unlink(temp)
231             except:
232                 pass
233
234 class Calculator:
235     """
236     Encapsulates signature calculations and .sconsign file generating
237     for the build engine.
238     """
239
240     def __init__(self, module=default_module, max_drift=2*24*60*60):
241         """
242         Initialize the calculator.
243
244         module - the signature module to use for signature calculations
245         max_drift - the maximum system clock drift used to determine when to
246           cache content signatures. A negative value means to never cache
247           content signatures. (defaults to 2 days)
248         """
249         self.module = module
250         self.max_drift = max_drift
251
252     def bsig(self, node):
253         """
254         Generate a node's build signature, the digested signatures
255         of its dependency files and build information.
256
257         node - the node whose sources will be collected
258         returns - the build signature
259
260         This no longer handles the recursive descent of the
261         node's children's signatures.  We expect that they're
262         already built and updated by someone else, if that's
263         what's wanted.
264         """
265         sigs = map(lambda n, c=self: n.calc_signature(c), node.children())
266         if node.builder:
267             sigs.append(self.module.signature(node.builder_sig_adapter()))
268
269         bsig = self.module.collect(filter(lambda x: not x is None, sigs))
270
271         node.set_bsig(bsig)
272
273         # don't store the bsig here, because it isn't accurate until
274         # the node is actually built.
275
276         return bsig
277
278     def csig(self, node):
279         """
280         Generate a node's content signature, the digested signature
281         of its content.
282
283         node - the node
284         returns - the content signature
285         """
286         if self.max_drift >= 0:
287             info = node.get_prevsiginfo()
288         else:
289             info = None
290
291         mtime = node.get_timestamp()
292
293         if (info and info[0] and info[2] and info[0] == mtime):
294             # use the signature stored in the .sconsign file
295             csig = info[2]
296             # Set the csig here so it doesn't get recalculated unnecessarily
297             # and so it's set when the .sconsign file gets written
298             node.set_csig(csig)
299         else:
300             csig = self.module.signature(node)
301             # Set the csig here so it doesn't get recalculated unnecessarily
302             # and so it's set when the .sconsign file gets written
303             node.set_csig(csig)
304
305             if self.max_drift >= 0 and (time.time() - mtime) > self.max_drift:
306                 node.store_csig()
307                 node.store_timestamp()
308
309         return csig
310
311     def current(self, node, newsig):
312         """
313         Check if a signature is up to date with respect to a node.
314
315         node - the node whose signature will be checked
316         newsig - the (presumably current) signature of the file
317
318         returns - 1 if the file is current with the specified signature,
319         0 if it isn't
320         """
321         oldtime, oldbsig, oldcsig = node.get_prevsiginfo()
322
323         if not node.builder and node.get_timestamp() == oldtime:
324             return 1
325
326         return self.module.current(newsig, oldbsig)
327
328
329 default_calc = Calculator()