1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7# BB Class inspired by ebuild.sh
8#
9# This class will test files after installation for certain
10# security issues and other kind of issues.
11#
12# Checks we do:
13#  -Check the ownership and permissions
14#  -Check the RUNTIME path for the $TMPDIR
15#  -Check if .la files wrongly point to workdir
16#  -Check if .pc files wrongly point to workdir
17#  -Check if packages contains .debug directories or .so files
18#   where they should be in -dev or -dbg
19#  -Check if config.log contains traces to broken autoconf tests
20#  -Check invalid characters (non-utf8) on some package metadata
21#  -Ensure that binaries in base_[bindir|sbindir|libdir] do not link
22#   into exec_prefix
23#  -Check that scripts in base_[bindir|sbindir|libdir] do not reference
24#   files under exec_prefix
25#  -Check if the package name is upper case
26
27# Elect whether a given type of error is a warning or error, they may
28# have been set by other files.
29WARN_QA ?= " libdir xorg-driver-abi buildpaths \
30            textrel incompatible-license files-invalid \
31            infodir build-deps src-uri-bad symlink-to-sysroot multilib \
32            invalid-packageconfig host-user-contaminated uppercase-pn patch-fuzz \
33            mime mime-xdg unlisted-pkg-lics unhandled-features-check \
34            missing-update-alternatives native-last missing-ptest \
35            license-exists license-no-generic license-syntax license-format \
36            license-incompatible license-file-missing obsolete-license \
37            "
38ERROR_QA ?= "dev-so debug-deps dev-deps debug-files arch pkgconfig la \
39            perms dep-cmp pkgvarcheck perm-config perm-line perm-link \
40            split-strip packages-list pkgv-undefined var-undefined \
41            version-going-backwards expanded-d invalid-chars \
42            license-checksum dev-elf file-rdeps configure-unsafe \
43            configure-gettext perllocalpod shebang-size \
44            already-stripped installed-vs-shipped ldflags compile-host-path \
45            install-host-path pn-overrides unknown-configure-option \
46            useless-rpaths rpaths staticdev empty-dirs \
47            "
48# Add usrmerge QA check based on distro feature
49ERROR_QA:append = "${@bb.utils.contains('DISTRO_FEATURES', 'usrmerge', ' usrmerge', '', d)}"
50
51FAKEROOT_QA = "host-user-contaminated"
52FAKEROOT_QA[doc] = "QA tests which need to run under fakeroot. If any \
53enabled tests are listed here, the do_package_qa task will run under fakeroot."
54
55ALL_QA = "${WARN_QA} ${ERROR_QA}"
56
57UNKNOWN_CONFIGURE_OPT_IGNORE ?= "--enable-nls --disable-nls --disable-silent-rules --disable-dependency-tracking --with-libtool-sysroot --disable-static"
58
59# This is a list of directories that are expected to be empty.
60QA_EMPTY_DIRS ?= " \
61    /dev/pts \
62    /media \
63    /proc \
64    /run \
65    /tmp \
66    ${localstatedir}/run \
67    ${localstatedir}/volatile \
68"
69# It is possible to specify why a directory is expected to be empty by defining
70# QA_EMPTY_DIRS_RECOMMENDATION:<path>, which will then be included in the error
71# message if the directory is not empty. If it is not specified for a directory,
72# then "but it is expected to be empty" will be used.
73
74def package_qa_clean_path(path, d, pkg=None):
75    """
76    Remove redundant paths from the path for display.  If pkg isn't set then
77    TMPDIR is stripped, otherwise PKGDEST/pkg is stripped.
78    """
79    if pkg:
80        path = path.replace(os.path.join(d.getVar("PKGDEST"), pkg), "/")
81    return path.replace(d.getVar("TMPDIR"), "/").replace("//", "/")
82
83QAPATHTEST[shebang-size] = "package_qa_check_shebang_size"
84def package_qa_check_shebang_size(path, name, d, elf, messages):
85    import stat
86    if os.path.islink(path) or stat.S_ISFIFO(os.stat(path).st_mode) or elf:
87        return
88
89    try:
90        with open(path, 'rb') as f:
91            stanza = f.readline(130)
92    except IOError:
93        return
94
95    if stanza.startswith(b'#!'):
96        #Shebang not found
97        try:
98            stanza = stanza.decode("utf-8")
99        except UnicodeDecodeError:
100            #If it is not a text file, it is not a script
101            return
102
103        if len(stanza) > 129:
104            oe.qa.add_message(messages, "shebang-size", "%s: %s maximum shebang size exceeded, the maximum size is 128." % (name, package_qa_clean_path(path, d)))
105            return
106
107QAPATHTEST[libexec] = "package_qa_check_libexec"
108def package_qa_check_libexec(path,name, d, elf, messages):
109
110    # Skip the case where the default is explicitly /usr/libexec
111    libexec = d.getVar('libexecdir')
112    if libexec == "/usr/libexec":
113        return True
114
115    if 'libexec' in path.split(os.path.sep):
116        oe.qa.add_message(messages, "libexec", "%s: %s is using libexec please relocate to %s" % (name, package_qa_clean_path(path, d), libexec))
117        return False
118
119    return True
120
121QAPATHTEST[rpaths] = "package_qa_check_rpath"
122def package_qa_check_rpath(file,name, d, elf, messages):
123    """
124    Check for dangerous RPATHs
125    """
126    if not elf:
127        return
128
129    if os.path.islink(file):
130        return
131
132    bad_dirs = [d.getVar('BASE_WORKDIR'), d.getVar('STAGING_DIR_TARGET')]
133
134    phdrs = elf.run_objdump("-p", d)
135
136    import re
137    rpath_re = re.compile(r"\s+RPATH\s+(.*)")
138    for line in phdrs.split("\n"):
139        m = rpath_re.match(line)
140        if m:
141            rpath = m.group(1)
142            for dir in bad_dirs:
143                if dir in rpath:
144                    oe.qa.add_message(messages, "rpaths", "package %s contains bad RPATH %s in file %s" % (name, rpath, file))
145
146QAPATHTEST[useless-rpaths] = "package_qa_check_useless_rpaths"
147def package_qa_check_useless_rpaths(file, name, d, elf, messages):
148    """
149    Check for RPATHs that are useless but not dangerous
150    """
151    def rpath_eq(a, b):
152        return os.path.normpath(a) == os.path.normpath(b)
153
154    if not elf:
155        return
156
157    if os.path.islink(file):
158        return
159
160    libdir = d.getVar("libdir")
161    base_libdir = d.getVar("base_libdir")
162
163    phdrs = elf.run_objdump("-p", d)
164
165    import re
166    rpath_re = re.compile(r"\s+RPATH\s+(.*)")
167    for line in phdrs.split("\n"):
168        m = rpath_re.match(line)
169        if m:
170            rpath = m.group(1)
171            if rpath_eq(rpath, libdir) or rpath_eq(rpath, base_libdir):
172                # The dynamic linker searches both these places anyway.  There is no point in
173                # looking there again.
174                oe.qa.add_message(messages, "useless-rpaths", "%s: %s contains probably-redundant RPATH %s" % (name, package_qa_clean_path(file, d, name), rpath))
175
176QAPATHTEST[dev-so] = "package_qa_check_dev"
177def package_qa_check_dev(path, name, d, elf, messages):
178    """
179    Check for ".so" library symlinks in non-dev packages
180    """
181
182    if not name.endswith("-dev") and not name.endswith("-dbg") and not name.endswith("-ptest") and not name.startswith("nativesdk-") and path.endswith(".so") and os.path.islink(path):
183        oe.qa.add_message(messages, "dev-so", "non -dev/-dbg/nativesdk- package %s contains symlink .so '%s'" % \
184                 (name, package_qa_clean_path(path, d, name)))
185
186QAPATHTEST[dev-elf] = "package_qa_check_dev_elf"
187def package_qa_check_dev_elf(path, name, d, elf, messages):
188    """
189    Check that -dev doesn't contain real shared libraries.  The test has to
190    check that the file is not a link and is an ELF object as some recipes
191    install link-time .so files that are linker scripts.
192    """
193    if name.endswith("-dev") and path.endswith(".so") and not os.path.islink(path) and elf:
194        oe.qa.add_message(messages, "dev-elf", "-dev package %s contains non-symlink .so '%s'" % \
195                 (name, package_qa_clean_path(path, d, name)))
196
197QAPATHTEST[staticdev] = "package_qa_check_staticdev"
198def package_qa_check_staticdev(path, name, d, elf, messages):
199    """
200    Check for ".a" library in non-staticdev packages
201    There are a number of exceptions to this rule, -pic packages can contain
202    static libraries, the _nonshared.a belong with their -dev packages and
203    libgcc.a, libgcov.a will be skipped in their packages
204    """
205
206    if not name.endswith("-pic") and not name.endswith("-staticdev") and not name.endswith("-ptest") and path.endswith(".a") and not path.endswith("_nonshared.a") and not '/usr/lib/debug-static/' in path and not '/.debug-static/' in path:
207        oe.qa.add_message(messages, "staticdev", "non -staticdev package contains static .a library: %s path '%s'" % \
208                 (name, package_qa_clean_path(path,d, name)))
209
210QAPATHTEST[mime] = "package_qa_check_mime"
211def package_qa_check_mime(path, name, d, elf, messages):
212    """
213    Check if package installs mime types to /usr/share/mime/packages
214    while no inheriting mime.bbclass
215    """
216
217    if d.getVar("datadir") + "/mime/packages" in path and path.endswith('.xml') and not bb.data.inherits_class("mime", d):
218        oe.qa.add_message(messages, "mime", "package contains mime types but does not inherit mime: %s path '%s'" % \
219                 (name, package_qa_clean_path(path,d)))
220
221QAPATHTEST[mime-xdg] = "package_qa_check_mime_xdg"
222def package_qa_check_mime_xdg(path, name, d, elf, messages):
223    """
224    Check if package installs desktop file containing MimeType and requires
225    mime-types.bbclass to create /usr/share/applications/mimeinfo.cache
226    """
227
228    if d.getVar("datadir") + "/applications" in path and path.endswith('.desktop') and not bb.data.inherits_class("mime-xdg", d):
229        mime_type_found = False
230        try:
231            with open(path, 'r') as f:
232                for line in f.read().split('\n'):
233                    if 'MimeType' in line:
234                        mime_type_found = True
235                        break;
236        except:
237            # At least libreoffice installs symlinks with absolute paths that are dangling here.
238            # We could implement some magic but for few (one) recipes it is not worth the effort so just warn:
239            wstr = "%s cannot open %s - is it a symlink with absolute path?\n" % (name, package_qa_clean_path(path,d))
240            wstr += "Please check if (linked) file contains key 'MimeType'.\n"
241            pkgname = name
242            if name == d.getVar('PN'):
243                pkgname = '${PN}'
244            wstr += "If yes: add \'inhert mime-xdg\' and \'MIME_XDG_PACKAGES += \"%s\"\' / if no add \'INSANE_SKIP:%s += \"mime-xdg\"\' to recipe." % (pkgname, pkgname)
245            oe.qa.add_message(messages, "mime-xdg", wstr)
246        if mime_type_found:
247            oe.qa.add_message(messages, "mime-xdg", "package contains desktop file with key 'MimeType' but does not inhert mime-xdg: %s path '%s'" % \
248                    (name, package_qa_clean_path(path,d)))
249
250def package_qa_check_libdir(d):
251    """
252    Check for wrong library installation paths. For instance, catch
253    recipes installing /lib/bar.so when ${base_libdir}="lib32" or
254    installing in /usr/lib64 when ${libdir}="/usr/lib"
255    """
256    import re
257
258    pkgdest = d.getVar('PKGDEST')
259    base_libdir = d.getVar("base_libdir") + os.sep
260    libdir = d.getVar("libdir") + os.sep
261    libexecdir = d.getVar("libexecdir") + os.sep
262    exec_prefix = d.getVar("exec_prefix") + os.sep
263
264    messages = []
265
266    # The re's are purposely fuzzy, as some there are some .so.x.y.z files
267    # that don't follow the standard naming convention. It checks later
268    # that they are actual ELF files
269    lib_re = re.compile(r"^/lib.+\.so(\..+)?$")
270    exec_re = re.compile(r"^%s.*/lib.+\.so(\..+)?$" % exec_prefix)
271
272    for root, dirs, files in os.walk(pkgdest):
273        if root == pkgdest:
274            # Skip subdirectories for any packages with libdir in INSANE_SKIP
275            skippackages = []
276            for package in dirs:
277                if 'libdir' in (d.getVar('INSANE_SKIP:' + package) or "").split():
278                    bb.note("Package %s skipping libdir QA test" % (package))
279                    skippackages.append(package)
280                elif d.getVar('PACKAGE_DEBUG_SPLIT_STYLE') == 'debug-file-directory' and package.endswith("-dbg"):
281                    bb.note("Package %s skipping libdir QA test for PACKAGE_DEBUG_SPLIT_STYLE equals debug-file-directory" % (package))
282                    skippackages.append(package)
283            for package in skippackages:
284                dirs.remove(package)
285        for file in files:
286            full_path = os.path.join(root, file)
287            rel_path = os.path.relpath(full_path, pkgdest)
288            if os.sep in rel_path:
289                package, rel_path = rel_path.split(os.sep, 1)
290                rel_path = os.sep + rel_path
291                if lib_re.match(rel_path):
292                    if base_libdir not in rel_path:
293                        # make sure it's an actual ELF file
294                        elf = oe.qa.ELFFile(full_path)
295                        try:
296                            elf.open()
297                            messages.append("%s: found library in wrong location: %s" % (package, rel_path))
298                        except (oe.qa.NotELFFileError):
299                            pass
300                if exec_re.match(rel_path):
301                    if libdir not in rel_path and libexecdir not in rel_path:
302                        # make sure it's an actual ELF file
303                        elf = oe.qa.ELFFile(full_path)
304                        try:
305                            elf.open()
306                            messages.append("%s: found library in wrong location: %s" % (package, rel_path))
307                        except (oe.qa.NotELFFileError):
308                            pass
309
310    if messages:
311        oe.qa.handle_error("libdir", "\n".join(messages), d)
312
313QAPATHTEST[debug-files] = "package_qa_check_dbg"
314def package_qa_check_dbg(path, name, d, elf, messages):
315    """
316    Check for ".debug" files or directories outside of the dbg package
317    """
318
319    if not "-dbg" in name and not "-ptest" in name:
320        if '.debug' in path.split(os.path.sep):
321            oe.qa.add_message(messages, "debug-files", "non debug package contains .debug directory: %s path %s" % \
322                     (name, package_qa_clean_path(path,d)))
323
324QAPATHTEST[arch] = "package_qa_check_arch"
325def package_qa_check_arch(path,name,d, elf, messages):
326    """
327    Check if archs are compatible
328    """
329    import re, oe.elf
330
331    if not elf:
332        return
333
334    target_os   = d.getVar('HOST_OS')
335    target_arch = d.getVar('HOST_ARCH')
336    provides = d.getVar('PROVIDES')
337    bpn = d.getVar('BPN')
338
339    if target_arch == "allarch":
340        pn = d.getVar('PN')
341        oe.qa.add_message(messages, "arch", pn + ": Recipe inherits the allarch class, but has packaged architecture-specific binaries")
342        return
343
344    # FIXME: Cross package confuse this check, so just skip them
345    for s in ['cross', 'nativesdk', 'cross-canadian']:
346        if bb.data.inherits_class(s, d):
347            return
348
349    # avoid following links to /usr/bin (e.g. on udev builds)
350    # we will check the files pointed to anyway...
351    if os.path.islink(path):
352        return
353
354    #if this will throw an exception, then fix the dict above
355    (machine, osabi, abiversion, littleendian, bits) \
356        = oe.elf.machine_dict(d)[target_os][target_arch]
357
358    # Check the architecture and endiannes of the binary
359    is_32 = (("virtual/kernel" in provides) or bb.data.inherits_class("module", d)) and \
360            (target_os == "linux-gnux32" or target_os == "linux-muslx32" or \
361            target_os == "linux-gnu_ilp32" or re.match(r'mips64.*32', d.getVar('DEFAULTTUNE')))
362    is_bpf = (oe.qa.elf_machine_to_string(elf.machine()) == "BPF")
363    if not ((machine == elf.machine()) or is_32 or is_bpf):
364        oe.qa.add_message(messages, "arch", "Architecture did not match (%s, expected %s) in %s" % \
365                 (oe.qa.elf_machine_to_string(elf.machine()), oe.qa.elf_machine_to_string(machine), package_qa_clean_path(path, d, name)))
366    elif not ((bits == elf.abiSize()) or is_32 or is_bpf):
367        oe.qa.add_message(messages, "arch", "Bit size did not match (%d, expected %d) in %s" % \
368                 (elf.abiSize(), bits, package_qa_clean_path(path, d, name)))
369    elif not ((littleendian == elf.isLittleEndian()) or is_bpf):
370        oe.qa.add_message(messages, "arch", "Endiannes did not match (%d, expected %d) in %s" % \
371                 (elf.isLittleEndian(), littleendian, package_qa_clean_path(path,d, name)))
372
373QAPATHTEST[desktop] = "package_qa_check_desktop"
374def package_qa_check_desktop(path, name, d, elf, messages):
375    """
376    Run all desktop files through desktop-file-validate.
377    """
378    if path.endswith(".desktop"):
379        desktop_file_validate = os.path.join(d.getVar('STAGING_BINDIR_NATIVE'),'desktop-file-validate')
380        output = os.popen("%s %s" % (desktop_file_validate, path))
381        # This only produces output on errors
382        for l in output:
383            oe.qa.add_message(messages, "desktop", "Desktop file issue: " + l.strip())
384
385QAPATHTEST[textrel] = "package_qa_textrel"
386def package_qa_textrel(path, name, d, elf, messages):
387    """
388    Check if the binary contains relocations in .text
389    """
390
391    if not elf:
392        return
393
394    if os.path.islink(path):
395        return
396
397    phdrs = elf.run_objdump("-p", d)
398    sane = True
399
400    import re
401    textrel_re = re.compile(r"\s+TEXTREL\s+")
402    for line in phdrs.split("\n"):
403        if textrel_re.match(line):
404            sane = False
405            break
406
407    if not sane:
408        path = package_qa_clean_path(path, d, name)
409        oe.qa.add_message(messages, "textrel", "%s: ELF binary %s has relocations in .text" % (name, path))
410
411QAPATHTEST[ldflags] = "package_qa_hash_style"
412def package_qa_hash_style(path, name, d, elf, messages):
413    """
414    Check if the binary has the right hash style...
415    """
416
417    if not elf:
418        return
419
420    if os.path.islink(path):
421        return
422
423    gnu_hash = "--hash-style=gnu" in d.getVar('LDFLAGS')
424    if not gnu_hash:
425        gnu_hash = "--hash-style=both" in d.getVar('LDFLAGS')
426    if not gnu_hash:
427        return
428
429    sane = False
430    has_syms = False
431
432    phdrs = elf.run_objdump("-p", d)
433
434    # If this binary has symbols, we expect it to have GNU_HASH too.
435    for line in phdrs.split("\n"):
436        if "SYMTAB" in line:
437            has_syms = True
438        if "GNU_HASH" in line or "MIPS_XHASH" in line:
439            sane = True
440        if ("[mips32]" in line or "[mips64]" in line) and d.getVar('TCLIBC') == "musl":
441            sane = True
442    if has_syms and not sane:
443        path = package_qa_clean_path(path, d, name)
444        oe.qa.add_message(messages, "ldflags", "File %s in package %s doesn't have GNU_HASH (didn't pass LDFLAGS?)" % (path, name))
445
446
447QAPATHTEST[buildpaths] = "package_qa_check_buildpaths"
448def package_qa_check_buildpaths(path, name, d, elf, messages):
449    """
450    Check for build paths inside target files and error if paths are not
451    explicitly ignored.
452    """
453    import stat
454
455    # Ignore symlinks/devs/fifos
456    mode = os.lstat(path).st_mode
457    if stat.S_ISLNK(mode) or stat.S_ISBLK(mode) or stat.S_ISFIFO(mode) or stat.S_ISCHR(mode) or stat.S_ISSOCK(mode):
458        return
459
460    tmpdir = bytes(d.getVar('TMPDIR'), encoding="utf-8")
461    with open(path, 'rb') as f:
462        file_content = f.read()
463        if tmpdir in file_content:
464            trimmed = path.replace(os.path.join (d.getVar("PKGDEST"), name), "")
465            oe.qa.add_message(messages, "buildpaths", "File %s in package %s contains reference to TMPDIR" % (trimmed, name))
466
467
468QAPATHTEST[xorg-driver-abi] = "package_qa_check_xorg_driver_abi"
469def package_qa_check_xorg_driver_abi(path, name, d, elf, messages):
470    """
471    Check that all packages containing Xorg drivers have ABI dependencies
472    """
473
474    # Skip dev, dbg or nativesdk packages
475    if name.endswith("-dev") or name.endswith("-dbg") or name.startswith("nativesdk-"):
476        return
477
478    driverdir = d.expand("${libdir}/xorg/modules/drivers/")
479    if driverdir in path and path.endswith(".so"):
480        mlprefix = d.getVar('MLPREFIX') or ''
481        for rdep in bb.utils.explode_deps(d.getVar('RDEPENDS:' + name) or ""):
482            if rdep.startswith("%sxorg-abi-" % mlprefix):
483                return
484        oe.qa.add_message(messages, "xorg-driver-abi", "Package %s contains Xorg driver (%s) but no xorg-abi- dependencies" % (name, os.path.basename(path)))
485
486QAPATHTEST[infodir] = "package_qa_check_infodir"
487def package_qa_check_infodir(path, name, d, elf, messages):
488    """
489    Check that /usr/share/info/dir isn't shipped in a particular package
490    """
491    infodir = d.expand("${infodir}/dir")
492
493    if infodir in path:
494        oe.qa.add_message(messages, "infodir", "The /usr/share/info/dir file is not meant to be shipped in a particular package.")
495
496QAPATHTEST[symlink-to-sysroot] = "package_qa_check_symlink_to_sysroot"
497def package_qa_check_symlink_to_sysroot(path, name, d, elf, messages):
498    """
499    Check that the package doesn't contain any absolute symlinks to the sysroot.
500    """
501    if os.path.islink(path):
502        target = os.readlink(path)
503        if os.path.isabs(target):
504            tmpdir = d.getVar('TMPDIR')
505            if target.startswith(tmpdir):
506                trimmed = path.replace(os.path.join (d.getVar("PKGDEST"), name), "")
507                oe.qa.add_message(messages, "symlink-to-sysroot", "Symlink %s in %s points to TMPDIR" % (trimmed, name))
508
509QAPATHTEST[32bit-time] = "check_32bit_symbols"
510def check_32bit_symbols(path, packagename, d, elf, messages):
511    """
512    Check that ELF files do not use any 32 bit time APIs from glibc.
513    """
514    import re
515    # This list is manually constructed by searching the image folder of the
516    # glibc recipe for __USE_TIME_BITS64.  There is no good way to do this
517    # automatically.
518    api32 = {
519        # /usr/include/time.h
520        "clock_getres", "clock_gettime", "clock_nanosleep", "clock_settime",
521        "ctime", "ctime_r", "difftime", "gmtime", "gmtime_r", "localtime",
522        "localtime_r", "mktime", "nanosleep", "time", "timegm", "timelocal",
523        "timer_gettime", "timer_settime", "timespec_get", "timespec_getres",
524        # /usr/include/bits/time.h
525        "clock_adjtime",
526        # /usr/include/signal.h
527        "sigtimedwait",
528        # /usr/include/sys/time.h
529        "futimes", "futimesat", "getitimer", "gettimeofday", "lutimes",
530        "setitimer", "settimeofday", "utimes",
531        # /usr/include/sys/timex.h
532        "adjtimex", "ntp_adjtime", "ntp_gettime", "ntp_gettimex",
533        # /usr/include/sys/wait.h
534        "wait3", "wait4",
535        # /usr/include/sys/stat.h
536        "fstat", "fstat64", "fstatat", "fstatat64", "futimens", "lstat",
537        "lstat64", "stat", "stat64", "utimensat",
538        # /usr/include/sys/poll.h
539        "ppoll",
540        # /usr/include/sys/resource.h
541        "getrusage",
542        # /usr/include/sys/ioctl.h
543        "ioctl",
544        # /usr/include/sys/select.h
545        "select", "pselect",
546        # /usr/include/sys/prctl.h
547        "prctl",
548        # /usr/include/sys/epoll.h
549        "epoll_pwait2",
550        # /usr/include/sys/timerfd.h
551        "timerfd_gettime", "timerfd_settime",
552        # /usr/include/sys/socket.h
553        "getsockopt", "recvmmsg", "recvmsg", "sendmmsg", "sendmsg",
554        "setsockopt",
555        # /usr/include/sys/msg.h
556        "msgctl",
557        # /usr/include/sys/sem.h
558        "semctl", "semtimedop",
559        # /usr/include/sys/shm.h
560        "shmctl",
561        # /usr/include/pthread.h
562        "pthread_clockjoin_np", "pthread_cond_clockwait",
563        "pthread_cond_timedwait", "pthread_mutex_clocklock",
564        "pthread_mutex_timedlock", "pthread_rwlock_clockrdlock",
565        "pthread_rwlock_clockwrlock", "pthread_rwlock_timedrdlock",
566        "pthread_rwlock_timedwrlock", "pthread_timedjoin_np",
567        # /usr/include/semaphore.h
568        "sem_clockwait", "sem_timedwait",
569        # /usr/include/threads.h
570        "cnd_timedwait", "mtx_timedlock", "thrd_sleep",
571        # /usr/include/aio.h
572        "aio_cancel", "aio_error", "aio_read", "aio_return", "aio_suspend",
573        "aio_write", "lio_listio",
574        # /usr/include/mqueue.h
575        "mq_timedreceive", "mq_timedsend",
576        # /usr/include/glob.h
577        "glob", "glob64", "globfree", "globfree64",
578        # /usr/include/sched.h
579        "sched_rr_get_interval",
580        # /usr/include/fcntl.h
581        "fcntl", "fcntl64",
582        # /usr/include/utime.h
583        "utime",
584        # /usr/include/ftw.h
585        "ftw", "ftw64", "nftw", "nftw64",
586        # /usr/include/fts.h
587        "fts64_children", "fts64_close", "fts64_open", "fts64_read",
588        "fts64_set", "fts_children", "fts_close", "fts_open", "fts_read",
589        "fts_set",
590        # /usr/include/netdb.h
591        "gai_suspend",
592    }
593
594    ptrn = re.compile(
595        r'''
596        (?P<value>[\da-fA-F]+) \s+
597        (?P<flags>[lgu! ][w ][C ][W ][Ii ][dD ]F) \s+
598        (?P<section>\*UND\*) \s+
599        (?P<alignment>(?P<size>[\da-fA-F]+)) \s+
600        (?P<symbol>
601        ''' +
602        r'(?P<notag>' + r'|'.join(sorted(api32)) + r')' +
603        r'''
604        (@+(?P<tag>GLIBC_\d+\.\d+\S*)))
605        ''', re.VERBOSE
606    )
607
608    # elf is a oe.qa.ELFFile object
609    if elf is not None:
610        phdrs = elf.run_objdump("-tw", d)
611        syms = re.finditer(ptrn, phdrs)
612        usedapis = {sym.group('notag') for sym in syms}
613        if usedapis:
614            elfpath = package_qa_clean_path(path, d, packagename)
615            # Remove any .debug dir, heuristic that probably works
616            # At this point, any symbol information is stripped into the debug
617            # package, so that is the only place we will find them.
618            elfpath = elfpath.replace('.debug/', '')
619            allowed = (
620                d.getVarFlag(
621                    'INSANE_SKIP:' + d.getVar('PN'), elfpath.replace('/', '_')
622                ) or ''
623            ).split()
624            usedapis -= set(allowed)
625            if usedapis:
626                msgformat = elfpath + " uses 32-bit api '%s'"
627                for sym in usedapis:
628                    oe.qa.add_message(messages, '32bit-time', msgformat % sym)
629                oe.qa.add_message(
630                    messages, '32bit-time',
631                    'Suppress with INSANE_SKIP:%s[%s] = "%s"' % (
632                        d.getVar('PN'), elfpath.replace('/', '_'),
633                        ' '.join(usedapis)
634                    )
635                )
636
637# Check license variables
638do_populate_lic[postfuncs] += "populate_lic_qa_checksum"
639python populate_lic_qa_checksum() {
640    """
641    Check for changes in the license files.
642    """
643
644    lic_files = d.getVar('LIC_FILES_CHKSUM') or ''
645    lic = d.getVar('LICENSE')
646    pn = d.getVar('PN')
647
648    if lic == "CLOSED":
649        return
650
651    if not lic_files and d.getVar('SRC_URI'):
652        oe.qa.handle_error("license-checksum", pn + ": Recipe file fetches files and does not have license file information (LIC_FILES_CHKSUM)", d)
653
654    srcdir = d.getVar('S')
655    corebase_licensefile = d.getVar('COREBASE') + "/LICENSE"
656    for url in lic_files.split():
657        try:
658            (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(url)
659        except bb.fetch.MalformedUrl:
660            oe.qa.handle_error("license-checksum", pn + ": LIC_FILES_CHKSUM contains an invalid URL: " + url, d)
661            continue
662        srclicfile = os.path.join(srcdir, path)
663        if not os.path.isfile(srclicfile):
664            oe.qa.handle_error("license-checksum", pn + ": LIC_FILES_CHKSUM points to an invalid file: " + srclicfile, d)
665            continue
666
667        if (srclicfile == corebase_licensefile):
668            bb.warn("${COREBASE}/LICENSE is not a valid license file, please use '${COMMON_LICENSE_DIR}/MIT' for a MIT License file in LIC_FILES_CHKSUM. This will become an error in the future")
669
670        recipemd5 = parm.get('md5', '')
671        beginline, endline = 0, 0
672        if 'beginline' in parm:
673            beginline = int(parm['beginline'])
674        if 'endline' in parm:
675            endline = int(parm['endline'])
676
677        if (not beginline) and (not endline):
678            md5chksum = bb.utils.md5_file(srclicfile)
679            with open(srclicfile, 'r', errors='replace') as f:
680                license = f.read().splitlines()
681        else:
682            with open(srclicfile, 'rb') as f:
683                import hashlib
684                lineno = 0
685                license = []
686                try:
687                    m = hashlib.new('MD5', usedforsecurity=False)
688                except TypeError:
689                    m = hashlib.new('MD5')
690                for line in f:
691                    lineno += 1
692                    if (lineno >= beginline):
693                        if ((lineno <= endline) or not endline):
694                            m.update(line)
695                            license.append(line.decode('utf-8', errors='replace').rstrip())
696                        else:
697                            break
698                md5chksum = m.hexdigest()
699        if recipemd5 == md5chksum:
700            bb.note (pn + ": md5 checksum matched for ", url)
701        else:
702            if recipemd5:
703                msg = pn + ": The LIC_FILES_CHKSUM does not match for " + url
704                msg = msg + "\n" + pn + ": The new md5 checksum is " + md5chksum
705                max_lines = int(d.getVar('QA_MAX_LICENSE_LINES') or 20)
706                if not license or license[-1] != '':
707                    # Ensure that our license text ends with a line break
708                    # (will be added with join() below).
709                    license.append('')
710                remove = len(license) - max_lines
711                if remove > 0:
712                    start = max_lines // 2
713                    end = start + remove - 1
714                    del license[start:end]
715                    license.insert(start, '...')
716                msg = msg + "\n" + pn + ": Here is the selected license text:" + \
717                        "\n" + \
718                        "{:v^70}".format(" beginline=%d " % beginline if beginline else "") + \
719                        "\n" + "\n".join(license) + \
720                        "{:^^70}".format(" endline=%d " % endline if endline else "")
721                if beginline:
722                    if endline:
723                        srcfiledesc = "%s (lines %d through to %d)" % (srclicfile, beginline, endline)
724                    else:
725                        srcfiledesc = "%s (beginning on line %d)" % (srclicfile, beginline)
726                elif endline:
727                    srcfiledesc = "%s (ending on line %d)" % (srclicfile, endline)
728                else:
729                    srcfiledesc = srclicfile
730                msg = msg + "\n" + pn + ": Check if the license information has changed in %s to verify that the LICENSE value \"%s\" remains valid" % (srcfiledesc, lic)
731
732            else:
733                msg = pn + ": LIC_FILES_CHKSUM is not specified for " +  url
734                msg = msg + "\n" + pn + ": The md5 checksum is " + md5chksum
735            oe.qa.handle_error("license-checksum", msg, d)
736
737    oe.qa.exit_if_errors(d)
738}
739
740def qa_check_staged(path,d):
741    """
742    Check staged la and pc files for common problems like references to the work
743    directory.
744
745    As this is run after every stage we should be able to find the one
746    responsible for the errors easily even if we look at every .pc and .la file.
747    """
748
749    tmpdir = d.getVar('TMPDIR')
750    workdir = os.path.join(tmpdir, "work")
751    recipesysroot = d.getVar("RECIPE_SYSROOT")
752
753    if bb.data.inherits_class("native", d) or bb.data.inherits_class("cross", d):
754        pkgconfigcheck = workdir
755    else:
756        pkgconfigcheck = tmpdir
757
758    skip = (d.getVar('INSANE_SKIP') or "").split()
759    skip_la = False
760    if 'la' in skip:
761        bb.note("Recipe %s skipping qa checking: la" % d.getVar('PN'))
762        skip_la = True
763
764    skip_pkgconfig = False
765    if 'pkgconfig' in skip:
766        bb.note("Recipe %s skipping qa checking: pkgconfig" % d.getVar('PN'))
767        skip_pkgconfig = True
768
769    skip_shebang_size = False
770    if 'shebang-size' in skip:
771        bb.note("Recipe %s skipping qa checkking: shebang-size" % d.getVar('PN'))
772        skip_shebang_size = True
773
774    # find all .la and .pc files
775    # read the content
776    # and check for stuff that looks wrong
777    for root, dirs, files in os.walk(path):
778        for file in files:
779            path = os.path.join(root,file)
780            if file.endswith(".la") and not skip_la:
781                with open(path) as f:
782                    file_content = f.read()
783                    file_content = file_content.replace(recipesysroot, "")
784                    if workdir in file_content:
785                        error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
786                        oe.qa.handle_error("la", error_msg, d)
787            elif file.endswith(".pc") and not skip_pkgconfig:
788                with open(path) as f:
789                    file_content = f.read()
790                    file_content = file_content.replace(recipesysroot, "")
791                    if pkgconfigcheck in file_content:
792                        error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
793                        oe.qa.handle_error("pkgconfig", error_msg, d)
794
795            if not skip_shebang_size:
796                errors = {}
797                package_qa_check_shebang_size(path, "", d, None, errors)
798                for e in errors:
799                    oe.qa.handle_error(e, errors[e], d)
800
801
802# Run all package-wide warnfuncs and errorfuncs
803def package_qa_package(warnfuncs, errorfuncs, package, d):
804    warnings = {}
805    errors = {}
806
807    for func in warnfuncs:
808        func(package, d, warnings)
809    for func in errorfuncs:
810        func(package, d, errors)
811
812    for w in warnings:
813        oe.qa.handle_error(w, warnings[w], d)
814    for e in errors:
815        oe.qa.handle_error(e, errors[e], d)
816
817    return len(errors) == 0
818
819# Run all recipe-wide warnfuncs and errorfuncs
820def package_qa_recipe(warnfuncs, errorfuncs, pn, d):
821    warnings = {}
822    errors = {}
823
824    for func in warnfuncs:
825        func(pn, d, warnings)
826    for func in errorfuncs:
827        func(pn, d, errors)
828
829    for w in warnings:
830        oe.qa.handle_error(w, warnings[w], d)
831    for e in errors:
832        oe.qa.handle_error(e, errors[e], d)
833
834    return len(errors) == 0
835
836def prepopulate_objdump_p(elf, d):
837    output = elf.run_objdump("-p", d)
838    return (elf.name, output)
839
840# Walk over all files in a directory and call func
841def package_qa_walk(warnfuncs, errorfuncs, package, d):
842    #if this will throw an exception, then fix the dict above
843    target_os   = d.getVar('HOST_OS')
844    target_arch = d.getVar('HOST_ARCH')
845
846    warnings = {}
847    errors = {}
848    elves = {}
849    for path in pkgfiles[package]:
850            elf = None
851            if os.path.isfile(path):
852                elf = oe.qa.ELFFile(path)
853                try:
854                    elf.open()
855                    elf.close()
856                except oe.qa.NotELFFileError:
857                    elf = None
858            if elf:
859                elves[path] = elf
860
861    results = oe.utils.multiprocess_launch(prepopulate_objdump_p, elves.values(), d, extraargs=(d,))
862    for item in results:
863        elves[item[0]].set_objdump("-p", item[1])
864
865    for path in pkgfiles[package]:
866            if path in elves:
867                elves[path].open()
868            for func in warnfuncs:
869                func(path, package, d, elves.get(path), warnings)
870            for func in errorfuncs:
871                func(path, package, d, elves.get(path), errors)
872            if path in elves:
873                elves[path].close()
874
875    for w in warnings:
876        oe.qa.handle_error(w, warnings[w], d)
877    for e in errors:
878        oe.qa.handle_error(e, errors[e], d)
879
880def package_qa_check_rdepends(pkg, pkgdest, skip, taskdeps, packages, d):
881    # Don't do this check for kernel/module recipes, there aren't too many debug/development
882    # packages and you can get false positives e.g. on kernel-module-lirc-dev
883    if bb.data.inherits_class("kernel", d) or bb.data.inherits_class("module-base", d):
884        return
885
886    if not "-dbg" in pkg and not "packagegroup-" in pkg and not "-image" in pkg:
887        localdata = bb.data.createCopy(d)
888        localdata.setVar('OVERRIDES', localdata.getVar('OVERRIDES') + ':' + pkg)
889
890        # Now check the RDEPENDS
891        rdepends = bb.utils.explode_deps(localdata.getVar('RDEPENDS') or "")
892
893        # Now do the sanity check!!!
894        if "build-deps" not in skip:
895            for rdepend in rdepends:
896                if "-dbg" in rdepend and "debug-deps" not in skip:
897                    error_msg = "%s rdepends on %s" % (pkg,rdepend)
898                    oe.qa.handle_error("debug-deps", error_msg, d)
899                if (not "-dev" in pkg and not "-staticdev" in pkg) and rdepend.endswith("-dev") and "dev-deps" not in skip:
900                    error_msg = "%s rdepends on %s" % (pkg, rdepend)
901                    oe.qa.handle_error("dev-deps", error_msg, d)
902                if rdepend not in packages:
903                    rdep_data = oe.packagedata.read_subpkgdata(rdepend, d)
904                    if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
905                        continue
906                    if not rdep_data or not 'PN' in rdep_data:
907                        pkgdata_dir = d.getVar("PKGDATA_DIR")
908                        try:
909                            possibles = os.listdir("%s/runtime-rprovides/%s/" % (pkgdata_dir, rdepend))
910                        except OSError:
911                            possibles = []
912                        for p in possibles:
913                            rdep_data = oe.packagedata.read_subpkgdata(p, d)
914                            if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
915                                break
916                    if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
917                        continue
918                    if rdep_data and 'PN' in rdep_data:
919                        error_msg = "%s rdepends on %s, but it isn't a build dependency, missing %s in DEPENDS or PACKAGECONFIG?" % (pkg, rdepend, rdep_data['PN'])
920                    else:
921                        error_msg = "%s rdepends on %s, but it isn't a build dependency?" % (pkg, rdepend)
922                    oe.qa.handle_error("build-deps", error_msg, d)
923
924        if "file-rdeps" not in skip:
925            ignored_file_rdeps = set(['/bin/sh', '/usr/bin/env', 'rtld(GNU_HASH)'])
926            if bb.data.inherits_class('nativesdk', d):
927                ignored_file_rdeps |= set(['/bin/bash', '/usr/bin/perl', 'perl'])
928            # For Saving the FILERDEPENDS
929            filerdepends = {}
930            rdep_data = oe.packagedata.read_subpkgdata(pkg, d)
931            for key in rdep_data:
932                if key.startswith("FILERDEPENDS:"):
933                    for subkey in bb.utils.explode_deps(rdep_data[key]):
934                        if subkey not in ignored_file_rdeps and \
935                                not subkey.startswith('perl('):
936                            # We already know it starts with FILERDEPENDS_
937                            filerdepends[subkey] = key[13:]
938
939            if filerdepends:
940                done = rdepends[:]
941                # Add the rprovides of itself
942                if pkg not in done:
943                    done.insert(0, pkg)
944
945                # The python is not a package, but python-core provides it, so
946                # skip checking /usr/bin/python if python is in the rdeps, in
947                # case there is a RDEPENDS:pkg = "python" in the recipe.
948                for py in [ d.getVar('MLPREFIX') + "python", "python" ]:
949                    if py in done:
950                        filerdepends.pop("/usr/bin/python",None)
951                        done.remove(py)
952                for rdep in done:
953                    # The file dependencies may contain package names, e.g.,
954                    # perl
955                    filerdepends.pop(rdep,None)
956
957                    # For Saving the FILERPROVIDES, RPROVIDES and FILES_INFO
958                    rdep_data = oe.packagedata.read_subpkgdata(rdep, d)
959                    for key in rdep_data:
960                        if key.startswith("FILERPROVIDES:") or key.startswith("RPROVIDES:"):
961                            for subkey in bb.utils.explode_deps(rdep_data[key]):
962                                filerdepends.pop(subkey,None)
963                        # Add the files list to the rprovides
964                        if key.startswith("FILES_INFO:"):
965                            # Use eval() to make it as a dict
966                            for subkey in eval(rdep_data[key]):
967                                filerdepends.pop(subkey,None)
968                    if not filerdepends:
969                        # Break if all the file rdepends are met
970                        break
971            if filerdepends:
972                for key in filerdepends:
973                    error_msg = "%s contained in package %s requires %s, but no providers found in RDEPENDS:%s?" % \
974                            (filerdepends[key].replace(":%s" % pkg, "").replace("@underscore@", "_"), pkg, key, pkg)
975                    oe.qa.handle_error("file-rdeps", error_msg, d)
976package_qa_check_rdepends[vardepsexclude] = "OVERRIDES"
977
978def package_qa_check_deps(pkg, pkgdest, d):
979
980    localdata = bb.data.createCopy(d)
981    localdata.setVar('OVERRIDES', pkg)
982
983    def check_valid_deps(var):
984        try:
985            rvar = bb.utils.explode_dep_versions2(localdata.getVar(var) or "")
986        except ValueError as e:
987            bb.fatal("%s:%s: %s" % (var, pkg, e))
988        for dep in rvar:
989            for v in rvar[dep]:
990                if v and not v.startswith(('< ', '= ', '> ', '<= ', '>=')):
991                    error_msg = "%s:%s is invalid: %s (%s)   only comparisons <, =, >, <=, and >= are allowed" % (var, pkg, dep, v)
992                    oe.qa.handle_error("dep-cmp", error_msg, d)
993
994    check_valid_deps('RDEPENDS')
995    check_valid_deps('RRECOMMENDS')
996    check_valid_deps('RSUGGESTS')
997    check_valid_deps('RPROVIDES')
998    check_valid_deps('RREPLACES')
999    check_valid_deps('RCONFLICTS')
1000
1001QAPKGTEST[usrmerge] = "package_qa_check_usrmerge"
1002def package_qa_check_usrmerge(pkg, d, messages):
1003
1004    pkgdest = d.getVar('PKGDEST')
1005    pkg_dir = pkgdest + os.sep + pkg + os.sep
1006    merged_dirs = ['bin', 'sbin', 'lib'] + d.getVar('MULTILIB_VARIANTS').split()
1007    for f in merged_dirs:
1008        if os.path.exists(pkg_dir + f) and not os.path.islink(pkg_dir + f):
1009            msg = "%s package is not obeying usrmerge distro feature. /%s should be relocated to /usr." % (pkg, f)
1010            oe.qa.add_message(messages, "usrmerge", msg)
1011            return False
1012    return True
1013
1014QAPKGTEST[perllocalpod] = "package_qa_check_perllocalpod"
1015def package_qa_check_perllocalpod(pkg, d, messages):
1016    """
1017    Check that the recipe didn't ship a perlocal.pod file, which shouldn't be
1018    installed in a distribution package.  cpan.bbclass sets NO_PERLLOCAL=1 to
1019    handle this for most recipes.
1020    """
1021    import glob
1022    pkgd = oe.path.join(d.getVar('PKGDEST'), pkg)
1023    podpath = oe.path.join(pkgd, d.getVar("libdir"), "perl*", "*", "*", "perllocal.pod")
1024
1025    matches = glob.glob(podpath)
1026    if matches:
1027        matches = [package_qa_clean_path(path, d, pkg) for path in matches]
1028        msg = "%s contains perllocal.pod (%s), should not be installed" % (pkg, " ".join(matches))
1029        oe.qa.add_message(messages, "perllocalpod", msg)
1030
1031QAPKGTEST[expanded-d] = "package_qa_check_expanded_d"
1032def package_qa_check_expanded_d(package, d, messages):
1033    """
1034    Check for the expanded D (${D}) value in pkg_* and FILES
1035    variables, warn the user to use it correctly.
1036    """
1037    sane = True
1038    expanded_d = d.getVar('D')
1039
1040    for var in 'FILES','pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm':
1041        bbvar = d.getVar(var + ":" + package) or ""
1042        if expanded_d in bbvar:
1043            if var == 'FILES':
1044                oe.qa.add_message(messages, "expanded-d", "FILES in %s recipe should not contain the ${D} variable as it references the local build directory not the target filesystem, best solution is to remove the ${D} reference" % package)
1045                sane = False
1046            else:
1047                oe.qa.add_message(messages, "expanded-d", "%s in %s recipe contains ${D}, it should be replaced by $D instead" % (var, package))
1048                sane = False
1049    return sane
1050
1051QAPKGTEST[unlisted-pkg-lics] = "package_qa_check_unlisted_pkg_lics"
1052def package_qa_check_unlisted_pkg_lics(package, d, messages):
1053    """
1054    Check that all licenses for a package are among the licenses for the recipe.
1055    """
1056    pkg_lics = d.getVar('LICENSE:' + package)
1057    if not pkg_lics:
1058        return True
1059
1060    recipe_lics_set = oe.license.list_licenses(d.getVar('LICENSE'))
1061    package_lics = oe.license.list_licenses(pkg_lics)
1062    unlisted = package_lics - recipe_lics_set
1063    if unlisted:
1064        oe.qa.add_message(messages, "unlisted-pkg-lics",
1065                               "LICENSE:%s includes licenses (%s) that are not "
1066                               "listed in LICENSE" % (package, ' '.join(unlisted)))
1067        return False
1068    obsolete = set(oe.license.obsolete_license_list()) & package_lics - recipe_lics_set
1069    if obsolete:
1070        oe.qa.add_message(messages, "obsolete-license",
1071                               "LICENSE:%s includes obsolete licenses %s" % (package, ' '.join(obsolete)))
1072        return False
1073    return True
1074
1075QAPKGTEST[empty-dirs] = "package_qa_check_empty_dirs"
1076def package_qa_check_empty_dirs(pkg, d, messages):
1077    """
1078    Check for the existence of files in directories that are expected to be
1079    empty.
1080    """
1081
1082    pkgd = oe.path.join(d.getVar('PKGDEST'), pkg)
1083    for dir in (d.getVar('QA_EMPTY_DIRS') or "").split():
1084        empty_dir = oe.path.join(pkgd, dir)
1085        if os.path.exists(empty_dir) and os.listdir(empty_dir):
1086            recommendation = (d.getVar('QA_EMPTY_DIRS_RECOMMENDATION:' + dir) or
1087                              "but it is expected to be empty")
1088            msg = "%s installs files in %s, %s" % (pkg, dir, recommendation)
1089            oe.qa.add_message(messages, "empty-dirs", msg)
1090
1091def package_qa_check_encoding(keys, encode, d):
1092    def check_encoding(key, enc):
1093        sane = True
1094        value = d.getVar(key)
1095        if value:
1096            try:
1097                s = value.encode(enc)
1098            except UnicodeDecodeError as e:
1099                error_msg = "%s has non %s characters" % (key,enc)
1100                sane = False
1101                oe.qa.handle_error("invalid-chars", error_msg, d)
1102        return sane
1103
1104    for key in keys:
1105        sane = check_encoding(key, encode)
1106        if not sane:
1107            break
1108
1109HOST_USER_UID := "${@os.getuid()}"
1110HOST_USER_GID := "${@os.getgid()}"
1111
1112QAPATHTEST[host-user-contaminated] = "package_qa_check_host_user"
1113def package_qa_check_host_user(path, name, d, elf, messages):
1114    """Check for paths outside of /home which are owned by the user running bitbake."""
1115
1116    if not os.path.lexists(path):
1117        return
1118
1119    dest = d.getVar('PKGDEST')
1120    pn = d.getVar('PN')
1121    home = os.path.join(dest, name, 'home')
1122    if path == home or path.startswith(home + os.sep):
1123        return
1124
1125    try:
1126        stat = os.lstat(path)
1127    except OSError as exc:
1128        import errno
1129        if exc.errno != errno.ENOENT:
1130            raise
1131    else:
1132        check_uid = int(d.getVar('HOST_USER_UID'))
1133        if stat.st_uid == check_uid:
1134            oe.qa.add_message(messages, "host-user-contaminated", "%s: %s is owned by uid %d, which is the same as the user running bitbake. This may be due to host contamination" % (pn, package_qa_clean_path(path, d, name), check_uid))
1135            return False
1136
1137        check_gid = int(d.getVar('HOST_USER_GID'))
1138        if stat.st_gid == check_gid:
1139            oe.qa.add_message(messages, "host-user-contaminated", "%s: %s is owned by gid %d, which is the same as the user running bitbake. This may be due to host contamination" % (pn, package_qa_clean_path(path, d, name), check_gid))
1140            return False
1141    return True
1142
1143QARECIPETEST[unhandled-features-check] = "package_qa_check_unhandled_features_check"
1144def package_qa_check_unhandled_features_check(pn, d, messages):
1145    if not bb.data.inherits_class('features_check', d):
1146        var_set = False
1147        for kind in ['DISTRO', 'MACHINE', 'COMBINED']:
1148            for var in ['ANY_OF_' + kind + '_FEATURES', 'REQUIRED_' + kind + '_FEATURES', 'CONFLICT_' + kind + '_FEATURES']:
1149                if d.getVar(var) is not None or d.hasOverrides(var):
1150                    var_set = True
1151        if var_set:
1152            oe.qa.handle_error("unhandled-features-check", "%s: recipe doesn't inherit features_check" % pn, d)
1153
1154QARECIPETEST[missing-update-alternatives] = "package_qa_check_missing_update_alternatives"
1155def package_qa_check_missing_update_alternatives(pn, d, messages):
1156    # Look at all packages and find out if any of those sets ALTERNATIVE variable
1157    # without inheriting update-alternatives class
1158    for pkg in (d.getVar('PACKAGES') or '').split():
1159        if d.getVar('ALTERNATIVE:%s' % pkg) and not bb.data.inherits_class('update-alternatives', d):
1160            oe.qa.handle_error("missing-update-alternatives", "%s: recipe defines ALTERNATIVE:%s but doesn't inherit update-alternatives. This might fail during do_rootfs later!" % (pn, pkg), d)
1161
1162# The PACKAGE FUNC to scan each package
1163python do_package_qa () {
1164    import subprocess
1165    import oe.packagedata
1166
1167    bb.note("DO PACKAGE QA")
1168
1169    main_lic = d.getVar('LICENSE')
1170
1171    # Check for obsolete license references in main LICENSE (packages are checked below for any changes)
1172    main_licenses = oe.license.list_licenses(d.getVar('LICENSE'))
1173    obsolete = set(oe.license.obsolete_license_list()) & main_licenses
1174    if obsolete:
1175        oe.qa.handle_error("obsolete-license", "Recipe LICENSE includes obsolete licenses %s" % ' '.join(obsolete), d)
1176
1177    bb.build.exec_func("read_subpackage_metadata", d)
1178
1179    # Check non UTF-8 characters on recipe's metadata
1180    package_qa_check_encoding(['DESCRIPTION', 'SUMMARY', 'LICENSE', 'SECTION'], 'utf-8', d)
1181
1182    logdir = d.getVar('T')
1183    pn = d.getVar('PN')
1184
1185    # Scan the packages...
1186    pkgdest = d.getVar('PKGDEST')
1187    packages = set((d.getVar('PACKAGES') or '').split())
1188
1189    global pkgfiles
1190    pkgfiles = {}
1191    for pkg in packages:
1192        pkgfiles[pkg] = []
1193        pkgdir = os.path.join(pkgdest, pkg)
1194        for walkroot, dirs, files in os.walk(pkgdir):
1195            # Don't walk into top-level CONTROL or DEBIAN directories as these
1196            # are temporary directories created by do_package.
1197            if walkroot == pkgdir:
1198                for control in ("CONTROL", "DEBIAN"):
1199                    if control in dirs:
1200                        dirs.remove(control)
1201            for file in files:
1202                pkgfiles[pkg].append(os.path.join(walkroot, file))
1203
1204    # no packages should be scanned
1205    if not packages:
1206        return
1207
1208    import re
1209    # The package name matches the [a-z0-9.+-]+ regular expression
1210    pkgname_pattern = re.compile(r"^[a-z0-9.+-]+$")
1211
1212    taskdepdata = d.getVar("BB_TASKDEPDATA", False)
1213    taskdeps = set()
1214    for dep in taskdepdata:
1215        taskdeps.add(taskdepdata[dep][0])
1216
1217    def parse_test_matrix(matrix_name):
1218        testmatrix = d.getVarFlags(matrix_name) or {}
1219        g = globals()
1220        warnchecks = []
1221        for w in (d.getVar("WARN_QA") or "").split():
1222            if w in skip:
1223               continue
1224            if w in testmatrix and testmatrix[w] in g:
1225                warnchecks.append(g[testmatrix[w]])
1226
1227        errorchecks = []
1228        for e in (d.getVar("ERROR_QA") or "").split():
1229            if e in skip:
1230               continue
1231            if e in testmatrix and testmatrix[e] in g:
1232                errorchecks.append(g[testmatrix[e]])
1233        return warnchecks, errorchecks
1234
1235    for package in packages:
1236        skip = set((d.getVar('INSANE_SKIP') or "").split() +
1237                   (d.getVar('INSANE_SKIP:' + package) or "").split())
1238        if skip:
1239            bb.note("Package %s skipping QA tests: %s" % (package, str(skip)))
1240
1241        bb.note("Checking Package: %s" % package)
1242        # Check package name
1243        if not pkgname_pattern.match(package):
1244            oe.qa.handle_error("pkgname",
1245                    "%s doesn't match the [a-z0-9.+-]+ regex" % package, d)
1246
1247        warn_checks, error_checks = parse_test_matrix("QAPATHTEST")
1248        package_qa_walk(warn_checks, error_checks, package, d)
1249
1250        warn_checks, error_checks = parse_test_matrix("QAPKGTEST")
1251        package_qa_package(warn_checks, error_checks, package, d)
1252
1253        package_qa_check_rdepends(package, pkgdest, skip, taskdeps, packages, d)
1254        package_qa_check_deps(package, pkgdest, d)
1255
1256    warn_checks, error_checks = parse_test_matrix("QARECIPETEST")
1257    package_qa_recipe(warn_checks, error_checks, pn, d)
1258
1259    if 'libdir' in d.getVar("ALL_QA").split():
1260        package_qa_check_libdir(d)
1261
1262    oe.qa.exit_if_errors(d)
1263}
1264
1265# binutils is used for most checks, so need to set as dependency
1266# POPULATESYSROOTDEPS is defined in staging class.
1267do_package_qa[depends] += "${POPULATESYSROOTDEPS}"
1268do_package_qa[vardeps] = "${@bb.utils.contains('ERROR_QA', 'empty-dirs', 'QA_EMPTY_DIRS', '', d)}"
1269do_package_qa[vardepsexclude] = "BB_TASKDEPDATA"
1270do_package_qa[rdeptask] = "do_packagedata"
1271addtask do_package_qa after do_packagedata do_package before do_build
1272
1273# Add the package specific INSANE_SKIPs to the sstate dependencies
1274python() {
1275    pkgs = (d.getVar('PACKAGES') or '').split()
1276    for pkg in pkgs:
1277        d.appendVarFlag("do_package_qa", "vardeps", " INSANE_SKIP:{}".format(pkg))
1278}
1279
1280SSTATETASKS += "do_package_qa"
1281do_package_qa[sstate-inputdirs] = ""
1282do_package_qa[sstate-outputdirs] = ""
1283python do_package_qa_setscene () {
1284    sstate_setscene(d)
1285}
1286addtask do_package_qa_setscene
1287
1288python do_qa_sysroot() {
1289    bb.note("QA checking do_populate_sysroot")
1290    sysroot_destdir = d.expand('${SYSROOT_DESTDIR}')
1291    for sysroot_dir in d.expand('${SYSROOT_DIRS}').split():
1292        qa_check_staged(sysroot_destdir + sysroot_dir, d)
1293    oe.qa.exit_with_message_if_errors("do_populate_sysroot for this recipe installed files with QA issues", d)
1294}
1295do_populate_sysroot[postfuncs] += "do_qa_sysroot"
1296
1297python do_qa_patch() {
1298    import subprocess
1299
1300    ###########################################################################
1301    # Check patch.log for fuzz warnings
1302    #
1303    # Further information on why we check for patch fuzz warnings:
1304    # http://lists.openembedded.org/pipermail/openembedded-core/2018-March/148675.html
1305    # https://bugzilla.yoctoproject.org/show_bug.cgi?id=10450
1306    ###########################################################################
1307
1308    logdir = d.getVar('T')
1309    patchlog = os.path.join(logdir,"log.do_patch")
1310
1311    if os.path.exists(patchlog):
1312        fuzzheader = '--- Patch fuzz start ---'
1313        fuzzfooter = '--- Patch fuzz end ---'
1314        statement = "grep -e '%s' %s > /dev/null" % (fuzzheader, patchlog)
1315        if subprocess.call(statement, shell=True) == 0:
1316            msg = "Fuzz detected:\n\n"
1317            fuzzmsg = ""
1318            inFuzzInfo = False
1319            f = open(patchlog, "r")
1320            for line in f:
1321                if fuzzheader in line:
1322                    inFuzzInfo = True
1323                    fuzzmsg = ""
1324                elif fuzzfooter in line:
1325                    fuzzmsg = fuzzmsg.replace('\n\n', '\n')
1326                    msg += fuzzmsg
1327                    msg += "\n"
1328                    inFuzzInfo = False
1329                elif inFuzzInfo and not 'Now at patch' in line:
1330                    fuzzmsg += line
1331            f.close()
1332            msg += "The context lines in the patches can be updated with devtool:\n"
1333            msg += "\n"
1334            msg += "    devtool modify %s\n" % d.getVar('PN')
1335            msg += "    devtool finish --force-patch-refresh %s <layer_path>\n\n" % d.getVar('PN')
1336            msg += "Don't forget to review changes done by devtool!\n"
1337            if bb.utils.filter('ERROR_QA', 'patch-fuzz', d):
1338                bb.error(msg)
1339            elif bb.utils.filter('WARN_QA', 'patch-fuzz', d):
1340                bb.warn(msg)
1341            msg = "Patch log indicates that patches do not apply cleanly."
1342            oe.qa.handle_error("patch-fuzz", msg, d)
1343
1344    # Check if the patch contains a correctly formatted and spelled Upstream-Status
1345    import re
1346    from oe import patch
1347
1348    coremeta_path = os.path.join(d.getVar('COREBASE'), 'meta', '')
1349    for url in patch.src_patches(d):
1350       (_, _, fullpath, _, _, _) = bb.fetch.decodeurl(url)
1351
1352       # skip patches not in oe-core
1353       if not os.path.abspath(fullpath).startswith(coremeta_path):
1354           continue
1355
1356       kinda_status_re = re.compile(r"^.*upstream.*status.*$", re.IGNORECASE | re.MULTILINE)
1357       strict_status_re = re.compile(r"^Upstream-Status: (Pending|Submitted|Denied|Accepted|Inappropriate|Backport|Inactive-Upstream)( .+)?$", re.MULTILINE)
1358       guidelines = "https://www.openembedded.org/wiki/Commit_Patch_Message_Guidelines#Patch_Header_Recommendations:_Upstream-Status"
1359
1360       with open(fullpath, encoding='utf-8', errors='ignore') as f:
1361           file_content = f.read()
1362           match_kinda = kinda_status_re.search(file_content)
1363           match_strict = strict_status_re.search(file_content)
1364
1365           if not match_strict:
1366               if match_kinda:
1367                   bb.error("Malformed Upstream-Status in patch\n%s\nPlease correct according to %s :\n%s" % (fullpath, guidelines, match_kinda.group(0)))
1368               else:
1369                   bb.error("Missing Upstream-Status in patch\n%s\nPlease add according to %s ." % (fullpath, guidelines))
1370}
1371
1372python do_qa_configure() {
1373    import subprocess
1374
1375    ###########################################################################
1376    # Check config.log for cross compile issues
1377    ###########################################################################
1378
1379    configs = []
1380    workdir = d.getVar('WORKDIR')
1381
1382    skip = (d.getVar('INSANE_SKIP') or "").split()
1383    skip_configure_unsafe = False
1384    if 'configure-unsafe' in skip:
1385        bb.note("Recipe %s skipping qa checking: configure-unsafe" % d.getVar('PN'))
1386        skip_configure_unsafe = True
1387
1388    if bb.data.inherits_class('autotools', d) and not skip_configure_unsafe:
1389        bb.note("Checking autotools environment for common misconfiguration")
1390        for root, dirs, files in os.walk(workdir):
1391            statement = "grep -q -F -e 'is unsafe for cross-compilation' %s" % \
1392                        os.path.join(root,"config.log")
1393            if "config.log" in files:
1394                if subprocess.call(statement, shell=True) == 0:
1395                    error_msg = """This autoconf log indicates errors, it looked at host include and/or library paths while determining system capabilities.
1396Rerun configure task after fixing this."""
1397                    oe.qa.handle_error("configure-unsafe", error_msg, d)
1398
1399            if "configure.ac" in files:
1400                configs.append(os.path.join(root,"configure.ac"))
1401            if "configure.in" in files:
1402                configs.append(os.path.join(root, "configure.in"))
1403
1404    ###########################################################################
1405    # Check gettext configuration and dependencies are correct
1406    ###########################################################################
1407
1408    skip_configure_gettext = False
1409    if 'configure-gettext' in skip:
1410        bb.note("Recipe %s skipping qa checking: configure-gettext" % d.getVar('PN'))
1411        skip_configure_gettext = True
1412
1413    cnf = d.getVar('EXTRA_OECONF') or ""
1414    if not ("gettext" in d.getVar('P') or "gcc-runtime" in d.getVar('P') or \
1415            "--disable-nls" in cnf or skip_configure_gettext):
1416        ml = d.getVar("MLPREFIX") or ""
1417        if bb.data.inherits_class('cross-canadian', d):
1418            gt = "nativesdk-gettext"
1419        else:
1420            gt = "gettext-native"
1421        deps = bb.utils.explode_deps(d.getVar('DEPENDS') or "")
1422        if gt not in deps:
1423            for config in configs:
1424                gnu = "grep \"^[[:space:]]*AM_GNU_GETTEXT\" %s >/dev/null" % config
1425                if subprocess.call(gnu, shell=True) == 0:
1426                    error_msg = "AM_GNU_GETTEXT used but no inherit gettext"
1427                    oe.qa.handle_error("configure-gettext", error_msg, d)
1428
1429    ###########################################################################
1430    # Check unrecognised configure options (with a white list)
1431    ###########################################################################
1432    if bb.data.inherits_class("autotools", d):
1433        bb.note("Checking configure output for unrecognised options")
1434        try:
1435            if bb.data.inherits_class("autotools", d):
1436                flag = "WARNING: unrecognized options:"
1437                log = os.path.join(d.getVar('B'), 'config.log')
1438            output = subprocess.check_output(['grep', '-F', flag, log]).decode("utf-8").replace(', ', ' ').replace('"', '')
1439            options = set()
1440            for line in output.splitlines():
1441                options |= set(line.partition(flag)[2].split())
1442            ignore_opts = set(d.getVar("UNKNOWN_CONFIGURE_OPT_IGNORE").split())
1443            options -= ignore_opts
1444            if options:
1445                pn = d.getVar('PN')
1446                error_msg = pn + ": configure was passed unrecognised options: " + " ".join(options)
1447                oe.qa.handle_error("unknown-configure-option", error_msg, d)
1448        except subprocess.CalledProcessError:
1449            pass
1450
1451    # Check invalid PACKAGECONFIG
1452    pkgconfig = (d.getVar("PACKAGECONFIG") or "").split()
1453    if pkgconfig:
1454        pkgconfigflags = d.getVarFlags("PACKAGECONFIG") or {}
1455        for pconfig in pkgconfig:
1456            if pconfig not in pkgconfigflags:
1457                pn = d.getVar('PN')
1458                error_msg = "%s: invalid PACKAGECONFIG: %s" % (pn, pconfig)
1459                oe.qa.handle_error("invalid-packageconfig", error_msg, d)
1460
1461    oe.qa.exit_if_errors(d)
1462}
1463
1464def unpack_check_src_uri(pn, d):
1465    import re
1466
1467    skip = (d.getVar('INSANE_SKIP') or "").split()
1468    if 'src-uri-bad' in skip:
1469        bb.note("Recipe %s skipping qa checking: src-uri-bad" % d.getVar('PN'))
1470        return
1471
1472    if "${PN}" in d.getVar("SRC_URI", False):
1473        oe.qa.handle_error("src-uri-bad", "%s: SRC_URI uses PN not BPN" % pn, d)
1474
1475    for url in d.getVar("SRC_URI").split():
1476        # Search for github and gitlab URLs that pull unstable archives (comment for future greppers)
1477        if re.search(r"git(hu|la)b\.com/.+/.+/archive/.+", url) or "//codeload.github.com/" in url:
1478            oe.qa.handle_error("src-uri-bad", "%s: SRC_URI uses unstable GitHub/GitLab archives, convert recipe to use git protocol" % pn, d)
1479
1480python do_qa_unpack() {
1481    src_uri = d.getVar('SRC_URI')
1482    s_dir = d.getVar('S')
1483    if src_uri and not os.path.exists(s_dir):
1484        bb.warn('%s: the directory %s (%s) pointed to by the S variable doesn\'t exist - please set S within the recipe to point to where the source has been unpacked to' % (d.getVar('PN'), d.getVar('S', False), s_dir))
1485
1486    unpack_check_src_uri(d.getVar('PN'), d)
1487}
1488
1489# Check for patch fuzz
1490do_patch[postfuncs] += "do_qa_patch "
1491
1492# Check broken config.log files, for packages requiring Gettext which
1493# don't have it in DEPENDS.
1494#addtask qa_configure after do_configure before do_compile
1495do_configure[postfuncs] += "do_qa_configure "
1496
1497# Check does S exist.
1498do_unpack[postfuncs] += "do_qa_unpack"
1499
1500python () {
1501    import re
1502
1503    tests = d.getVar('ALL_QA').split()
1504    if "desktop" in tests:
1505        d.appendVar("PACKAGE_DEPENDS", " desktop-file-utils-native")
1506
1507    ###########################################################################
1508    # Check various variables
1509    ###########################################################################
1510
1511    # Checking ${FILESEXTRAPATHS}
1512    extrapaths = (d.getVar("FILESEXTRAPATHS") or "")
1513    if '__default' not in extrapaths.split(":"):
1514        msg = "FILESEXTRAPATHS-variable, must always use :prepend (or :append)\n"
1515        msg += "type of assignment, and don't forget the colon.\n"
1516        msg += "Please assign it with the format of:\n"
1517        msg += "  FILESEXTRAPATHS:append := \":${THISDIR}/Your_Files_Path\" or\n"
1518        msg += "  FILESEXTRAPATHS:prepend := \"${THISDIR}/Your_Files_Path:\"\n"
1519        msg += "in your bbappend file\n\n"
1520        msg += "Your incorrect assignment is:\n"
1521        msg += "%s\n" % extrapaths
1522        bb.warn(msg)
1523
1524    overrides = d.getVar('OVERRIDES').split(':')
1525    pn = d.getVar('PN')
1526    if pn in overrides:
1527        msg = 'Recipe %s has PN of "%s" which is in OVERRIDES, this can result in unexpected behaviour.' % (d.getVar("FILE"), pn)
1528        oe.qa.handle_error("pn-overrides", msg, d)
1529    prog = re.compile(r'[A-Z]')
1530    if prog.search(pn):
1531        oe.qa.handle_error("uppercase-pn", 'PN: %s is upper case, this can result in unexpected behavior.' % pn, d)
1532
1533    # Some people mistakenly use DEPENDS:${PN} instead of DEPENDS and wonder
1534    # why it doesn't work.
1535    if (d.getVar(d.expand('DEPENDS:${PN}'))):
1536        oe.qa.handle_error("pkgvarcheck", "recipe uses DEPENDS:${PN}, should use DEPENDS", d)
1537
1538    issues = []
1539    if (d.getVar('PACKAGES') or "").split():
1540        for dep in (d.getVar('QADEPENDS') or "").split():
1541            d.appendVarFlag('do_package_qa', 'depends', " %s:do_populate_sysroot" % dep)
1542        for var in 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RCONFLICTS', 'RPROVIDES', 'RREPLACES', 'FILES', 'pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm', 'ALLOW_EMPTY':
1543            if d.getVar(var, False):
1544                issues.append(var)
1545
1546        fakeroot_tests = d.getVar('FAKEROOT_QA').split()
1547        if set(tests) & set(fakeroot_tests):
1548            d.setVarFlag('do_package_qa', 'fakeroot', '1')
1549            d.appendVarFlag('do_package_qa', 'depends', ' virtual/fakeroot-native:do_populate_sysroot')
1550    else:
1551        d.setVarFlag('do_package_qa', 'rdeptask', '')
1552    for i in issues:
1553        oe.qa.handle_error("pkgvarcheck", "%s: Variable %s is set as not being package specific, please fix this." % (d.getVar("FILE"), i), d)
1554
1555    if 'native-last' not in (d.getVar('INSANE_SKIP') or "").split():
1556        for native_class in ['native', 'nativesdk']:
1557            if bb.data.inherits_class(native_class, d):
1558
1559                inherited_classes = d.getVar('__inherit_cache', False) or []
1560                needle = "/" + native_class
1561
1562                bbclassextend = (d.getVar('BBCLASSEXTEND') or '').split()
1563                # BBCLASSEXTEND items are always added in the end
1564                skip_classes = bbclassextend
1565                if bb.data.inherits_class('native', d) or 'native' in bbclassextend:
1566                    # native also inherits nopackages and relocatable bbclasses
1567                    skip_classes.extend(['nopackages', 'relocatable'])
1568
1569                broken_order = []
1570                for class_item in reversed(inherited_classes):
1571                    if needle not in class_item:
1572                        for extend_item in skip_classes:
1573                            if '/%s.bbclass' % extend_item in class_item:
1574                                break
1575                        else:
1576                            pn = d.getVar('PN')
1577                            broken_order.append(os.path.basename(class_item))
1578                    else:
1579                        break
1580                if broken_order:
1581                    oe.qa.handle_error("native-last", "%s: native/nativesdk class is not inherited last, this can result in unexpected behaviour. "
1582                                             "Classes inherited after native/nativesdk: %s" % (pn, " ".join(broken_order)), d)
1583
1584    oe.qa.exit_if_errors(d)
1585}
1586