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