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