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