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