blob: ada8a7ef4e49d30a7f07217d3d583167d8f04762 [file] [log] [blame]
Patrick Williams92b42cb2022-09-03 06:53:57 -05001#
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
Andrew Geissler517393d2023-01-13 08:55:19 -0600509QAPATHTEST[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
Patrick Williams92b42cb2022-09-03 06:53:57 -0500637# 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 = []
Patrick Williams2390b1b2022-11-03 13:47:49 -0500686 try:
687 m = hashlib.new('MD5', usedforsecurity=False)
688 except TypeError:
689 m = hashlib.new('MD5')
Patrick Williams92b42cb2022-09-03 06:53:57 -0500690 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)
Patrick Williams7784c422022-11-17 07:29:11 -06001477 if re.search(r"git(hu|la)b\.com/.+/.+/archive/.+", url) or "//codeload.github.com/" in url:
Patrick Williams92b42cb2022-09-03 06:53:57 -05001478 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}