siggen: Remove fsync that is causing performance issues
[bitbake.git] / lib / bb / siggen.py
1 import hashlib
2 import logging
3 import os
4 import re
5 import tempfile
6 import bb.data
7
8 logger = logging.getLogger('BitBake.SigGen')
9
10 try:
11     import cPickle as pickle
12 except ImportError:
13     import pickle
14     logger.info('Importing cPickle failed.  Falling back to a very slow implementation.')
15
16 def init(d):
17     siggens = [obj for obj in globals().itervalues()
18                       if type(obj) is type and issubclass(obj, SignatureGenerator)]
19
20     desired = d.getVar("BB_SIGNATURE_HANDLER", True) or "noop"
21     for sg in siggens:
22         if desired == sg.name:
23             return sg(d)
24             break
25     else:
26         logger.error("Invalid signature generator '%s', using default 'noop'\n"
27                      "Available generators: %s", desired,
28                      ', '.join(obj.name for obj in siggens))
29         return SignatureGenerator(d)
30
31 class SignatureGenerator(object):
32     """
33     """
34     name = "noop"
35
36     def __init__(self, data):
37         return
38
39     def finalise(self, fn, d, varient):
40         return
41
42     def get_taskhash(self, fn, task, deps, dataCache):
43         return "0"
44
45     def set_taskdata(self, hashes, deps):
46         return
47
48     def stampfile(self, stampbase, file_name, taskname, extrainfo):
49         return ("%s.%s.%s" % (stampbase, taskname, extrainfo)).rstrip('.')
50
51     def stampcleanmask(self, stampbase, file_name, taskname, extrainfo):
52         return ("%s.%s.%s" % (stampbase, taskname, extrainfo)).rstrip('.')
53
54     def dump_sigtask(self, fn, task, stampbase, runtime):
55         return
56
57     def invalidate_task(self, task, d, fn):
58         bb.build.del_stamp(task, d, fn)
59
60
61 class SignatureGeneratorBasic(SignatureGenerator):
62     """
63     """
64     name = "basic"
65
66     def __init__(self, data):
67         self.basehash = {}
68         self.taskhash = {}
69         self.taskdeps = {}
70         self.runtaskdeps = {}
71         self.file_checksum_values = {}
72         self.gendeps = {}
73         self.lookupcache = {}
74         self.pkgnameextract = re.compile("(?P<fn>.*)\..*")
75         self.basewhitelist = set((data.getVar("BB_HASHBASE_WHITELIST", True) or "").split())
76         self.taskwhitelist = None
77         self.init_rundepcheck(data)
78
79     def init_rundepcheck(self, data):
80         self.taskwhitelist = data.getVar("BB_HASHTASK_WHITELIST", True) or None
81         if self.taskwhitelist:
82             self.twl = re.compile(self.taskwhitelist)
83         else:
84             self.twl = None
85
86     def _build_data(self, fn, d):
87
88         tasklist, gendeps, lookupcache = bb.data.generate_dependencies(d)
89
90         taskdeps = {}
91         basehash = {}
92
93         for task in tasklist:
94             data = lookupcache[task]
95
96             if data is None:
97                 bb.error("Task %s from %s seems to be empty?!" % (task, fn))
98                 data = ''
99
100             gendeps[task] -= self.basewhitelist
101             newdeps = gendeps[task]
102             seen = set()
103             while newdeps:
104                 nextdeps = newdeps
105                 seen |= nextdeps
106                 newdeps = set()
107                 for dep in nextdeps:
108                     if dep in self.basewhitelist:
109                         continue
110                     gendeps[dep] -= self.basewhitelist
111                     newdeps |= gendeps[dep]
112                 newdeps -= seen
113
114             alldeps = sorted(seen)
115             for dep in alldeps:
116                 data = data + dep
117                 var = lookupcache[dep]
118                 if var is not None:
119                     data = data + str(var)
120             self.basehash[fn + "." + task] = hashlib.md5(data).hexdigest()
121             taskdeps[task] = alldeps
122
123         self.taskdeps[fn] = taskdeps
124         self.gendeps[fn] = gendeps
125         self.lookupcache[fn] = lookupcache
126
127         return taskdeps
128
129     def finalise(self, fn, d, variant):
130
131         if variant:
132             fn = "virtual:" + variant + ":" + fn
133
134         try:
135             taskdeps = self._build_data(fn, d)
136         except:
137             bb.note("Error during finalise of %s" % fn)
138             raise
139
140         #Slow but can be useful for debugging mismatched basehashes
141         #for task in self.taskdeps[fn]:
142         #    self.dump_sigtask(fn, task, d.getVar("STAMP", True), False)
143
144         for task in taskdeps:
145             d.setVar("BB_BASEHASH_task-%s" % task, self.basehash[fn + "." + task])
146
147     def rundep_check(self, fn, recipename, task, dep, depname, dataCache):
148         # Return True if we should keep the dependency, False to drop it
149         # We only manipulate the dependencies for packages not in the whitelist
150         if self.twl and not self.twl.search(recipename):
151             # then process the actual dependencies
152             if self.twl.search(depname):
153                 return False
154         return True
155
156     def read_taint(self, fn, task, stampbase):
157         taint = None
158         try:
159             with open(stampbase + '.' + task + '.taint', 'r') as taintf:
160                 taint = taintf.read()
161         except IOError:
162             pass
163         return taint
164
165     def get_taskhash(self, fn, task, deps, dataCache):
166         k = fn + "." + task
167         data = dataCache.basetaskhash[k]
168         self.runtaskdeps[k] = []
169         self.file_checksum_values[k] = {}
170         recipename = dataCache.pkg_fn[fn]
171         for dep in sorted(deps, key=clean_basepath):
172             depname = dataCache.pkg_fn[self.pkgnameextract.search(dep).group('fn')]
173             if not self.rundep_check(fn, recipename, task, dep, depname, dataCache):
174                 continue
175             if dep not in self.taskhash:
176                 bb.fatal("%s is not in taskhash, caller isn't calling in dependency order?", dep)
177             data = data + self.taskhash[dep]
178             self.runtaskdeps[k].append(dep)
179
180         if task in dataCache.file_checksums[fn]:
181             checksums = bb.fetch2.get_file_checksums(dataCache.file_checksums[fn][task], recipename)
182             for (f,cs) in checksums:
183                 self.file_checksum_values[k][f] = cs
184                 data = data + cs
185
186         taint = self.read_taint(fn, task, dataCache.stamp[fn])
187         if taint:
188             data = data + taint
189
190         h = hashlib.md5(data).hexdigest()
191         self.taskhash[k] = h
192         #d.setVar("BB_TASKHASH_task-%s" % task, taskhash[task])
193         return h
194
195     def set_taskdata(self, hashes, deps, checksums):
196         self.runtaskdeps = deps
197         self.taskhash = hashes
198         self.file_checksum_values = checksums
199
200     def dump_sigtask(self, fn, task, stampbase, runtime):
201         k = fn + "." + task
202         if runtime == "customfile":
203             sigfile = stampbase
204         elif runtime and k in self.taskhash:
205             sigfile = stampbase + "." + task + ".sigdata" + "." + self.taskhash[k]
206         else:
207             sigfile = stampbase + "." + task + ".sigbasedata" + "." + self.basehash[k]
208
209         bb.utils.mkdirhier(os.path.dirname(sigfile))
210
211         data = {}
212         data['basewhitelist'] = self.basewhitelist
213         data['taskwhitelist'] = self.taskwhitelist
214         data['taskdeps'] = self.taskdeps[fn][task]
215         data['basehash'] = self.basehash[k]
216         data['gendeps'] = {}
217         data['varvals'] = {}
218         data['varvals'][task] = self.lookupcache[fn][task]
219         for dep in self.taskdeps[fn][task]:
220             if dep in self.basewhitelist:
221                 continue
222             data['gendeps'][dep] = self.gendeps[fn][dep]
223             data['varvals'][dep] = self.lookupcache[fn][dep]
224
225         if runtime and k in self.taskhash:
226             data['runtaskdeps'] = self.runtaskdeps[k]
227             data['file_checksum_values'] = [(os.path.basename(f), cs) for f,cs in self.file_checksum_values[k].items()]
228             data['runtaskhashes'] = {}
229             for dep in data['runtaskdeps']:
230                 data['runtaskhashes'][dep] = self.taskhash[dep]
231
232         taint = self.read_taint(fn, task, stampbase)
233         if taint:
234             data['taint'] = taint
235
236         fd, tmpfile = tempfile.mkstemp(dir=os.path.dirname(sigfile), prefix="sigtask.")
237         try:
238             with os.fdopen(fd, "wb") as stream:
239                 p = pickle.dump(data, stream, -1)
240                 stream.flush()
241             os.chmod(tmpfile, 0664)
242             os.rename(tmpfile, sigfile)
243         except (OSError, IOError) as err:
244             try:
245                 os.unlink(tmpfile)
246             except OSError:
247                 pass
248             raise err
249
250     def dump_sigs(self, dataCache):
251         for fn in self.taskdeps:
252             for task in self.taskdeps[fn]:
253                 k = fn + "." + task
254                 if k not in self.taskhash:
255                     continue
256                 if dataCache.basetaskhash[k] != self.basehash[k]:
257                     bb.error("Bitbake's cached basehash does not match the one we just generated (%s)!" % k)
258                     bb.error("The mismatched hashes were %s and %s" % (dataCache.basetaskhash[k], self.basehash[k]))
259                 self.dump_sigtask(fn, task, dataCache.stamp[fn], True)
260
261 class SignatureGeneratorBasicHash(SignatureGeneratorBasic):
262     name = "basichash"
263
264     def stampfile(self, stampbase, fn, taskname, extrainfo, clean=False):
265         if taskname != "do_setscene" and taskname.endswith("_setscene"):
266             k = fn + "." + taskname[:-9]
267         else:
268             k = fn + "." + taskname
269         if clean:
270             h = "*"
271         elif k in self.taskhash:
272             h = self.taskhash[k]
273         else:
274             # If k is not in basehash, then error
275             h = self.basehash[k]
276         return ("%s.%s.%s.%s" % (stampbase, taskname, h, extrainfo)).rstrip('.')
277
278     def stampcleanmask(self, stampbase, fn, taskname, extrainfo):
279         return self.stampfile(stampbase, fn, taskname, extrainfo, clean=True)
280         
281     def invalidate_task(self, task, d, fn):
282         bb.note("Tainting hash to force rebuild of task %s, %s" % (fn, task))
283         bb.build.write_taint(task, d, fn)
284
285 def dump_this_task(outfile, d):
286     import bb.parse
287     fn = d.getVar("BB_FILENAME", True)
288     task = "do_" + d.getVar("BB_CURRENTTASK", True)
289     bb.parse.siggen.dump_sigtask(fn, task, outfile, "customfile")
290
291 def clean_basepath(a):
292     if a.startswith("virtual:"):
293         b = a.rsplit(":", 1)[0] + ":" + a.rsplit("/", 1)[1]
294     else:
295         b = a.rsplit("/", 1)[1]
296     return b
297
298 def clean_basepaths(a):
299     b = {}
300     for x in a:
301         b[clean_basepath(x)] = a[x]
302     return b
303
304 def compare_sigfiles(a, b, recursecb = None):
305     output = []
306
307     p1 = pickle.Unpickler(open(a, "rb"))
308     a_data = p1.load()
309     p2 = pickle.Unpickler(open(b, "rb"))
310     b_data = p2.load()
311
312     def dict_diff(a, b, whitelist=set()):
313         sa = set(a.keys())
314         sb = set(b.keys())
315         common = sa & sb
316         changed = set()
317         for i in common:
318             if a[i] != b[i] and i not in whitelist:
319                 changed.add(i)
320         added = sb - sa
321         removed = sa - sb
322         return changed, added, removed
323
324     def file_checksums_diff(a, b):
325         from collections import Counter
326         # Handle old siginfo format
327         if isinstance(a, dict):
328             a = [(os.path.basename(f), cs) for f, cs in a.items()]
329         if isinstance(b, dict):
330             b = [(os.path.basename(f), cs) for f, cs in b.items()]
331         # Compare lists, ensuring we can handle duplicate filenames if they exist
332         removedcount = Counter(a)
333         removedcount.subtract(b)
334         addedcount = Counter(b)
335         addedcount.subtract(a)
336         added = []
337         for x in b:
338             if addedcount[x] > 0:
339                 addedcount[x] -= 1
340                 added.append(x)
341         removed = []
342         changed = []
343         for x in a:
344             if removedcount[x] > 0:
345                 removedcount[x] -= 1
346                 for y in added:
347                     if y[0] == x[0]:
348                         changed.append((x[0], x[1], y[1]))
349                         added.remove(y)
350                         break
351                 else:
352                     removed.append(x)
353         added = [x[0] for x in added]
354         removed = [x[0] for x in removed]
355         return changed, added, removed
356
357     if 'basewhitelist' in a_data and a_data['basewhitelist'] != b_data['basewhitelist']:
358         output.append("basewhitelist changed from '%s' to '%s'" % (a_data['basewhitelist'], b_data['basewhitelist']))
359         if a_data['basewhitelist'] and b_data['basewhitelist']:
360             output.append("changed items: %s" % a_data['basewhitelist'].symmetric_difference(b_data['basewhitelist']))
361
362     if 'taskwhitelist' in a_data and a_data['taskwhitelist'] != b_data['taskwhitelist']:
363         output.append("taskwhitelist changed from '%s' to '%s'" % (a_data['taskwhitelist'], b_data['taskwhitelist']))
364         if a_data['taskwhitelist'] and b_data['taskwhitelist']:
365             output.append("changed items: %s" % a_data['taskwhitelist'].symmetric_difference(b_data['taskwhitelist']))
366
367     if a_data['taskdeps'] != b_data['taskdeps']:
368         output.append("Task dependencies changed from:\n%s\nto:\n%s" % (sorted(a_data['taskdeps']), sorted(b_data['taskdeps'])))
369
370     if a_data['basehash'] != b_data['basehash']:
371         output.append("basehash changed from %s to %s" % (a_data['basehash'], b_data['basehash']))
372
373     changed, added, removed = dict_diff(a_data['gendeps'], b_data['gendeps'], a_data['basewhitelist'] & b_data['basewhitelist'])
374     if changed:
375         for dep in changed:
376             output.append("List of dependencies for variable %s changed from '%s' to '%s'" % (dep, a_data['gendeps'][dep], b_data['gendeps'][dep]))
377             if a_data['gendeps'][dep] and b_data['gendeps'][dep]:
378                 output.append("changed items: %s" % a_data['gendeps'][dep].symmetric_difference(b_data['gendeps'][dep]))
379     if added:
380         for dep in added:
381             output.append("Dependency on variable %s was added" % (dep))
382     if removed:
383         for dep in removed:
384             output.append("Dependency on Variable %s was removed" % (dep))
385
386
387     changed, added, removed = dict_diff(a_data['varvals'], b_data['varvals'])
388     if changed:
389         for dep in changed:
390             output.append("Variable %s value changed from '%s' to '%s'" % (dep, a_data['varvals'][dep], b_data['varvals'][dep]))
391
392     changed, added, removed = file_checksums_diff(a_data['file_checksum_values'], b_data['file_checksum_values'])
393     if changed:
394         for f, old, new in changed:
395             output.append("Checksum for file %s changed from %s to %s" % (f, old, new))
396     if added:
397         for f in added:
398             output.append("Dependency on checksum of file %s was added" % (f))
399     if removed:
400         for f in removed:
401             output.append("Dependency on checksum of file %s was removed" % (f))
402
403
404     if 'runtaskhashes' in a_data and 'runtaskhashes' in b_data:
405         a = a_data['runtaskhashes']
406         b = b_data['runtaskhashes']
407         changed, added, removed = dict_diff(a, b)
408         if added:
409             for dep in added:
410                 bdep_found = False
411                 if removed:
412                     for bdep in removed:
413                         if b[dep] == a[bdep]:
414                             #output.append("Dependency on task %s was replaced by %s with same hash" % (dep, bdep))
415                             bdep_found = True
416                 if not bdep_found:
417                     output.append("Dependency on task %s was added with hash %s" % (clean_basepath(dep), b[dep]))
418         if removed:
419             for dep in removed:
420                 adep_found = False
421                 if added:
422                     for adep in added:
423                         if b[adep] == a[dep]:
424                             #output.append("Dependency on task %s was replaced by %s with same hash" % (adep, dep))
425                             adep_found = True
426                 if not adep_found:
427                     output.append("Dependency on task %s was removed with hash %s" % (clean_basepath(dep), a[dep]))
428         if changed:
429             for dep in changed:
430                 output.append("Hash for dependent task %s changed from %s to %s" % (clean_basepath(dep), a[dep], b[dep]))
431                 if callable(recursecb):
432                     # If a dependent hash changed, might as well print the line above and then defer to the changes in 
433                     # that hash since in all likelyhood, they're the same changes this task also saw.
434                     recout = recursecb(dep, a[dep], b[dep])
435                     if recout:
436                         output = [output[-1]] + recout
437
438     a_taint = a_data.get('taint', None)
439     b_taint = b_data.get('taint', None)
440     if a_taint != b_taint:
441         output.append("Taint (by forced/invalidated task) changed from %s to %s" % (a_taint, b_taint))
442
443     return output
444
445
446 def dump_sigfile(a):
447     output = []
448
449     p1 = pickle.Unpickler(open(a, "rb"))
450     a_data = p1.load()
451
452     output.append("basewhitelist: %s" % (a_data['basewhitelist']))
453
454     output.append("taskwhitelist: %s" % (a_data['taskwhitelist']))
455
456     output.append("Task dependencies: %s" % (sorted(a_data['taskdeps'])))
457
458     output.append("basehash: %s" % (a_data['basehash']))
459
460     for dep in a_data['gendeps']:
461         output.append("List of dependencies for variable %s is %s" % (dep, a_data['gendeps'][dep]))
462
463     for dep in a_data['varvals']:
464         output.append("Variable %s value is %s" % (dep, a_data['varvals'][dep]))
465
466     if 'runtaskdeps' in a_data:
467         output.append("Tasks this task depends on: %s" % (a_data['runtaskdeps']))
468
469     if 'file_checksum_values' in a_data:
470         output.append("This task depends on the checksums of files: %s" % (a_data['file_checksum_values']))
471
472     if 'runtaskhashes' in a_data:
473         for dep in a_data['runtaskhashes']:
474             output.append("Hash for dependent task %s is %s" % (dep, a_data['runtaskhashes'][dep]))
475
476     if 'taint' in a_data:
477         output.append("Tainted (by forced/invalidated task): %s" % a_data['taint'])
478
479     return output