xref: /openbmc/openbmc/poky/bitbake/lib/bb/siggen.py (revision 44b3caf2)
1#
2# Copyright BitBake Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
7import hashlib
8import logging
9import os
10import re
11import tempfile
12import pickle
13import bb.data
14import difflib
15import simplediff
16import json
17import types
18from contextlib import contextmanager
19import bb.compress.zstd
20from bb.checksum import FileChecksumCache
21from bb import runqueue
22import hashserv
23import hashserv.client
24
25logger = logging.getLogger('BitBake.SigGen')
26hashequiv_logger = logging.getLogger('BitBake.SigGen.HashEquiv')
27
28#find_siginfo and find_siginfo_version are set by the metadata siggen
29# The minimum version of the find_siginfo function we need
30find_siginfo_minversion = 2
31
32HASHSERV_ENVVARS = [
33    "SSL_CERT_DIR",
34    "SSL_CERT_FILE",
35    "NO_PROXY",
36    "HTTPS_PROXY",
37    "HTTP_PROXY"
38]
39
40def check_siggen_version(siggen):
41    if not hasattr(siggen, "find_siginfo_version"):
42        bb.fatal("Siggen from metadata (OE-Core?) is too old, please update it (no version found)")
43    if siggen.find_siginfo_version < siggen.find_siginfo_minversion:
44        bb.fatal("Siggen from metadata (OE-Core?) is too old, please update it (%s vs %s)" % (siggen.find_siginfo_version, siggen.find_siginfo_minversion))
45
46class SetEncoder(json.JSONEncoder):
47    def default(self, obj):
48        if isinstance(obj, set) or isinstance(obj, frozenset):
49            return dict(_set_object=list(sorted(obj)))
50        return json.JSONEncoder.default(self, obj)
51
52def SetDecoder(dct):
53    if '_set_object' in dct:
54        return frozenset(dct['_set_object'])
55    return dct
56
57def init(d):
58    siggens = [obj for obj in globals().values()
59                      if type(obj) is type and issubclass(obj, SignatureGenerator)]
60
61    desired = d.getVar("BB_SIGNATURE_HANDLER") or "noop"
62    for sg in siggens:
63        if desired == sg.name:
64            return sg(d)
65    else:
66        logger.error("Invalid signature generator '%s', using default 'noop'\n"
67                     "Available generators: %s", desired,
68                     ', '.join(obj.name for obj in siggens))
69        return SignatureGenerator(d)
70
71class SignatureGenerator(object):
72    """
73    """
74    name = "noop"
75
76    def __init__(self, data):
77        self.basehash = {}
78        self.taskhash = {}
79        self.unihash = {}
80        self.runtaskdeps = {}
81        self.file_checksum_values = {}
82        self.taints = {}
83        self.unitaskhashes = {}
84        self.tidtopn = {}
85        self.setscenetasks = set()
86
87    def finalise(self, fn, d, varient):
88        return
89
90    def postparsing_clean_cache(self):
91        return
92
93    def setup_datacache(self, datacaches):
94        self.datacaches = datacaches
95
96    def setup_datacache_from_datastore(self, mcfn, d):
97        # In task context we have no cache so setup internal data structures
98        # from the fully parsed data store provided
99
100        mc = d.getVar("__BBMULTICONFIG", False) or ""
101        tasks = d.getVar('__BBTASKS', False)
102
103        self.datacaches = {}
104        self.datacaches[mc] = types.SimpleNamespace()
105        setattr(self.datacaches[mc], "stamp", {})
106        self.datacaches[mc].stamp[mcfn] = d.getVar('STAMP')
107        setattr(self.datacaches[mc], "stamp_extrainfo", {})
108        self.datacaches[mc].stamp_extrainfo[mcfn] = {}
109        for t in tasks:
110            flag = d.getVarFlag(t, "stamp-extra-info")
111            if flag:
112                self.datacaches[mc].stamp_extrainfo[mcfn][t] = flag
113
114    def get_cached_unihash(self, tid):
115        return None
116
117    def get_unihash(self, tid):
118        unihash = self.get_cached_unihash(tid)
119        if unihash:
120            return unihash
121        return self.taskhash[tid]
122
123    def get_unihashes(self, tids):
124        return {tid: self.get_unihash(tid) for tid in tids}
125
126    def prep_taskhash(self, tid, deps, dataCaches):
127        return
128
129    def get_taskhash(self, tid, deps, dataCaches):
130        self.taskhash[tid] = hashlib.sha256(tid.encode("utf-8")).hexdigest()
131        return self.taskhash[tid]
132
133    def writeout_file_checksum_cache(self):
134        """Write/update the file checksum cache onto disk"""
135        return
136
137    def stampfile_base(self, mcfn):
138        mc = bb.runqueue.mc_from_tid(mcfn)
139        return self.datacaches[mc].stamp[mcfn]
140
141    def stampfile_mcfn(self, taskname, mcfn, extrainfo=True):
142        mc = bb.runqueue.mc_from_tid(mcfn)
143        stamp = self.datacaches[mc].stamp[mcfn]
144        if not stamp:
145            return
146
147        stamp_extrainfo = ""
148        if extrainfo:
149            taskflagname = taskname
150            if taskname.endswith("_setscene"):
151                taskflagname = taskname.replace("_setscene", "")
152            stamp_extrainfo = self.datacaches[mc].stamp_extrainfo[mcfn].get(taskflagname) or ""
153
154        return self.stampfile(stamp, mcfn, taskname, stamp_extrainfo)
155
156    def stampfile(self, stampbase, file_name, taskname, extrainfo):
157        return ("%s.%s.%s" % (stampbase, taskname, extrainfo)).rstrip('.')
158
159    def stampcleanmask_mcfn(self, taskname, mcfn):
160        mc = bb.runqueue.mc_from_tid(mcfn)
161        stamp = self.datacaches[mc].stamp[mcfn]
162        if not stamp:
163            return []
164
165        taskflagname = taskname
166        if taskname.endswith("_setscene"):
167            taskflagname = taskname.replace("_setscene", "")
168        stamp_extrainfo = self.datacaches[mc].stamp_extrainfo[mcfn].get(taskflagname) or ""
169
170        return self.stampcleanmask(stamp, mcfn, taskname, stamp_extrainfo)
171
172    def stampcleanmask(self, stampbase, file_name, taskname, extrainfo):
173        return ("%s.%s.%s" % (stampbase, taskname, extrainfo)).rstrip('.')
174
175    def dump_sigtask(self, mcfn, task, stampbase, runtime):
176        return
177
178    def invalidate_task(self, task, mcfn):
179        mc = bb.runqueue.mc_from_tid(mcfn)
180        stamp = self.datacaches[mc].stamp[mcfn]
181        bb.utils.remove(stamp)
182
183    def dump_sigs(self, dataCache, options):
184        return
185
186    def get_taskdata(self):
187        return (self.runtaskdeps, self.taskhash, self.unihash, self.file_checksum_values, self.taints, self.basehash, self.unitaskhashes, self.tidtopn, self.setscenetasks)
188
189    def set_taskdata(self, data):
190        self.runtaskdeps, self.taskhash, self.unihash, self.file_checksum_values, self.taints, self.basehash, self.unitaskhashes, self.tidtopn, self.setscenetasks = data
191
192    def reset(self, data):
193        self.__init__(data)
194
195    def get_taskhashes(self):
196        return self.taskhash, self.unihash, self.unitaskhashes, self.tidtopn
197
198    def set_taskhashes(self, hashes):
199        self.taskhash, self.unihash, self.unitaskhashes, self.tidtopn = hashes
200
201    def save_unitaskhashes(self):
202        return
203
204    def copy_unitaskhashes(self, targetdir):
205        return
206
207    def set_setscene_tasks(self, setscene_tasks):
208        return
209
210    def exit(self):
211        return
212
213def build_pnid(mc, pn, taskname):
214    if mc:
215        return "mc:" + mc + ":" + pn + ":" + taskname
216    return pn + ":" + taskname
217
218class SignatureGeneratorBasic(SignatureGenerator):
219    """
220    """
221    name = "basic"
222
223    def __init__(self, data):
224        self.basehash = {}
225        self.taskhash = {}
226        self.unihash = {}
227        self.runtaskdeps = {}
228        self.file_checksum_values = {}
229        self.taints = {}
230        self.setscenetasks = set()
231        self.basehash_ignore_vars = set((data.getVar("BB_BASEHASH_IGNORE_VARS") or "").split())
232        self.taskhash_ignore_tasks = None
233        self.init_rundepcheck(data)
234        checksum_cache_file = data.getVar("BB_HASH_CHECKSUM_CACHE_FILE")
235        if checksum_cache_file:
236            self.checksum_cache = FileChecksumCache()
237            self.checksum_cache.init_cache(data, checksum_cache_file)
238        else:
239            self.checksum_cache = None
240
241        self.unihash_cache = bb.cache.SimpleCache("3")
242        self.unitaskhashes = self.unihash_cache.init_cache(data, "bb_unihashes.dat", {})
243        self.localdirsexclude = (data.getVar("BB_SIGNATURE_LOCAL_DIRS_EXCLUDE") or "CVS .bzr .git .hg .osc .p4 .repo .svn").split()
244        self.tidtopn = {}
245
246    def init_rundepcheck(self, data):
247        self.taskhash_ignore_tasks = data.getVar("BB_TASKHASH_IGNORE_TASKS") or None
248        if self.taskhash_ignore_tasks:
249            self.twl = re.compile(self.taskhash_ignore_tasks)
250        else:
251            self.twl = None
252
253    def _build_data(self, mcfn, d):
254
255        ignore_mismatch = ((d.getVar("BB_HASH_IGNORE_MISMATCH") or '') == '1')
256        tasklist, gendeps, lookupcache = bb.data.generate_dependencies(d, self.basehash_ignore_vars)
257
258        taskdeps, basehash = bb.data.generate_dependency_hash(tasklist, gendeps, lookupcache, self.basehash_ignore_vars, mcfn)
259
260        for task in tasklist:
261            tid = mcfn + ":" + task
262            if not ignore_mismatch and tid in self.basehash and self.basehash[tid] != basehash[tid]:
263                bb.error("When reparsing %s, the basehash value changed from %s to %s. The metadata is not deterministic and this needs to be fixed." % (tid, self.basehash[tid], basehash[tid]))
264                bb.error("The following commands may help:")
265                cmd = "$ bitbake %s -c%s" % (d.getVar('PN'), task)
266                # Make sure sigdata is dumped before run printdiff
267                bb.error("%s -Snone" % cmd)
268                bb.error("Then:")
269                bb.error("%s -Sprintdiff\n" % cmd)
270            self.basehash[tid] = basehash[tid]
271
272        return taskdeps, gendeps, lookupcache
273
274    def set_setscene_tasks(self, setscene_tasks):
275        self.setscenetasks = set(setscene_tasks)
276
277    def finalise(self, fn, d, variant):
278
279        mc = d.getVar("__BBMULTICONFIG", False) or ""
280        mcfn = fn
281        if variant or mc:
282            mcfn = bb.cache.realfn2virtual(fn, variant, mc)
283
284        try:
285            taskdeps, gendeps, lookupcache = self._build_data(mcfn, d)
286        except bb.parse.SkipRecipe:
287            raise
288        except:
289            bb.warn("Error during finalise of %s" % mcfn)
290            raise
291
292        basehashes = {}
293        for task in taskdeps:
294            basehashes[task] = self.basehash[mcfn + ":" + task]
295
296        d.setVar("__siggen_basehashes", basehashes)
297        d.setVar("__siggen_gendeps", gendeps)
298        d.setVar("__siggen_varvals", lookupcache)
299        d.setVar("__siggen_taskdeps", taskdeps)
300
301        #Slow but can be useful for debugging mismatched basehashes
302        #self.setup_datacache_from_datastore(mcfn, d)
303        #for task in taskdeps:
304        #    self.dump_sigtask(mcfn, task, d.getVar("STAMP"), False)
305
306    def setup_datacache_from_datastore(self, mcfn, d):
307        super().setup_datacache_from_datastore(mcfn, d)
308
309        mc = bb.runqueue.mc_from_tid(mcfn)
310        for attr in ["siggen_varvals", "siggen_taskdeps", "siggen_gendeps"]:
311            if not hasattr(self.datacaches[mc], attr):
312                setattr(self.datacaches[mc], attr, {})
313        self.datacaches[mc].siggen_varvals[mcfn] = d.getVar("__siggen_varvals")
314        self.datacaches[mc].siggen_taskdeps[mcfn] = d.getVar("__siggen_taskdeps")
315        self.datacaches[mc].siggen_gendeps[mcfn] = d.getVar("__siggen_gendeps")
316
317    def rundep_check(self, fn, recipename, task, dep, depname, dataCaches):
318        # Return True if we should keep the dependency, False to drop it
319        # We only manipulate the dependencies for packages not in the ignore
320        # list
321        if self.twl and not self.twl.search(recipename):
322            # then process the actual dependencies
323            if self.twl.search(depname):
324                return False
325        return True
326
327    def read_taint(self, fn, task, stampbase):
328        taint = None
329        try:
330            with open(stampbase + '.' + task + '.taint', 'r') as taintf:
331                taint = taintf.read()
332        except IOError:
333            pass
334        return taint
335
336    def prep_taskhash(self, tid, deps, dataCaches):
337
338        (mc, _, task, mcfn) = bb.runqueue.split_tid_mcfn(tid)
339
340        self.basehash[tid] = dataCaches[mc].basetaskhash[tid]
341        self.runtaskdeps[tid] = []
342        self.file_checksum_values[tid] = []
343        recipename = dataCaches[mc].pkg_fn[mcfn]
344
345        self.tidtopn[tid] = recipename
346        # save hashfn for deps into siginfo?
347        for dep in deps:
348            (depmc, _, deptask, depmcfn) = bb.runqueue.split_tid_mcfn(dep)
349            dep_pn = dataCaches[depmc].pkg_fn[depmcfn]
350
351            if not self.rundep_check(mcfn, recipename, task, dep, dep_pn, dataCaches):
352                continue
353
354            if dep not in self.taskhash:
355                bb.fatal("%s is not in taskhash, caller isn't calling in dependency order?" % dep)
356
357            dep_pnid = build_pnid(depmc, dep_pn, deptask)
358            self.runtaskdeps[tid].append((dep_pnid, dep))
359
360        if task in dataCaches[mc].file_checksums[mcfn]:
361            if self.checksum_cache:
362                checksums = self.checksum_cache.get_checksums(dataCaches[mc].file_checksums[mcfn][task], recipename, self.localdirsexclude)
363            else:
364                checksums = bb.fetch2.get_file_checksums(dataCaches[mc].file_checksums[mcfn][task], recipename, self.localdirsexclude)
365            for (f,cs) in checksums:
366                self.file_checksum_values[tid].append((f,cs))
367
368        taskdep = dataCaches[mc].task_deps[mcfn]
369        if 'nostamp' in taskdep and task in taskdep['nostamp']:
370            # Nostamp tasks need an implicit taint so that they force any dependent tasks to run
371            if tid in self.taints and self.taints[tid].startswith("nostamp:"):
372                # Don't reset taint value upon every call
373                pass
374            else:
375                import uuid
376                taint = str(uuid.uuid4())
377                self.taints[tid] = "nostamp:" + taint
378
379        taint = self.read_taint(mcfn, task, dataCaches[mc].stamp[mcfn])
380        if taint:
381            self.taints[tid] = taint
382            logger.warning("%s is tainted from a forced run" % tid)
383
384        return
385
386    def get_taskhash(self, tid, deps, dataCaches):
387
388        data = self.basehash[tid]
389        for dep in sorted(self.runtaskdeps[tid]):
390            data += self.get_unihash(dep[1])
391
392        for (f, cs) in sorted(self.file_checksum_values[tid], key=clean_checksum_file_path):
393            if cs:
394                if "/./" in f:
395                    data += "./" + f.split("/./")[1]
396                data += cs
397
398        if tid in self.taints:
399            if self.taints[tid].startswith("nostamp:"):
400                data += self.taints[tid][8:]
401            else:
402                data += self.taints[tid]
403
404        h = hashlib.sha256(data.encode("utf-8")).hexdigest()
405        self.taskhash[tid] = h
406        #d.setVar("BB_TASKHASH:task-%s" % task, taskhash[task])
407        return h
408
409    def writeout_file_checksum_cache(self):
410        """Write/update the file checksum cache onto disk"""
411        if self.checksum_cache:
412            self.checksum_cache.save_extras()
413            self.checksum_cache.save_merge()
414        else:
415            bb.fetch2.fetcher_parse_save()
416            bb.fetch2.fetcher_parse_done()
417
418    def save_unitaskhashes(self):
419        self.unihash_cache.save(self.unitaskhashes)
420
421    def copy_unitaskhashes(self, targetdir):
422        self.unihash_cache.copyfile(targetdir)
423
424    def dump_sigtask(self, mcfn, task, stampbase, runtime):
425        tid = mcfn + ":" + task
426        mc = bb.runqueue.mc_from_tid(mcfn)
427        referencestamp = stampbase
428        if isinstance(runtime, str) and runtime.startswith("customfile"):
429            sigfile = stampbase
430            referencestamp = runtime[11:]
431        elif runtime and tid in self.taskhash:
432            sigfile = stampbase + "." + task + ".sigdata" + "." + self.get_unihash(tid)
433        else:
434            sigfile = stampbase + "." + task + ".sigbasedata" + "." + self.basehash[tid]
435
436        with bb.utils.umask(0o002):
437            bb.utils.mkdirhier(os.path.dirname(sigfile))
438
439        data = {}
440        data['task'] = task
441        data['basehash_ignore_vars'] = self.basehash_ignore_vars
442        data['taskhash_ignore_tasks'] = self.taskhash_ignore_tasks
443        data['taskdeps'] = self.datacaches[mc].siggen_taskdeps[mcfn][task]
444        data['basehash'] = self.basehash[tid]
445        data['gendeps'] = {}
446        data['varvals'] = {}
447        data['varvals'][task] = self.datacaches[mc].siggen_varvals[mcfn][task]
448        for dep in self.datacaches[mc].siggen_taskdeps[mcfn][task]:
449            if dep in self.basehash_ignore_vars:
450                continue
451            data['gendeps'][dep] = self.datacaches[mc].siggen_gendeps[mcfn][dep]
452            data['varvals'][dep] = self.datacaches[mc].siggen_varvals[mcfn][dep]
453
454        if runtime and tid in self.taskhash:
455            data['runtaskdeps'] = [dep[0] for dep in sorted(self.runtaskdeps[tid])]
456            data['file_checksum_values'] = []
457            for f,cs in sorted(self.file_checksum_values[tid], key=clean_checksum_file_path):
458                if "/./" in f:
459                    data['file_checksum_values'].append(("./" + f.split("/./")[1], cs))
460                else:
461                    data['file_checksum_values'].append((os.path.basename(f), cs))
462            data['runtaskhashes'] = {}
463            for dep in self.runtaskdeps[tid]:
464                data['runtaskhashes'][dep[0]] = self.get_unihash(dep[1])
465            data['taskhash'] = self.taskhash[tid]
466            data['unihash'] = self.get_unihash(tid)
467
468        taint = self.read_taint(mcfn, task, referencestamp)
469        if taint:
470            data['taint'] = taint
471
472        if runtime and tid in self.taints:
473            if 'nostamp:' in self.taints[tid]:
474                data['taint'] = self.taints[tid]
475
476        computed_basehash = calc_basehash(data)
477        if computed_basehash != self.basehash[tid]:
478            bb.error("Basehash mismatch %s versus %s for %s" % (computed_basehash, self.basehash[tid], tid))
479        if runtime and tid in self.taskhash:
480            computed_taskhash = calc_taskhash(data)
481            if computed_taskhash != self.taskhash[tid]:
482                bb.error("Taskhash mismatch %s versus %s for %s" % (computed_taskhash, self.taskhash[tid], tid))
483                sigfile = sigfile.replace(self.taskhash[tid], computed_taskhash)
484
485        fd, tmpfile = bb.utils.mkstemp(dir=os.path.dirname(sigfile), prefix="sigtask.")
486        try:
487            with bb.compress.zstd.open(fd, "wt", encoding="utf-8", num_threads=1) as f:
488                json.dump(data, f, sort_keys=True, separators=(",", ":"), cls=SetEncoder)
489                f.flush()
490            os.chmod(tmpfile, 0o664)
491            bb.utils.rename(tmpfile, sigfile)
492        except (OSError, IOError) as err:
493            try:
494                os.unlink(tmpfile)
495            except OSError:
496                pass
497            raise err
498
499class SignatureGeneratorBasicHash(SignatureGeneratorBasic):
500    name = "basichash"
501
502    def get_stampfile_hash(self, tid):
503        if tid in self.taskhash:
504            return self.taskhash[tid]
505
506        # If task is not in basehash, then error
507        return self.basehash[tid]
508
509    def stampfile(self, stampbase, mcfn, taskname, extrainfo, clean=False):
510        if taskname.endswith("_setscene"):
511            tid = mcfn + ":" + taskname[:-9]
512        else:
513            tid = mcfn + ":" + taskname
514        if clean:
515            h = "*"
516        else:
517            h = self.get_stampfile_hash(tid)
518
519        return ("%s.%s.%s.%s" % (stampbase, taskname, h, extrainfo)).rstrip('.')
520
521    def stampcleanmask(self, stampbase, mcfn, taskname, extrainfo):
522        return self.stampfile(stampbase, mcfn, taskname, extrainfo, clean=True)
523
524    def invalidate_task(self, task, mcfn):
525        bb.note("Tainting hash to force rebuild of task %s, %s" % (mcfn, task))
526
527        mc = bb.runqueue.mc_from_tid(mcfn)
528        stamp = self.datacaches[mc].stamp[mcfn]
529
530        taintfn = stamp + '.' + task + '.taint'
531
532        import uuid
533        bb.utils.mkdirhier(os.path.dirname(taintfn))
534        # The specific content of the taint file is not really important,
535        # we just need it to be random, so a random UUID is used
536        with open(taintfn, 'w') as taintf:
537            taintf.write(str(uuid.uuid4()))
538
539class SignatureGeneratorUniHashMixIn(object):
540    def __init__(self, data):
541        self.extramethod = {}
542        # NOTE: The cache only tracks hashes that exist. Hashes that don't
543        # exist are always queries from the server since it is possible for
544        # hashes to appear over time, but much less likely for them to
545        # disappear
546        self.unihash_exists_cache = set()
547        self.username = None
548        self.password = None
549        self.env = {}
550
551        origenv = data.getVar("BB_ORIGENV")
552        for e in HASHSERV_ENVVARS:
553            value = data.getVar(e)
554            if not value and origenv:
555                value = origenv.getVar(e)
556            if value:
557                self.env[e] = value
558        super().__init__(data)
559
560    def get_taskdata(self):
561        return (self.server, self.method, self.extramethod, self.max_parallel, self.username, self.password, self.env) + super().get_taskdata()
562
563    def set_taskdata(self, data):
564        self.server, self.method, self.extramethod, self.max_parallel, self.username, self.password, self.env = data[:7]
565        super().set_taskdata(data[7:])
566
567    def get_hashserv_creds(self):
568        if self.username and self.password:
569            return {
570                "username": self.username,
571                "password": self.password,
572            }
573
574        return {}
575
576    @contextmanager
577    def _client_env(self):
578        orig_env = os.environ.copy()
579        try:
580            for k, v in self.env.items():
581                os.environ[k] = v
582
583            yield
584        finally:
585            os.environ = orig_env
586
587    @contextmanager
588    def client(self):
589        with self._client_env():
590            if getattr(self, '_client', None) is None:
591                self._client = hashserv.create_client(self.server, **self.get_hashserv_creds())
592            yield self._client
593
594    @contextmanager
595    def client_pool(self):
596        with self._client_env():
597            if getattr(self, '_client_pool', None) is None:
598                self._client_pool = hashserv.client.ClientPool(self.server, self.max_parallel, **self.get_hashserv_creds())
599            yield self._client_pool
600
601    def reset(self, data):
602        self.__close_clients()
603        return super().reset(data)
604
605    def exit(self):
606        self.__close_clients()
607        return super().exit()
608
609    def __close_clients(self):
610        with self._client_env():
611            if getattr(self, '_client', None) is not None:
612                self._client.close()
613                self._client = None
614            if getattr(self, '_client_pool', None) is not None:
615                self._client_pool.close()
616                self._client_pool = None
617
618    def get_stampfile_hash(self, tid):
619        if tid in self.taskhash:
620            # If a unique hash is reported, use it as the stampfile hash. This
621            # ensures that if a task won't be re-run if the taskhash changes,
622            # but it would result in the same output hash
623            unihash = self._get_unihash(tid)
624            if unihash is not None:
625                return unihash
626
627        return super().get_stampfile_hash(tid)
628
629    def set_unihash(self, tid, unihash):
630        (mc, fn, taskname, taskfn) = bb.runqueue.split_tid_mcfn(tid)
631        key = mc + ":" + self.tidtopn[tid] + ":" + taskname
632        self.unitaskhashes[key] = (self.taskhash[tid], unihash)
633        self.unihash[tid] = unihash
634
635    def _get_unihash(self, tid, checkkey=None):
636        if tid not in self.tidtopn:
637            return None
638        (mc, fn, taskname, taskfn) = bb.runqueue.split_tid_mcfn(tid)
639        key = mc + ":" + self.tidtopn[tid] + ":" + taskname
640        if key not in self.unitaskhashes:
641            return None
642        if not checkkey:
643            checkkey = self.taskhash[tid]
644        (key, unihash) = self.unitaskhashes[key]
645        if key != checkkey:
646            return None
647        return unihash
648
649    def get_cached_unihash(self, tid):
650        taskhash = self.taskhash[tid]
651
652        # If its not a setscene task we can return
653        if self.setscenetasks and tid not in self.setscenetasks:
654            self.unihash[tid] = None
655            return taskhash
656
657        # TODO: This cache can grow unbounded. It probably only needs to keep
658        # for each task
659        unihash =  self._get_unihash(tid)
660        if unihash is not None:
661            self.unihash[tid] = unihash
662            return unihash
663
664        return None
665
666    def _get_method(self, tid):
667        method = self.method
668        if tid in self.extramethod:
669            method = method + self.extramethod[tid]
670
671        return method
672
673    def unihashes_exist(self, query):
674        if len(query) == 0:
675            return {}
676
677        uncached_query = {}
678        result = {}
679        for key, unihash in query.items():
680            if unihash in self.unihash_exists_cache:
681                result[key] = True
682            else:
683                uncached_query[key] = unihash
684
685        if self.max_parallel <= 1 or len(uncached_query) <= 1:
686            # No parallelism required. Make the query serially with the single client
687            with self.client() as client:
688                uncached_result = {
689                    key: client.unihash_exists(value) for key, value in uncached_query.items()
690                }
691        else:
692            with self.client_pool() as client_pool:
693                uncached_result = client_pool.unihashes_exist(uncached_query)
694
695        for key, exists in uncached_result.items():
696            if exists:
697                self.unihash_exists_cache.add(query[key])
698            result[key] = exists
699
700        return result
701
702    def get_unihash(self, tid):
703        return self.get_unihashes([tid])[tid]
704
705    def get_unihashes(self, tids):
706        """
707        For a iterable of tids, returns a dictionary that maps each tid to a
708        unihash
709        """
710        result = {}
711        queries = {}
712        query_result = {}
713
714        for tid in tids:
715            unihash = self.get_cached_unihash(tid)
716            if unihash:
717                result[tid] = unihash
718            else:
719                queries[tid] = (self._get_method(tid), self.taskhash[tid])
720
721        if len(queries) == 0:
722            return result
723
724        if self.max_parallel <= 1 or len(queries) <= 1:
725            # No parallelism required. Make the query serially with the single client
726            with self.client() as client:
727                for tid, args in queries.items():
728                    query_result[tid] = client.get_unihash(*args)
729        else:
730            with self.client_pool() as client_pool:
731                query_result = client_pool.get_unihashes(queries)
732
733        for tid, unihash in query_result.items():
734            # In the absence of being able to discover a unique hash from the
735            # server, make it be equivalent to the taskhash. The unique "hash" only
736            # really needs to be a unique string (not even necessarily a hash), but
737            # making it match the taskhash has a few advantages:
738            #
739            # 1) All of the sstate code that assumes hashes can be the same
740            # 2) It provides maximal compatibility with builders that don't use
741            #    an equivalency server
742            # 3) The value is easy for multiple independent builders to derive the
743            #    same unique hash from the same input. This means that if the
744            #    independent builders find the same taskhash, but it isn't reported
745            #    to the server, there is a better chance that they will agree on
746            #    the unique hash.
747            taskhash = self.taskhash[tid]
748            if unihash:
749                # A unique hash equal to the taskhash is not very interesting,
750                # so it is reported it at debug level 2. If they differ, that
751                # is much more interesting, so it is reported at debug level 1
752                hashequiv_logger.bbdebug((1, 2)[unihash == taskhash], 'Found unihash %s in place of %s for %s from %s' % (unihash, taskhash, tid, self.server))
753            else:
754                hashequiv_logger.debug2('No reported unihash for %s:%s from %s' % (tid, taskhash, self.server))
755                unihash = taskhash
756
757
758            self.set_unihash(tid, unihash)
759            self.unihash[tid] = unihash
760            result[tid] = unihash
761
762        return result
763
764    def report_unihash(self, path, task, d):
765        import importlib
766
767        taskhash = d.getVar('BB_TASKHASH')
768        unihash = d.getVar('BB_UNIHASH')
769        report_taskdata = d.getVar('SSTATE_HASHEQUIV_REPORT_TASKDATA') == '1'
770        tempdir = d.getVar('T')
771        mcfn = d.getVar('BB_FILENAME')
772        tid = mcfn + ':do_' + task
773        key = tid + ':' + taskhash
774
775        if self.setscenetasks and tid not in self.setscenetasks:
776            return
777
778        # This can happen if locked sigs are in action. Detect and just exit
779        if taskhash != self.taskhash[tid]:
780            return
781
782        # Sanity checks
783        cache_unihash = self._get_unihash(tid, checkkey=taskhash)
784        if cache_unihash is None:
785            bb.fatal('%s not in unihash cache. Please report this error' % key)
786
787        if cache_unihash != unihash:
788            bb.fatal("Cache unihash %s doesn't match BB_UNIHASH %s" % (cache_unihash, unihash))
789
790        sigfile = None
791        sigfile_name = "depsig.do_%s.%d" % (task, os.getpid())
792        sigfile_link = "depsig.do_%s" % task
793
794        try:
795            sigfile = open(os.path.join(tempdir, sigfile_name), 'w+b')
796
797            locs = {'path': path, 'sigfile': sigfile, 'task': task, 'd': d}
798
799            if "." in self.method:
800                (module, method) = self.method.rsplit('.', 1)
801                locs['method'] = getattr(importlib.import_module(module), method)
802                outhash = bb.utils.better_eval('method(path, sigfile, task, d)', locs)
803            else:
804                outhash = bb.utils.better_eval(self.method + '(path, sigfile, task, d)', locs)
805
806            try:
807                extra_data = {}
808
809                owner = d.getVar('SSTATE_HASHEQUIV_OWNER')
810                if owner:
811                    extra_data['owner'] = owner
812
813                if report_taskdata:
814                    sigfile.seek(0)
815
816                    extra_data['PN'] = d.getVar('PN')
817                    extra_data['PV'] = d.getVar('PV')
818                    extra_data['PR'] = d.getVar('PR')
819                    extra_data['task'] = task
820                    extra_data['outhash_siginfo'] = sigfile.read().decode('utf-8')
821
822                method = self.method
823                if tid in self.extramethod:
824                    method = method + self.extramethod[tid]
825
826                with self.client() as client:
827                    data = client.report_unihash(taskhash, method, outhash, unihash, extra_data)
828
829                new_unihash = data['unihash']
830
831                if new_unihash != unihash:
832                    hashequiv_logger.debug('Task %s unihash changed %s -> %s by server %s' % (taskhash, unihash, new_unihash, self.server))
833                    bb.event.fire(bb.runqueue.taskUniHashUpdate(mcfn + ':do_' + task, new_unihash), d)
834                    self.set_unihash(tid, new_unihash)
835                    d.setVar('BB_UNIHASH', new_unihash)
836                else:
837                    hashequiv_logger.debug('Reported task %s as unihash %s to %s' % (taskhash, unihash, self.server))
838            except ConnectionError as e:
839                bb.warn('Error contacting Hash Equivalence Server %s: %s' % (self.server, str(e)))
840        finally:
841            if sigfile:
842                sigfile.close()
843
844                sigfile_link_path = os.path.join(tempdir, sigfile_link)
845                bb.utils.remove(sigfile_link_path)
846
847                try:
848                    os.symlink(sigfile_name, sigfile_link_path)
849                except OSError:
850                    pass
851
852    def report_unihash_equiv(self, tid, taskhash, wanted_unihash, current_unihash, datacaches):
853        try:
854            extra_data = {}
855            method = self.method
856            if tid in self.extramethod:
857                method = method + self.extramethod[tid]
858
859            with self.client() as client:
860                data = client.report_unihash_equiv(taskhash, method, wanted_unihash, extra_data)
861
862            hashequiv_logger.verbose('Reported task %s as unihash %s to %s (%s)' % (tid, wanted_unihash, self.server, str(data)))
863
864            if data is None:
865                bb.warn("Server unable to handle unihash report")
866                return False
867
868            finalunihash = data['unihash']
869
870            if finalunihash == current_unihash:
871                hashequiv_logger.verbose('Task %s unihash %s unchanged by server' % (tid, finalunihash))
872            elif finalunihash == wanted_unihash:
873                hashequiv_logger.verbose('Task %s unihash changed %s -> %s as wanted' % (tid, current_unihash, finalunihash))
874                self.set_unihash(tid, finalunihash)
875                return True
876            else:
877                # TODO: What to do here?
878                hashequiv_logger.verbose('Task %s unihash reported as unwanted hash %s' % (tid, finalunihash))
879
880        except ConnectionError as e:
881            bb.warn('Error contacting Hash Equivalence Server %s: %s' % (self.server, str(e)))
882
883        return False
884
885#
886# Dummy class used for bitbake-selftest
887#
888class SignatureGeneratorTestEquivHash(SignatureGeneratorUniHashMixIn, SignatureGeneratorBasicHash):
889    name = "TestEquivHash"
890    def init_rundepcheck(self, data):
891        super().init_rundepcheck(data)
892        self.server = data.getVar('BB_HASHSERVE')
893        self.method = "sstate_output_hash"
894        self.max_parallel = 1
895
896def clean_checksum_file_path(file_checksum_tuple):
897    f, cs = file_checksum_tuple
898    if "/./" in f:
899        return "./" + f.split("/./")[1]
900    return f
901
902def dump_this_task(outfile, d):
903    import bb.parse
904    mcfn = d.getVar("BB_FILENAME")
905    task = "do_" + d.getVar("BB_CURRENTTASK")
906    referencestamp = bb.parse.siggen.stampfile_base(mcfn)
907    bb.parse.siggen.dump_sigtask(mcfn, task, outfile, "customfile:" + referencestamp)
908
909def init_colors(enable_color):
910    """Initialise colour dict for passing to compare_sigfiles()"""
911    # First set up the colours
912    colors = {'color_title':   '\033[1m',
913              'color_default': '\033[0m',
914              'color_add':     '\033[0;32m',
915              'color_remove':  '\033[0;31m',
916             }
917    # Leave all keys present but clear the values
918    if not enable_color:
919        for k in colors.keys():
920            colors[k] = ''
921    return colors
922
923def worddiff_str(oldstr, newstr, colors=None):
924    if not colors:
925        colors = init_colors(False)
926    diff = simplediff.diff(oldstr.split(' '), newstr.split(' '))
927    ret = []
928    for change, value in diff:
929        value = ' '.join(value)
930        if change == '=':
931            ret.append(value)
932        elif change == '+':
933            item = '{color_add}{{+{value}+}}{color_default}'.format(value=value, **colors)
934            ret.append(item)
935        elif change == '-':
936            item = '{color_remove}[-{value}-]{color_default}'.format(value=value, **colors)
937            ret.append(item)
938    whitespace_note = ''
939    if oldstr != newstr and ' '.join(oldstr.split()) == ' '.join(newstr.split()):
940        whitespace_note = ' (whitespace changed)'
941    return '"%s"%s' % (' '.join(ret), whitespace_note)
942
943def list_inline_diff(oldlist, newlist, colors=None):
944    if not colors:
945        colors = init_colors(False)
946    diff = simplediff.diff(oldlist, newlist)
947    ret = []
948    for change, value in diff:
949        value = ' '.join(value)
950        if change == '=':
951            ret.append("'%s'" % value)
952        elif change == '+':
953            item = '{color_add}+{value}{color_default}'.format(value=value, **colors)
954            ret.append(item)
955        elif change == '-':
956            item = '{color_remove}-{value}{color_default}'.format(value=value, **colors)
957            ret.append(item)
958    return '[%s]' % (', '.join(ret))
959
960# Handled renamed fields
961def handle_renames(data):
962    if 'basewhitelist' in data:
963        data['basehash_ignore_vars'] = data['basewhitelist']
964        del data['basewhitelist']
965    if 'taskwhitelist' in data:
966        data['taskhash_ignore_tasks'] = data['taskwhitelist']
967        del data['taskwhitelist']
968
969
970def compare_sigfiles(a, b, recursecb=None, color=False, collapsed=False):
971    output = []
972
973    colors = init_colors(color)
974    def color_format(formatstr, **values):
975        """
976        Return colour formatted string.
977        NOTE: call with the format string, not an already formatted string
978        containing values (otherwise you could have trouble with { and }
979        characters)
980        """
981        if not formatstr.endswith('{color_default}'):
982            formatstr += '{color_default}'
983        # In newer python 3 versions you can pass both of these directly,
984        # but we only require 3.4 at the moment
985        formatparams = {}
986        formatparams.update(colors)
987        formatparams.update(values)
988        return formatstr.format(**formatparams)
989
990    try:
991        with bb.compress.zstd.open(a, "rt", encoding="utf-8", num_threads=1) as f:
992            a_data = json.load(f, object_hook=SetDecoder)
993    except (TypeError, OSError) as err:
994        bb.error("Failed to open sigdata file '%s': %s" % (a, str(err)))
995        raise err
996    try:
997        with bb.compress.zstd.open(b, "rt", encoding="utf-8", num_threads=1) as f:
998            b_data = json.load(f, object_hook=SetDecoder)
999    except (TypeError, OSError) as err:
1000        bb.error("Failed to open sigdata file '%s': %s" % (b, str(err)))
1001        raise err
1002
1003    for data in [a_data, b_data]:
1004        handle_renames(data)
1005
1006    def dict_diff(a, b, ignored_vars=set()):
1007        sa = set(a.keys())
1008        sb = set(b.keys())
1009        common = sa & sb
1010        changed = set()
1011        for i in common:
1012            if a[i] != b[i] and i not in ignored_vars:
1013                changed.add(i)
1014        added = sb - sa
1015        removed = sa - sb
1016        return changed, added, removed
1017
1018    def file_checksums_diff(a, b):
1019        from collections import Counter
1020
1021        # Convert lists back to tuples
1022        a = [(f[0], f[1]) for f in a]
1023        b = [(f[0], f[1]) for f in b]
1024
1025        # Compare lists, ensuring we can handle duplicate filenames if they exist
1026        removedcount = Counter(a)
1027        removedcount.subtract(b)
1028        addedcount = Counter(b)
1029        addedcount.subtract(a)
1030        added = []
1031        for x in b:
1032            if addedcount[x] > 0:
1033                addedcount[x] -= 1
1034                added.append(x)
1035        removed = []
1036        changed = []
1037        for x in a:
1038            if removedcount[x] > 0:
1039                removedcount[x] -= 1
1040                for y in added:
1041                    if y[0] == x[0]:
1042                        changed.append((x[0], x[1], y[1]))
1043                        added.remove(y)
1044                        break
1045                else:
1046                    removed.append(x)
1047        added = [x[0] for x in added]
1048        removed = [x[0] for x in removed]
1049        return changed, added, removed
1050
1051    if 'basehash_ignore_vars' in a_data and a_data['basehash_ignore_vars'] != b_data['basehash_ignore_vars']:
1052        output.append(color_format("{color_title}basehash_ignore_vars changed{color_default} from '%s' to '%s'") % (a_data['basehash_ignore_vars'], b_data['basehash_ignore_vars']))
1053        if a_data['basehash_ignore_vars'] and b_data['basehash_ignore_vars']:
1054            output.append("changed items: %s" % a_data['basehash_ignore_vars'].symmetric_difference(b_data['basehash_ignore_vars']))
1055
1056    if 'taskhash_ignore_tasks' in a_data and a_data['taskhash_ignore_tasks'] != b_data['taskhash_ignore_tasks']:
1057        output.append(color_format("{color_title}taskhash_ignore_tasks changed{color_default} from '%s' to '%s'") % (a_data['taskhash_ignore_tasks'], b_data['taskhash_ignore_tasks']))
1058        if a_data['taskhash_ignore_tasks'] and b_data['taskhash_ignore_tasks']:
1059            output.append("changed items: %s" % a_data['taskhash_ignore_tasks'].symmetric_difference(b_data['taskhash_ignore_tasks']))
1060
1061    if a_data['taskdeps'] != b_data['taskdeps']:
1062        output.append(color_format("{color_title}Task dependencies changed{color_default} from:\n%s\nto:\n%s") % (sorted(a_data['taskdeps']), sorted(b_data['taskdeps'])))
1063
1064    if a_data['basehash'] != b_data['basehash'] and not collapsed:
1065        output.append(color_format("{color_title}basehash changed{color_default} from %s to %s") % (a_data['basehash'], b_data['basehash']))
1066
1067    changed, added, removed = dict_diff(a_data['gendeps'], b_data['gendeps'], a_data['basehash_ignore_vars'] & b_data['basehash_ignore_vars'])
1068    if changed:
1069        for dep in sorted(changed):
1070            output.append(color_format("{color_title}List of dependencies for variable %s changed from '{color_default}%s{color_title}' to '{color_default}%s{color_title}'") % (dep, a_data['gendeps'][dep], b_data['gendeps'][dep]))
1071            if a_data['gendeps'][dep] and b_data['gendeps'][dep]:
1072                output.append("changed items: %s" % a_data['gendeps'][dep].symmetric_difference(b_data['gendeps'][dep]))
1073    if added:
1074        for dep in sorted(added):
1075            output.append(color_format("{color_title}Dependency on variable %s was added") % (dep))
1076    if removed:
1077        for dep in sorted(removed):
1078            output.append(color_format("{color_title}Dependency on Variable %s was removed") % (dep))
1079
1080
1081    changed, added, removed = dict_diff(a_data['varvals'], b_data['varvals'])
1082    if changed:
1083        for dep in sorted(changed):
1084            oldval = a_data['varvals'][dep]
1085            newval = b_data['varvals'][dep]
1086            if newval and oldval and ('\n' in oldval or '\n' in newval):
1087                diff = difflib.unified_diff(oldval.splitlines(), newval.splitlines(), lineterm='')
1088                # Cut off the first two lines, since we aren't interested in
1089                # the old/new filename (they are blank anyway in this case)
1090                difflines = list(diff)[2:]
1091                if color:
1092                    # Add colour to diff output
1093                    for i, line in enumerate(difflines):
1094                        if line.startswith('+'):
1095                            line = color_format('{color_add}{line}', line=line)
1096                            difflines[i] = line
1097                        elif line.startswith('-'):
1098                            line = color_format('{color_remove}{line}', line=line)
1099                            difflines[i] = line
1100                output.append(color_format("{color_title}Variable {var} value changed:{color_default}\n{diff}", var=dep, diff='\n'.join(difflines)))
1101            elif newval and oldval and (' ' in oldval or ' ' in newval):
1102                output.append(color_format("{color_title}Variable {var} value changed:{color_default}\n{diff}", var=dep, diff=worddiff_str(oldval, newval, colors)))
1103            else:
1104                output.append(color_format("{color_title}Variable {var} value changed from '{color_default}{oldval}{color_title}' to '{color_default}{newval}{color_title}'{color_default}", var=dep, oldval=oldval, newval=newval))
1105
1106    if not 'file_checksum_values' in a_data:
1107         a_data['file_checksum_values'] = []
1108    if not 'file_checksum_values' in b_data:
1109         b_data['file_checksum_values'] = []
1110
1111    changed, added, removed = file_checksums_diff(a_data['file_checksum_values'], b_data['file_checksum_values'])
1112    if changed:
1113        for f, old, new in changed:
1114            output.append(color_format("{color_title}Checksum for file %s changed{color_default} from %s to %s") % (f, old, new))
1115    if added:
1116        for f in added:
1117            output.append(color_format("{color_title}Dependency on checksum of file %s was added") % (f))
1118    if removed:
1119        for f in removed:
1120            output.append(color_format("{color_title}Dependency on checksum of file %s was removed") % (f))
1121
1122    if not 'runtaskdeps' in a_data:
1123         a_data['runtaskdeps'] = {}
1124    if not 'runtaskdeps' in b_data:
1125         b_data['runtaskdeps'] = {}
1126
1127    if not collapsed:
1128        if len(a_data['runtaskdeps']) != len(b_data['runtaskdeps']):
1129            changed = ["Number of task dependencies changed"]
1130        else:
1131            changed = []
1132            for idx, task in enumerate(a_data['runtaskdeps']):
1133                a = a_data['runtaskdeps'][idx]
1134                b = b_data['runtaskdeps'][idx]
1135                if a_data['runtaskhashes'][a] != b_data['runtaskhashes'][b] and not collapsed:
1136                    changed.append("%s with hash %s\n changed to\n%s with hash %s" % (a, a_data['runtaskhashes'][a], b, b_data['runtaskhashes'][b]))
1137
1138        if changed:
1139            clean_a = a_data['runtaskdeps']
1140            clean_b = b_data['runtaskdeps']
1141            if clean_a != clean_b:
1142                output.append(color_format("{color_title}runtaskdeps changed:{color_default}\n%s") % list_inline_diff(clean_a, clean_b, colors))
1143            else:
1144                output.append(color_format("{color_title}runtaskdeps changed:"))
1145            output.append("\n".join(changed))
1146
1147
1148    if 'runtaskhashes' in a_data and 'runtaskhashes' in b_data:
1149        a = a_data['runtaskhashes']
1150        b = b_data['runtaskhashes']
1151        changed, added, removed = dict_diff(a, b)
1152        if added:
1153            for dep in sorted(added):
1154                bdep_found = False
1155                if removed:
1156                    for bdep in removed:
1157                        if b[dep] == a[bdep]:
1158                            #output.append("Dependency on task %s was replaced by %s with same hash" % (dep, bdep))
1159                            bdep_found = True
1160                if not bdep_found:
1161                    output.append(color_format("{color_title}Dependency on task %s was added{color_default} with hash %s") % (dep, b[dep]))
1162        if removed:
1163            for dep in sorted(removed):
1164                adep_found = False
1165                if added:
1166                    for adep in added:
1167                        if b[adep] == a[dep]:
1168                            #output.append("Dependency on task %s was replaced by %s with same hash" % (adep, dep))
1169                            adep_found = True
1170                if not adep_found:
1171                    output.append(color_format("{color_title}Dependency on task %s was removed{color_default} with hash %s") % (dep, a[dep]))
1172        if changed:
1173            for dep in sorted(changed):
1174                if not collapsed:
1175                    output.append(color_format("{color_title}Hash for task dependency %s changed{color_default} from %s to %s") % (dep, a[dep], b[dep]))
1176                if callable(recursecb):
1177                    recout = recursecb(dep, a[dep], b[dep])
1178                    if recout:
1179                        if collapsed:
1180                            output.extend(recout)
1181                        else:
1182                            # If a dependent hash changed, might as well print the line above and then defer to the changes in
1183                            # that hash since in all likelyhood, they're the same changes this task also saw.
1184                            output = [output[-1]] + recout
1185                            break
1186
1187    a_taint = a_data.get('taint', None)
1188    b_taint = b_data.get('taint', None)
1189    if a_taint != b_taint:
1190        if a_taint and a_taint.startswith('nostamp:'):
1191            a_taint = a_taint.replace('nostamp:', 'nostamp(uuid4):')
1192        if b_taint and b_taint.startswith('nostamp:'):
1193            b_taint = b_taint.replace('nostamp:', 'nostamp(uuid4):')
1194        output.append(color_format("{color_title}Taint (by forced/invalidated task) changed{color_default} from %s to %s") % (a_taint, b_taint))
1195
1196    return output
1197
1198
1199def calc_basehash(sigdata):
1200    task = sigdata['task']
1201    basedata = sigdata['varvals'][task]
1202
1203    if basedata is None:
1204        basedata = ''
1205
1206    alldeps = sigdata['taskdeps']
1207    for dep in sorted(alldeps):
1208        basedata = basedata + dep
1209        val = sigdata['varvals'][dep]
1210        if val is not None:
1211            basedata = basedata + str(val)
1212
1213    return hashlib.sha256(basedata.encode("utf-8")).hexdigest()
1214
1215def calc_taskhash(sigdata):
1216    data = sigdata['basehash']
1217
1218    for dep in sigdata['runtaskdeps']:
1219        data = data + sigdata['runtaskhashes'][dep]
1220
1221    for c in sigdata['file_checksum_values']:
1222        if c[1]:
1223            if "./" in c[0]:
1224                data = data + c[0]
1225            data = data + c[1]
1226
1227    if 'taint' in sigdata:
1228        if 'nostamp:' in sigdata['taint']:
1229            data = data + sigdata['taint'][8:]
1230        else:
1231            data = data + sigdata['taint']
1232
1233    return hashlib.sha256(data.encode("utf-8")).hexdigest()
1234
1235
1236def dump_sigfile(a):
1237    output = []
1238
1239    try:
1240        with bb.compress.zstd.open(a, "rt", encoding="utf-8", num_threads=1) as f:
1241            a_data = json.load(f, object_hook=SetDecoder)
1242    except (TypeError, OSError) as err:
1243        bb.error("Failed to open sigdata file '%s': %s" % (a, str(err)))
1244        raise err
1245
1246    handle_renames(a_data)
1247
1248    output.append("basehash_ignore_vars: %s" % (sorted(a_data['basehash_ignore_vars'])))
1249
1250    output.append("taskhash_ignore_tasks: %s" % (sorted(a_data['taskhash_ignore_tasks'] or [])))
1251
1252    output.append("Task dependencies: %s" % (sorted(a_data['taskdeps'])))
1253
1254    output.append("basehash: %s" % (a_data['basehash']))
1255
1256    for dep in sorted(a_data['gendeps']):
1257        output.append("List of dependencies for variable %s is %s" % (dep, sorted(a_data['gendeps'][dep])))
1258
1259    for dep in sorted(a_data['varvals']):
1260        output.append("Variable %s value is %s" % (dep, a_data['varvals'][dep]))
1261
1262    if 'runtaskdeps' in a_data:
1263        output.append("Tasks this task depends on: %s" % (sorted(a_data['runtaskdeps'])))
1264
1265    if 'file_checksum_values' in a_data:
1266        output.append("This task depends on the checksums of files: %s" % (sorted(a_data['file_checksum_values'])))
1267
1268    if 'runtaskhashes' in a_data:
1269        for dep in sorted(a_data['runtaskhashes']):
1270            output.append("Hash for dependent task %s is %s" % (dep, a_data['runtaskhashes'][dep]))
1271
1272    if 'taint' in a_data:
1273        if a_data['taint'].startswith('nostamp:'):
1274            msg = a_data['taint'].replace('nostamp:', 'nostamp(uuid4):')
1275        else:
1276            msg = a_data['taint']
1277        output.append("Tainted (by forced/invalidated task): %s" % msg)
1278
1279    if 'task' in a_data:
1280        computed_basehash = calc_basehash(a_data)
1281        output.append("Computed base hash is %s and from file %s" % (computed_basehash, a_data['basehash']))
1282    else:
1283        output.append("Unable to compute base hash")
1284
1285    computed_taskhash = calc_taskhash(a_data)
1286    output.append("Computed task hash is %s" % computed_taskhash)
1287
1288    return output
1289