xref: /openbmc/openbmc/poky/meta/lib/oe/sstatesig.py (revision 92b42cb3)
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6import bb.siggen
7import bb.runqueue
8import oe
9
10def sstate_rundepfilter(siggen, fn, recipename, task, dep, depname, dataCaches):
11    # Return True if we should keep the dependency, False to drop it
12    def isNative(x):
13        return x.endswith("-native")
14    def isCross(x):
15        return "-cross-" in x
16    def isNativeSDK(x):
17        return x.startswith("nativesdk-")
18    def isKernel(mc, fn):
19        inherits = " ".join(dataCaches[mc].inherits[fn])
20        return inherits.find("/module-base.bbclass") != -1 or inherits.find("/linux-kernel-base.bbclass") != -1
21    def isPackageGroup(mc, fn):
22        inherits = " ".join(dataCaches[mc].inherits[fn])
23        return "/packagegroup.bbclass" in inherits
24    def isAllArch(mc, fn):
25        inherits = " ".join(dataCaches[mc].inherits[fn])
26        return "/allarch.bbclass" in inherits
27    def isImage(mc, fn):
28        return "/image.bbclass" in " ".join(dataCaches[mc].inherits[fn])
29    def isSPDXTask(task):
30        return task in ("do_create_spdx", "do_create_runtime_spdx")
31
32    depmc, _, deptaskname, depmcfn = bb.runqueue.split_tid_mcfn(dep)
33    mc, _ = bb.runqueue.split_mc(fn)
34
35    # Keep all dependencies between SPDX tasks in the signature. SPDX documents
36    # are linked together by hashes, which means if a dependent document changes,
37    # all downstream documents must be re-written (even if they are "safe"
38    # dependencies).
39    if isSPDXTask(task) and isSPDXTask(deptaskname):
40        return True
41
42    # (Almost) always include our own inter-task dependencies (unless it comes
43    # from a mcdepends). The exception is the special
44    # do_kernel_configme->do_unpack_and_patch dependency from archiver.bbclass.
45    if recipename == depname and depmc == mc:
46        if task == "do_kernel_configme" and deptaskname == "do_unpack_and_patch":
47            return False
48        return True
49
50    # Exclude well defined recipe->dependency
51    if "%s->%s" % (recipename, depname) in siggen.saferecipedeps:
52        return False
53
54    # Check for special wildcard
55    if "*->%s" % depname in siggen.saferecipedeps and recipename != depname:
56        return False
57
58    # Don't change native/cross/nativesdk recipe dependencies any further
59    if isNative(recipename) or isCross(recipename) or isNativeSDK(recipename):
60        return True
61
62    # Only target packages beyond here
63
64    # allarch packagegroups are assumed to have well behaved names which don't change between architecures/tunes
65    if isPackageGroup(mc, fn) and isAllArch(mc, fn) and not isNative(depname):
66        return False
67
68    # Exclude well defined machine specific configurations which don't change ABI
69    if depname in siggen.abisaferecipes and not isImage(mc, fn):
70        return False
71
72    # Kernel modules are well namespaced. We don't want to depend on the kernel's checksum
73    # if we're just doing an RRECOMMENDS:xxx = "kernel-module-*", not least because the checksum
74    # is machine specific.
75    # Therefore if we're not a kernel or a module recipe (inheriting the kernel classes)
76    # and we reccomend a kernel-module, we exclude the dependency.
77    if dataCaches and isKernel(depmc, depmcfn) and not isKernel(mc, fn):
78        for pkg in dataCaches[mc].runrecs[fn]:
79            if " ".join(dataCaches[mc].runrecs[fn][pkg]).find("kernel-module-") != -1:
80                return False
81
82    # Default to keep dependencies
83    return True
84
85def sstate_lockedsigs(d):
86    sigs = {}
87    types = (d.getVar("SIGGEN_LOCKEDSIGS_TYPES") or "").split()
88    for t in types:
89        siggen_lockedsigs_var = "SIGGEN_LOCKEDSIGS_%s" % t
90        lockedsigs = (d.getVar(siggen_lockedsigs_var) or "").split()
91        for ls in lockedsigs:
92            pn, task, h = ls.split(":", 2)
93            if pn not in sigs:
94                sigs[pn] = {}
95            sigs[pn][task] = [h, siggen_lockedsigs_var]
96    return sigs
97
98class SignatureGeneratorOEBasic(bb.siggen.SignatureGeneratorBasic):
99    name = "OEBasic"
100    def init_rundepcheck(self, data):
101        self.abisaferecipes = (data.getVar("SIGGEN_EXCLUDERECIPES_ABISAFE") or "").split()
102        self.saferecipedeps = (data.getVar("SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS") or "").split()
103        pass
104    def rundep_check(self, fn, recipename, task, dep, depname, dataCaches = None):
105        return sstate_rundepfilter(self, fn, recipename, task, dep, depname, dataCaches)
106
107class SignatureGeneratorOEBasicHashMixIn(object):
108    supports_multiconfig_datacaches = True
109
110    def init_rundepcheck(self, data):
111        self.abisaferecipes = (data.getVar("SIGGEN_EXCLUDERECIPES_ABISAFE") or "").split()
112        self.saferecipedeps = (data.getVar("SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS") or "").split()
113        self.lockedsigs = sstate_lockedsigs(data)
114        self.lockedhashes = {}
115        self.lockedpnmap = {}
116        self.lockedhashfn = {}
117        self.machine = data.getVar("MACHINE")
118        self.mismatch_msgs = []
119        self.unlockedrecipes = (data.getVar("SIGGEN_UNLOCKED_RECIPES") or
120                                "").split()
121        self.unlockedrecipes = { k: "" for k in self.unlockedrecipes }
122        self._internal = False
123        pass
124
125    def tasks_resolved(self, virtmap, virtpnmap, dataCache):
126        # Translate virtual/xxx entries to PN values
127        newabisafe = []
128        for a in self.abisaferecipes:
129            if a in virtpnmap:
130                newabisafe.append(virtpnmap[a])
131            else:
132                newabisafe.append(a)
133        self.abisaferecipes = newabisafe
134        newsafedeps = []
135        for a in self.saferecipedeps:
136            a1, a2 = a.split("->")
137            if a1 in virtpnmap:
138                a1 = virtpnmap[a1]
139            if a2 in virtpnmap:
140                a2 = virtpnmap[a2]
141            newsafedeps.append(a1 + "->" + a2)
142        self.saferecipedeps = newsafedeps
143
144    def rundep_check(self, fn, recipename, task, dep, depname, dataCaches = None):
145        return sstate_rundepfilter(self, fn, recipename, task, dep, depname, dataCaches)
146
147    def get_taskdata(self):
148        return (self.lockedpnmap, self.lockedhashfn, self.lockedhashes) + super().get_taskdata()
149
150    def set_taskdata(self, data):
151        self.lockedpnmap, self.lockedhashfn, self.lockedhashes = data[:3]
152        super().set_taskdata(data[3:])
153
154    def dump_sigs(self, dataCache, options):
155        sigfile = os.getcwd() + "/locked-sigs.inc"
156        bb.plain("Writing locked sigs to %s" % sigfile)
157        self.dump_lockedsigs(sigfile)
158        return super(bb.siggen.SignatureGeneratorBasicHash, self).dump_sigs(dataCache, options)
159
160
161    def get_taskhash(self, tid, deps, dataCaches):
162        if tid in self.lockedhashes:
163            if self.lockedhashes[tid]:
164                return self.lockedhashes[tid]
165            else:
166                return super().get_taskhash(tid, deps, dataCaches)
167
168        h = super().get_taskhash(tid, deps, dataCaches)
169
170        (mc, _, task, fn) = bb.runqueue.split_tid_mcfn(tid)
171
172        recipename = dataCaches[mc].pkg_fn[fn]
173        self.lockedpnmap[fn] = recipename
174        self.lockedhashfn[fn] = dataCaches[mc].hashfn[fn]
175
176        unlocked = False
177        if recipename in self.unlockedrecipes:
178            unlocked = True
179        else:
180            def recipename_from_dep(dep):
181                (depmc, _, _, depfn) = bb.runqueue.split_tid_mcfn(dep)
182                return dataCaches[depmc].pkg_fn[depfn]
183
184            # If any unlocked recipe is in the direct dependencies then the
185            # current recipe should be unlocked as well.
186            depnames = [ recipename_from_dep(x) for x in deps if mc == bb.runqueue.mc_from_tid(x)]
187            if any(x in y for y in depnames for x in self.unlockedrecipes):
188                self.unlockedrecipes[recipename] = ''
189                unlocked = True
190
191        if not unlocked and recipename in self.lockedsigs:
192            if task in self.lockedsigs[recipename]:
193                h_locked = self.lockedsigs[recipename][task][0]
194                var = self.lockedsigs[recipename][task][1]
195                self.lockedhashes[tid] = h_locked
196                self._internal = True
197                unihash = self.get_unihash(tid)
198                self._internal = False
199                #bb.warn("Using %s %s %s" % (recipename, task, h))
200
201                if h != h_locked and h_locked != unihash:
202                    self.mismatch_msgs.append('The %s:%s sig is computed to be %s, but the sig is locked to %s in %s'
203                                          % (recipename, task, h, h_locked, var))
204
205                return h_locked
206
207        self.lockedhashes[tid] = False
208        #bb.warn("%s %s %s" % (recipename, task, h))
209        return h
210
211    def get_stampfile_hash(self, tid):
212        if tid in self.lockedhashes and self.lockedhashes[tid]:
213            return self.lockedhashes[tid]
214        return super().get_stampfile_hash(tid)
215
216    def get_unihash(self, tid):
217        if tid in self.lockedhashes and self.lockedhashes[tid] and not self._internal:
218            return self.lockedhashes[tid]
219        return super().get_unihash(tid)
220
221    def dump_sigtask(self, fn, task, stampbase, runtime):
222        tid = fn + ":" + task
223        if tid in self.lockedhashes and self.lockedhashes[tid]:
224            return
225        super(bb.siggen.SignatureGeneratorBasicHash, self).dump_sigtask(fn, task, stampbase, runtime)
226
227    def dump_lockedsigs(self, sigfile, taskfilter=None):
228        types = {}
229        for tid in self.runtaskdeps:
230            if taskfilter:
231                if not tid in taskfilter:
232                    continue
233            fn = bb.runqueue.fn_from_tid(tid)
234            t = self.lockedhashfn[fn].split(" ")[1].split(":")[5]
235            t = 't-' + t.replace('_', '-')
236            if t not in types:
237                types[t] = []
238            types[t].append(tid)
239
240        with open(sigfile, "w") as f:
241            l = sorted(types)
242            for t in l:
243                f.write('SIGGEN_LOCKEDSIGS_%s = "\\\n' % t)
244                types[t].sort()
245                sortedtid = sorted(types[t], key=lambda tid: self.lockedpnmap[bb.runqueue.fn_from_tid(tid)])
246                for tid in sortedtid:
247                    (_, _, task, fn) = bb.runqueue.split_tid_mcfn(tid)
248                    if tid not in self.taskhash:
249                        continue
250                    f.write("    " + self.lockedpnmap[fn] + ":" + task + ":" + self.get_unihash(tid) + " \\\n")
251                f.write('    "\n')
252            f.write('SIGGEN_LOCKEDSIGS_TYPES:%s = "%s"' % (self.machine, " ".join(l)))
253
254    def dump_siglist(self, sigfile, path_prefix_strip=None):
255        def strip_fn(fn):
256            nonlocal path_prefix_strip
257            if not path_prefix_strip:
258                return fn
259
260            fn_exp = fn.split(":")
261            if fn_exp[-1].startswith(path_prefix_strip):
262                fn_exp[-1] = fn_exp[-1][len(path_prefix_strip):]
263
264            return ":".join(fn_exp)
265
266        with open(sigfile, "w") as f:
267            tasks = []
268            for taskitem in self.taskhash:
269                (fn, task) = taskitem.rsplit(":", 1)
270                pn = self.lockedpnmap[fn]
271                tasks.append((pn, task, strip_fn(fn), self.taskhash[taskitem]))
272            for (pn, task, fn, taskhash) in sorted(tasks):
273                f.write('%s:%s %s %s\n' % (pn, task, fn, taskhash))
274
275    def checkhashes(self, sq_data, missed, found, d):
276        warn_msgs = []
277        error_msgs = []
278        sstate_missing_msgs = []
279
280        for tid in sq_data['hash']:
281            if tid not in found:
282                for pn in self.lockedsigs:
283                    taskname = bb.runqueue.taskname_from_tid(tid)
284                    if sq_data['hash'][tid] in iter(self.lockedsigs[pn].values()):
285                        if taskname == 'do_shared_workdir':
286                            continue
287                        sstate_missing_msgs.append("Locked sig is set for %s:%s (%s) yet not in sstate cache?"
288                                               % (pn, taskname, sq_data['hash'][tid]))
289
290        checklevel = d.getVar("SIGGEN_LOCKEDSIGS_TASKSIG_CHECK")
291        if checklevel == 'warn':
292            warn_msgs += self.mismatch_msgs
293        elif checklevel == 'error':
294            error_msgs += self.mismatch_msgs
295
296        checklevel = d.getVar("SIGGEN_LOCKEDSIGS_SSTATE_EXISTS_CHECK")
297        if checklevel == 'warn':
298            warn_msgs += sstate_missing_msgs
299        elif checklevel == 'error':
300            error_msgs += sstate_missing_msgs
301
302        if warn_msgs:
303            bb.warn("\n".join(warn_msgs))
304        if error_msgs:
305            bb.fatal("\n".join(error_msgs))
306
307class SignatureGeneratorOEBasicHash(SignatureGeneratorOEBasicHashMixIn, bb.siggen.SignatureGeneratorBasicHash):
308    name = "OEBasicHash"
309
310class SignatureGeneratorOEEquivHash(SignatureGeneratorOEBasicHashMixIn, bb.siggen.SignatureGeneratorUniHashMixIn, bb.siggen.SignatureGeneratorBasicHash):
311    name = "OEEquivHash"
312
313    def init_rundepcheck(self, data):
314        super().init_rundepcheck(data)
315        self.server = data.getVar('BB_HASHSERVE')
316        if not self.server:
317            bb.fatal("OEEquivHash requires BB_HASHSERVE to be set")
318        self.method = data.getVar('SSTATE_HASHEQUIV_METHOD')
319        if not self.method:
320            bb.fatal("OEEquivHash requires SSTATE_HASHEQUIV_METHOD to be set")
321
322# Insert these classes into siggen's namespace so it can see and select them
323bb.siggen.SignatureGeneratorOEBasic = SignatureGeneratorOEBasic
324bb.siggen.SignatureGeneratorOEBasicHash = SignatureGeneratorOEBasicHash
325bb.siggen.SignatureGeneratorOEEquivHash = SignatureGeneratorOEEquivHash
326
327
328def find_siginfo(pn, taskname, taskhashlist, d):
329    """ Find signature data files for comparison purposes """
330
331    import fnmatch
332    import glob
333
334    if not taskname:
335        # We have to derive pn and taskname
336        key = pn
337        splitit = key.split('.bb:')
338        taskname = splitit[1]
339        pn = os.path.basename(splitit[0]).split('_')[0]
340        if key.startswith('virtual:native:'):
341            pn = pn + '-native'
342
343    hashfiles = {}
344    filedates = {}
345
346    def get_hashval(siginfo):
347        if siginfo.endswith('.siginfo'):
348            return siginfo.rpartition(':')[2].partition('_')[0]
349        else:
350            return siginfo.rpartition('.')[2]
351
352    # First search in stamps dir
353    localdata = d.createCopy()
354    localdata.setVar('MULTIMACH_TARGET_SYS', '*')
355    localdata.setVar('PN', pn)
356    localdata.setVar('PV', '*')
357    localdata.setVar('PR', '*')
358    localdata.setVar('EXTENDPE', '')
359    stamp = localdata.getVar('STAMP')
360    if pn.startswith("gcc-source"):
361        # gcc-source shared workdir is a special case :(
362        stamp = localdata.expand("${STAMPS_DIR}/work-shared/gcc-${PV}-${PR}")
363
364    filespec = '%s.%s.sigdata.*' % (stamp, taskname)
365    foundall = False
366    import glob
367    for fullpath in glob.glob(filespec):
368        match = False
369        if taskhashlist:
370            for taskhash in taskhashlist:
371                if fullpath.endswith('.%s' % taskhash):
372                    hashfiles[taskhash] = fullpath
373                    if len(hashfiles) == len(taskhashlist):
374                        foundall = True
375                        break
376        else:
377            try:
378                filedates[fullpath] = os.stat(fullpath).st_mtime
379            except OSError:
380                continue
381            hashval = get_hashval(fullpath)
382            hashfiles[hashval] = fullpath
383
384    if not taskhashlist or (len(filedates) < 2 and not foundall):
385        # That didn't work, look in sstate-cache
386        hashes = taskhashlist or ['?' * 64]
387        localdata = bb.data.createCopy(d)
388        for hashval in hashes:
389            localdata.setVar('PACKAGE_ARCH', '*')
390            localdata.setVar('TARGET_VENDOR', '*')
391            localdata.setVar('TARGET_OS', '*')
392            localdata.setVar('PN', pn)
393            localdata.setVar('PV', '*')
394            localdata.setVar('PR', '*')
395            localdata.setVar('BB_TASKHASH', hashval)
396            localdata.setVar('SSTATE_CURRTASK', taskname[3:])
397            swspec = localdata.getVar('SSTATE_SWSPEC')
398            if taskname in ['do_fetch', 'do_unpack', 'do_patch', 'do_populate_lic', 'do_preconfigure'] and swspec:
399                localdata.setVar('SSTATE_PKGSPEC', '${SSTATE_SWSPEC}')
400            elif pn.endswith('-native') or "-cross-" in pn or "-crosssdk-" in pn:
401                localdata.setVar('SSTATE_EXTRAPATH', "${NATIVELSBSTRING}/")
402            filespec = '%s.siginfo' % localdata.getVar('SSTATE_PKG')
403
404            matchedfiles = glob.glob(filespec)
405            for fullpath in matchedfiles:
406                actual_hashval = get_hashval(fullpath)
407                if actual_hashval in hashfiles:
408                    continue
409                hashfiles[hashval] = fullpath
410                if not taskhashlist:
411                    try:
412                        filedates[fullpath] = os.stat(fullpath).st_mtime
413                    except:
414                        continue
415
416    if taskhashlist:
417        return hashfiles
418    else:
419        return filedates
420
421bb.siggen.find_siginfo = find_siginfo
422
423
424def sstate_get_manifest_filename(task, d):
425    """
426    Return the sstate manifest file path for a particular task.
427    Also returns the datastore that can be used to query related variables.
428    """
429    d2 = d.createCopy()
430    extrainf = d.getVarFlag("do_" + task, 'stamp-extra-info')
431    if extrainf:
432        d2.setVar("SSTATE_MANMACH", extrainf)
433    return (d2.expand("${SSTATE_MANFILEPREFIX}.%s" % task), d2)
434
435def find_sstate_manifest(taskdata, taskdata2, taskname, d, multilibcache):
436    d2 = d
437    variant = ''
438    curr_variant = ''
439    if d.getVar("BBEXTENDCURR") == "multilib":
440        curr_variant = d.getVar("BBEXTENDVARIANT")
441        if "virtclass-multilib" not in d.getVar("OVERRIDES"):
442            curr_variant = "invalid"
443    if taskdata2.startswith("virtual:multilib"):
444        variant = taskdata2.split(":")[2]
445    if curr_variant != variant:
446        if variant not in multilibcache:
447            multilibcache[variant] = oe.utils.get_multilib_datastore(variant, d)
448        d2 = multilibcache[variant]
449
450    if taskdata.endswith("-native"):
451        pkgarchs = ["${BUILD_ARCH}", "${BUILD_ARCH}_${ORIGNATIVELSBSTRING}"]
452    elif taskdata.startswith("nativesdk-"):
453        pkgarchs = ["${SDK_ARCH}_${SDK_OS}", "allarch"]
454    elif "-cross-canadian" in taskdata:
455        pkgarchs = ["${SDK_ARCH}_${SDK_ARCH}-${SDKPKGSUFFIX}"]
456    elif "-cross-" in taskdata:
457        pkgarchs = ["${BUILD_ARCH}"]
458    elif "-crosssdk" in taskdata:
459        pkgarchs = ["${BUILD_ARCH}_${SDK_ARCH}_${SDK_OS}"]
460    else:
461        pkgarchs = ['${MACHINE_ARCH}']
462        pkgarchs = pkgarchs + list(reversed(d2.getVar("PACKAGE_EXTRA_ARCHS").split()))
463        pkgarchs.append('allarch')
464        pkgarchs.append('${SDK_ARCH}_${SDK_ARCH}-${SDKPKGSUFFIX}')
465
466    for pkgarch in pkgarchs:
467        manifest = d2.expand("${SSTATE_MANIFESTS}/manifest-%s-%s.%s" % (pkgarch, taskdata, taskname))
468        if os.path.exists(manifest):
469            return manifest, d2
470    bb.fatal("Manifest %s not found in %s (variant '%s')?" % (manifest, d2.expand(" ".join(pkgarchs)), variant))
471    return None, d2
472
473def OEOuthashBasic(path, sigfile, task, d):
474    """
475    Basic output hash function
476
477    Calculates the output hash of a task by hashing all output file metadata,
478    and file contents.
479    """
480    import hashlib
481    import stat
482    import pwd
483    import grp
484    import re
485    import fnmatch
486
487    def update_hash(s):
488        s = s.encode('utf-8')
489        h.update(s)
490        if sigfile:
491            sigfile.write(s)
492
493    h = hashlib.sha256()
494    prev_dir = os.getcwd()
495    corebase = d.getVar("COREBASE")
496    tmpdir = d.getVar("TMPDIR")
497    include_owners = os.environ.get('PSEUDO_DISABLED') == '0'
498    if "package_write_" in task or task == "package_qa":
499        include_owners = False
500    include_timestamps = False
501    include_root = True
502    if task == "package":
503        include_timestamps = True
504        include_root = False
505    hash_version = d.getVar('HASHEQUIV_HASH_VERSION')
506    extra_sigdata = d.getVar("HASHEQUIV_EXTRA_SIGDATA")
507
508    filemaps = {}
509    for m in (d.getVar('SSTATE_HASHEQUIV_FILEMAP') or '').split():
510        entry = m.split(":")
511        if len(entry) != 3 or entry[0] != task:
512            continue
513        filemaps.setdefault(entry[1], [])
514        filemaps[entry[1]].append(entry[2])
515
516    try:
517        os.chdir(path)
518        basepath = os.path.normpath(path)
519
520        update_hash("OEOuthashBasic\n")
521        if hash_version:
522            update_hash(hash_version + "\n")
523
524        if extra_sigdata:
525            update_hash(extra_sigdata + "\n")
526
527        # It is only currently useful to get equivalent hashes for things that
528        # can be restored from sstate. Since the sstate object is named using
529        # SSTATE_PKGSPEC and the task name, those should be included in the
530        # output hash calculation.
531        update_hash("SSTATE_PKGSPEC=%s\n" % d.getVar('SSTATE_PKGSPEC'))
532        update_hash("task=%s\n" % task)
533
534        for root, dirs, files in os.walk('.', topdown=True):
535            # Sort directories to ensure consistent ordering when recursing
536            dirs.sort()
537            files.sort()
538
539            def process(path):
540                s = os.lstat(path)
541
542                if stat.S_ISDIR(s.st_mode):
543                    update_hash('d')
544                elif stat.S_ISCHR(s.st_mode):
545                    update_hash('c')
546                elif stat.S_ISBLK(s.st_mode):
547                    update_hash('b')
548                elif stat.S_ISSOCK(s.st_mode):
549                    update_hash('s')
550                elif stat.S_ISLNK(s.st_mode):
551                    update_hash('l')
552                elif stat.S_ISFIFO(s.st_mode):
553                    update_hash('p')
554                else:
555                    update_hash('-')
556
557                def add_perm(mask, on, off='-'):
558                    if mask & s.st_mode:
559                        update_hash(on)
560                    else:
561                        update_hash(off)
562
563                add_perm(stat.S_IRUSR, 'r')
564                add_perm(stat.S_IWUSR, 'w')
565                if stat.S_ISUID & s.st_mode:
566                    add_perm(stat.S_IXUSR, 's', 'S')
567                else:
568                    add_perm(stat.S_IXUSR, 'x')
569
570                if include_owners:
571                    # Group/other permissions are only relevant in pseudo context
572                    add_perm(stat.S_IRGRP, 'r')
573                    add_perm(stat.S_IWGRP, 'w')
574                    if stat.S_ISGID & s.st_mode:
575                        add_perm(stat.S_IXGRP, 's', 'S')
576                    else:
577                        add_perm(stat.S_IXGRP, 'x')
578
579                    add_perm(stat.S_IROTH, 'r')
580                    add_perm(stat.S_IWOTH, 'w')
581                    if stat.S_ISVTX & s.st_mode:
582                        update_hash('t')
583                    else:
584                        add_perm(stat.S_IXOTH, 'x')
585
586                    try:
587                        update_hash(" %10s" % pwd.getpwuid(s.st_uid).pw_name)
588                        update_hash(" %10s" % grp.getgrgid(s.st_gid).gr_name)
589                    except KeyError as e:
590                        bb.warn("KeyError in %s" % path)
591                        msg = ("KeyError: %s\nPath %s is owned by uid %d, gid %d, which doesn't match "
592                            "any user/group on target. This may be due to host contamination." % (e, path, s.st_uid, s.st_gid))
593                        raise Exception(msg).with_traceback(e.__traceback__)
594
595                if include_timestamps:
596                    update_hash(" %10d" % s.st_mtime)
597
598                update_hash(" ")
599                if stat.S_ISBLK(s.st_mode) or stat.S_ISCHR(s.st_mode):
600                    update_hash("%9s" % ("%d.%d" % (os.major(s.st_rdev), os.minor(s.st_rdev))))
601                else:
602                    update_hash(" " * 9)
603
604                filterfile = False
605                for entry in filemaps:
606                    if fnmatch.fnmatch(path, entry):
607                        filterfile = True
608
609                update_hash(" ")
610                if stat.S_ISREG(s.st_mode) and not filterfile:
611                    update_hash("%10d" % s.st_size)
612                else:
613                    update_hash(" " * 10)
614
615                update_hash(" ")
616                fh = hashlib.sha256()
617                if stat.S_ISREG(s.st_mode):
618                    # Hash file contents
619                    if filterfile:
620                        # Need to ignore paths in crossscripts and postinst-useradd files.
621                        with open(path, 'rb') as d:
622                            chunk = d.read()
623                            chunk = chunk.replace(bytes(basepath, encoding='utf8'), b'')
624                            for entry in filemaps:
625                                if not fnmatch.fnmatch(path, entry):
626                                    continue
627                                for r in filemaps[entry]:
628                                    if r.startswith("regex-"):
629                                        chunk = re.sub(bytes(r[6:], encoding='utf8'), b'', chunk)
630                                    else:
631                                        chunk = chunk.replace(bytes(r, encoding='utf8'), b'')
632                            fh.update(chunk)
633                    else:
634                        with open(path, 'rb') as d:
635                            for chunk in iter(lambda: d.read(4096), b""):
636                                fh.update(chunk)
637                    update_hash(fh.hexdigest())
638                else:
639                    update_hash(" " * len(fh.hexdigest()))
640
641                update_hash(" %s" % path)
642
643                if stat.S_ISLNK(s.st_mode):
644                    update_hash(" -> %s" % os.readlink(path))
645
646                update_hash("\n")
647
648            # Process this directory and all its child files
649            if include_root or root != ".":
650                process(root)
651            for f in files:
652                if f == 'fixmepath':
653                    continue
654                process(os.path.join(root, f))
655    finally:
656        os.chdir(prev_dir)
657
658    return h.hexdigest()
659
660
661