blob: e963001d09accfc0cb765c611460fd233ceb8805 [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 \
Patrick Williams73bd93f2024-02-20 08:07:48 -060037 32bit-time virtual-slash \
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'#!'):
Patrick Williams92b42cb2022-09-03 06:53:57 -0500100 try:
Andrew Geissler220dafd2023-10-04 10:18:08 -0500101 stanza.decode("utf-8")
Patrick Williams92b42cb2022-09-03 06:53:57 -0500102 except UnicodeDecodeError:
103 #If it is not a text file, it is not a script
104 return
105
106 if len(stanza) > 129:
Patrick Williams169d7bc2024-01-05 11:33:25 -0600107 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, name)))
Patrick Williams92b42cb2022-09-03 06:53:57 -0500108 return
109
110QAPATHTEST[libexec] = "package_qa_check_libexec"
111def package_qa_check_libexec(path,name, d, elf, messages):
112
113 # Skip the case where the default is explicitly /usr/libexec
114 libexec = d.getVar('libexecdir')
115 if libexec == "/usr/libexec":
116 return True
117
118 if 'libexec' in path.split(os.path.sep):
Patrick Williams169d7bc2024-01-05 11:33:25 -0600119 oe.qa.add_message(messages, "libexec", "%s: %s is using libexec please relocate to %s" % (name, package_qa_clean_path(path, d, name), libexec))
Patrick Williams92b42cb2022-09-03 06:53:57 -0500120 return False
121
122 return True
123
124QAPATHTEST[rpaths] = "package_qa_check_rpath"
125def package_qa_check_rpath(file,name, d, elf, messages):
126 """
127 Check for dangerous RPATHs
128 """
129 if not elf:
130 return
131
132 if os.path.islink(file):
133 return
134
135 bad_dirs = [d.getVar('BASE_WORKDIR'), d.getVar('STAGING_DIR_TARGET')]
136
137 phdrs = elf.run_objdump("-p", d)
138
139 import re
140 rpath_re = re.compile(r"\s+RPATH\s+(.*)")
141 for line in phdrs.split("\n"):
142 m = rpath_re.match(line)
143 if m:
144 rpath = m.group(1)
145 for dir in bad_dirs:
146 if dir in rpath:
147 oe.qa.add_message(messages, "rpaths", "package %s contains bad RPATH %s in file %s" % (name, rpath, file))
148
149QAPATHTEST[useless-rpaths] = "package_qa_check_useless_rpaths"
150def package_qa_check_useless_rpaths(file, name, d, elf, messages):
151 """
152 Check for RPATHs that are useless but not dangerous
153 """
154 def rpath_eq(a, b):
155 return os.path.normpath(a) == os.path.normpath(b)
156
157 if not elf:
158 return
159
160 if os.path.islink(file):
161 return
162
163 libdir = d.getVar("libdir")
164 base_libdir = d.getVar("base_libdir")
165
166 phdrs = elf.run_objdump("-p", d)
167
168 import re
169 rpath_re = re.compile(r"\s+RPATH\s+(.*)")
170 for line in phdrs.split("\n"):
171 m = rpath_re.match(line)
172 if m:
173 rpath = m.group(1)
174 if rpath_eq(rpath, libdir) or rpath_eq(rpath, base_libdir):
175 # The dynamic linker searches both these places anyway. There is no point in
176 # looking there again.
177 oe.qa.add_message(messages, "useless-rpaths", "%s: %s contains probably-redundant RPATH %s" % (name, package_qa_clean_path(file, d, name), rpath))
178
179QAPATHTEST[dev-so] = "package_qa_check_dev"
180def package_qa_check_dev(path, name, d, elf, messages):
181 """
182 Check for ".so" library symlinks in non-dev packages
183 """
184
185 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):
186 oe.qa.add_message(messages, "dev-so", "non -dev/-dbg/nativesdk- package %s contains symlink .so '%s'" % \
187 (name, package_qa_clean_path(path, d, name)))
188
189QAPATHTEST[dev-elf] = "package_qa_check_dev_elf"
190def package_qa_check_dev_elf(path, name, d, elf, messages):
191 """
192 Check that -dev doesn't contain real shared libraries. The test has to
193 check that the file is not a link and is an ELF object as some recipes
194 install link-time .so files that are linker scripts.
195 """
196 if name.endswith("-dev") and path.endswith(".so") and not os.path.islink(path) and elf:
197 oe.qa.add_message(messages, "dev-elf", "-dev package %s contains non-symlink .so '%s'" % \
198 (name, package_qa_clean_path(path, d, name)))
199
200QAPATHTEST[staticdev] = "package_qa_check_staticdev"
201def package_qa_check_staticdev(path, name, d, elf, messages):
202 """
203 Check for ".a" library in non-staticdev packages
204 There are a number of exceptions to this rule, -pic packages can contain
205 static libraries, the _nonshared.a belong with their -dev packages and
206 libgcc.a, libgcov.a will be skipped in their packages
207 """
208
209 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:
210 oe.qa.add_message(messages, "staticdev", "non -staticdev package contains static .a library: %s path '%s'" % \
Patrick Williams169d7bc2024-01-05 11:33:25 -0600211 (name, package_qa_clean_path(path, d, name)))
Patrick Williams92b42cb2022-09-03 06:53:57 -0500212
213QAPATHTEST[mime] = "package_qa_check_mime"
214def package_qa_check_mime(path, name, d, elf, messages):
215 """
216 Check if package installs mime types to /usr/share/mime/packages
217 while no inheriting mime.bbclass
218 """
219
220 if d.getVar("datadir") + "/mime/packages" in path and path.endswith('.xml') and not bb.data.inherits_class("mime", d):
221 oe.qa.add_message(messages, "mime", "package contains mime types but does not inherit mime: %s path '%s'" % \
Patrick Williams169d7bc2024-01-05 11:33:25 -0600222 (name, package_qa_clean_path(path, d, name)))
Patrick Williams92b42cb2022-09-03 06:53:57 -0500223
224QAPATHTEST[mime-xdg] = "package_qa_check_mime_xdg"
225def package_qa_check_mime_xdg(path, name, d, elf, messages):
226 """
227 Check if package installs desktop file containing MimeType and requires
228 mime-types.bbclass to create /usr/share/applications/mimeinfo.cache
229 """
230
231 if d.getVar("datadir") + "/applications" in path and path.endswith('.desktop') and not bb.data.inherits_class("mime-xdg", d):
232 mime_type_found = False
233 try:
234 with open(path, 'r') as f:
235 for line in f.read().split('\n'):
236 if 'MimeType' in line:
237 mime_type_found = True
238 break;
239 except:
240 # At least libreoffice installs symlinks with absolute paths that are dangling here.
241 # We could implement some magic but for few (one) recipes it is not worth the effort so just warn:
Patrick Williams169d7bc2024-01-05 11:33:25 -0600242 wstr = "%s cannot open %s - is it a symlink with absolute path?\n" % (name, package_qa_clean_path(path, d, name))
Patrick Williams92b42cb2022-09-03 06:53:57 -0500243 wstr += "Please check if (linked) file contains key 'MimeType'.\n"
244 pkgname = name
245 if name == d.getVar('PN'):
246 pkgname = '${PN}'
247 wstr += "If yes: add \'inhert mime-xdg\' and \'MIME_XDG_PACKAGES += \"%s\"\' / if no add \'INSANE_SKIP:%s += \"mime-xdg\"\' to recipe." % (pkgname, pkgname)
248 oe.qa.add_message(messages, "mime-xdg", wstr)
249 if mime_type_found:
Patrick Williams169d7bc2024-01-05 11:33:25 -0600250 oe.qa.add_message(messages, "mime-xdg", "%s: contains desktop file with key 'MimeType' but does not inhert mime-xdg: %s" % \
251 (name, package_qa_clean_path(path, d, name)))
Patrick Williams92b42cb2022-09-03 06:53:57 -0500252
253def package_qa_check_libdir(d):
254 """
255 Check for wrong library installation paths. For instance, catch
256 recipes installing /lib/bar.so when ${base_libdir}="lib32" or
257 installing in /usr/lib64 when ${libdir}="/usr/lib"
258 """
259 import re
260
261 pkgdest = d.getVar('PKGDEST')
262 base_libdir = d.getVar("base_libdir") + os.sep
263 libdir = d.getVar("libdir") + os.sep
264 libexecdir = d.getVar("libexecdir") + os.sep
265 exec_prefix = d.getVar("exec_prefix") + os.sep
266
267 messages = []
268
269 # The re's are purposely fuzzy, as some there are some .so.x.y.z files
270 # that don't follow the standard naming convention. It checks later
271 # that they are actual ELF files
272 lib_re = re.compile(r"^/lib.+\.so(\..+)?$")
273 exec_re = re.compile(r"^%s.*/lib.+\.so(\..+)?$" % exec_prefix)
274
275 for root, dirs, files in os.walk(pkgdest):
276 if root == pkgdest:
277 # Skip subdirectories for any packages with libdir in INSANE_SKIP
278 skippackages = []
279 for package in dirs:
280 if 'libdir' in (d.getVar('INSANE_SKIP:' + package) or "").split():
281 bb.note("Package %s skipping libdir QA test" % (package))
282 skippackages.append(package)
283 elif d.getVar('PACKAGE_DEBUG_SPLIT_STYLE') == 'debug-file-directory' and package.endswith("-dbg"):
284 bb.note("Package %s skipping libdir QA test for PACKAGE_DEBUG_SPLIT_STYLE equals debug-file-directory" % (package))
285 skippackages.append(package)
286 for package in skippackages:
287 dirs.remove(package)
288 for file in files:
289 full_path = os.path.join(root, file)
290 rel_path = os.path.relpath(full_path, pkgdest)
291 if os.sep in rel_path:
292 package, rel_path = rel_path.split(os.sep, 1)
293 rel_path = os.sep + rel_path
294 if lib_re.match(rel_path):
295 if base_libdir not in rel_path:
296 # make sure it's an actual ELF file
297 elf = oe.qa.ELFFile(full_path)
298 try:
299 elf.open()
300 messages.append("%s: found library in wrong location: %s" % (package, rel_path))
301 except (oe.qa.NotELFFileError):
302 pass
303 if exec_re.match(rel_path):
304 if libdir not in rel_path and libexecdir not in rel_path:
305 # make sure it's an actual ELF file
306 elf = oe.qa.ELFFile(full_path)
307 try:
308 elf.open()
309 messages.append("%s: found library in wrong location: %s" % (package, rel_path))
310 except (oe.qa.NotELFFileError):
311 pass
312
313 if messages:
314 oe.qa.handle_error("libdir", "\n".join(messages), d)
315
316QAPATHTEST[debug-files] = "package_qa_check_dbg"
317def package_qa_check_dbg(path, name, d, elf, messages):
318 """
319 Check for ".debug" files or directories outside of the dbg package
320 """
321
322 if not "-dbg" in name and not "-ptest" in name:
323 if '.debug' in path.split(os.path.sep):
Patrick Williams169d7bc2024-01-05 11:33:25 -0600324 oe.qa.add_message(messages, "debug-files", "%s: non debug package contains .debug directory %s" % \
325 (name, package_qa_clean_path(path, d, name)))
Patrick Williams92b42cb2022-09-03 06:53:57 -0500326
327QAPATHTEST[arch] = "package_qa_check_arch"
328def package_qa_check_arch(path,name,d, elf, messages):
329 """
330 Check if archs are compatible
331 """
332 import re, oe.elf
333
334 if not elf:
335 return
336
337 target_os = d.getVar('HOST_OS')
338 target_arch = d.getVar('HOST_ARCH')
339 provides = d.getVar('PROVIDES')
340 bpn = d.getVar('BPN')
341
342 if target_arch == "allarch":
343 pn = d.getVar('PN')
344 oe.qa.add_message(messages, "arch", pn + ": Recipe inherits the allarch class, but has packaged architecture-specific binaries")
345 return
346
347 # FIXME: Cross package confuse this check, so just skip them
348 for s in ['cross', 'nativesdk', 'cross-canadian']:
349 if bb.data.inherits_class(s, d):
350 return
351
352 # avoid following links to /usr/bin (e.g. on udev builds)
353 # we will check the files pointed to anyway...
354 if os.path.islink(path):
355 return
356
357 #if this will throw an exception, then fix the dict above
358 (machine, osabi, abiversion, littleendian, bits) \
359 = oe.elf.machine_dict(d)[target_os][target_arch]
360
361 # Check the architecture and endiannes of the binary
362 is_32 = (("virtual/kernel" in provides) or bb.data.inherits_class("module", d)) and \
363 (target_os == "linux-gnux32" or target_os == "linux-muslx32" or \
364 target_os == "linux-gnu_ilp32" or re.match(r'mips64.*32', d.getVar('DEFAULTTUNE')))
365 is_bpf = (oe.qa.elf_machine_to_string(elf.machine()) == "BPF")
366 if not ((machine == elf.machine()) or is_32 or is_bpf):
367 oe.qa.add_message(messages, "arch", "Architecture did not match (%s, expected %s) in %s" % \
368 (oe.qa.elf_machine_to_string(elf.machine()), oe.qa.elf_machine_to_string(machine), package_qa_clean_path(path, d, name)))
369 elif not ((bits == elf.abiSize()) or is_32 or is_bpf):
370 oe.qa.add_message(messages, "arch", "Bit size did not match (%d, expected %d) in %s" % \
371 (elf.abiSize(), bits, package_qa_clean_path(path, d, name)))
372 elif not ((littleendian == elf.isLittleEndian()) or is_bpf):
373 oe.qa.add_message(messages, "arch", "Endiannes did not match (%d, expected %d) in %s" % \
Patrick Williams169d7bc2024-01-05 11:33:25 -0600374 (elf.isLittleEndian(), littleendian, package_qa_clean_path(path, d, name)))
Patrick Williams92b42cb2022-09-03 06:53:57 -0500375
376QAPATHTEST[desktop] = "package_qa_check_desktop"
377def package_qa_check_desktop(path, name, d, elf, messages):
378 """
379 Run all desktop files through desktop-file-validate.
380 """
381 if path.endswith(".desktop"):
382 desktop_file_validate = os.path.join(d.getVar('STAGING_BINDIR_NATIVE'),'desktop-file-validate')
383 output = os.popen("%s %s" % (desktop_file_validate, path))
384 # This only produces output on errors
385 for l in output:
386 oe.qa.add_message(messages, "desktop", "Desktop file issue: " + l.strip())
387
388QAPATHTEST[textrel] = "package_qa_textrel"
389def package_qa_textrel(path, name, d, elf, messages):
390 """
391 Check if the binary contains relocations in .text
392 """
393
394 if not elf:
395 return
396
397 if os.path.islink(path):
398 return
399
400 phdrs = elf.run_objdump("-p", d)
401 sane = True
402
403 import re
404 textrel_re = re.compile(r"\s+TEXTREL\s+")
405 for line in phdrs.split("\n"):
406 if textrel_re.match(line):
407 sane = False
408 break
409
410 if not sane:
411 path = package_qa_clean_path(path, d, name)
412 oe.qa.add_message(messages, "textrel", "%s: ELF binary %s has relocations in .text" % (name, path))
413
414QAPATHTEST[ldflags] = "package_qa_hash_style"
415def package_qa_hash_style(path, name, d, elf, messages):
416 """
417 Check if the binary has the right hash style...
418 """
419
420 if not elf:
421 return
422
423 if os.path.islink(path):
424 return
425
426 gnu_hash = "--hash-style=gnu" in d.getVar('LDFLAGS')
427 if not gnu_hash:
428 gnu_hash = "--hash-style=both" in d.getVar('LDFLAGS')
429 if not gnu_hash:
430 return
431
432 sane = False
433 has_syms = False
434
435 phdrs = elf.run_objdump("-p", d)
436
437 # If this binary has symbols, we expect it to have GNU_HASH too.
438 for line in phdrs.split("\n"):
439 if "SYMTAB" in line:
440 has_syms = True
441 if "GNU_HASH" in line or "MIPS_XHASH" in line:
442 sane = True
443 if ("[mips32]" in line or "[mips64]" in line) and d.getVar('TCLIBC') == "musl":
444 sane = True
445 if has_syms and not sane:
446 path = package_qa_clean_path(path, d, name)
447 oe.qa.add_message(messages, "ldflags", "File %s in package %s doesn't have GNU_HASH (didn't pass LDFLAGS?)" % (path, name))
448
449
450QAPATHTEST[buildpaths] = "package_qa_check_buildpaths"
451def package_qa_check_buildpaths(path, name, d, elf, messages):
452 """
453 Check for build paths inside target files and error if paths are not
454 explicitly ignored.
455 """
456 import stat
457
458 # Ignore symlinks/devs/fifos
459 mode = os.lstat(path).st_mode
460 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):
461 return
462
463 tmpdir = bytes(d.getVar('TMPDIR'), encoding="utf-8")
464 with open(path, 'rb') as f:
465 file_content = f.read()
466 if tmpdir in file_content:
467 trimmed = path.replace(os.path.join (d.getVar("PKGDEST"), name), "")
468 oe.qa.add_message(messages, "buildpaths", "File %s in package %s contains reference to TMPDIR" % (trimmed, name))
469
470
471QAPATHTEST[xorg-driver-abi] = "package_qa_check_xorg_driver_abi"
472def package_qa_check_xorg_driver_abi(path, name, d, elf, messages):
473 """
474 Check that all packages containing Xorg drivers have ABI dependencies
475 """
476
477 # Skip dev, dbg or nativesdk packages
478 if name.endswith("-dev") or name.endswith("-dbg") or name.startswith("nativesdk-"):
479 return
480
481 driverdir = d.expand("${libdir}/xorg/modules/drivers/")
482 if driverdir in path and path.endswith(".so"):
483 mlprefix = d.getVar('MLPREFIX') or ''
484 for rdep in bb.utils.explode_deps(d.getVar('RDEPENDS:' + name) or ""):
485 if rdep.startswith("%sxorg-abi-" % mlprefix):
486 return
487 oe.qa.add_message(messages, "xorg-driver-abi", "Package %s contains Xorg driver (%s) but no xorg-abi- dependencies" % (name, os.path.basename(path)))
488
489QAPATHTEST[infodir] = "package_qa_check_infodir"
490def package_qa_check_infodir(path, name, d, elf, messages):
491 """
492 Check that /usr/share/info/dir isn't shipped in a particular package
493 """
494 infodir = d.expand("${infodir}/dir")
495
496 if infodir in path:
497 oe.qa.add_message(messages, "infodir", "The /usr/share/info/dir file is not meant to be shipped in a particular package.")
498
499QAPATHTEST[symlink-to-sysroot] = "package_qa_check_symlink_to_sysroot"
500def package_qa_check_symlink_to_sysroot(path, name, d, elf, messages):
501 """
502 Check that the package doesn't contain any absolute symlinks to the sysroot.
503 """
504 if os.path.islink(path):
505 target = os.readlink(path)
506 if os.path.isabs(target):
507 tmpdir = d.getVar('TMPDIR')
508 if target.startswith(tmpdir):
509 trimmed = path.replace(os.path.join (d.getVar("PKGDEST"), name), "")
510 oe.qa.add_message(messages, "symlink-to-sysroot", "Symlink %s in %s points to TMPDIR" % (trimmed, name))
511
Andrew Geissler517393d2023-01-13 08:55:19 -0600512QAPATHTEST[32bit-time] = "check_32bit_symbols"
513def check_32bit_symbols(path, packagename, d, elf, messages):
514 """
515 Check that ELF files do not use any 32 bit time APIs from glibc.
516 """
Patrick Williams56b44a92024-01-19 08:49:29 -0600517 thirtytwo_bit_time_archs = {'arm','armeb','mipsarcho32','powerpc','x86'}
Andrew Geissler8f840682023-07-21 09:09:43 -0500518 overrides = set(d.getVar('OVERRIDES').split(':'))
Patrick Williams56b44a92024-01-19 08:49:29 -0600519 if not (thirtytwo_bit_time_archs & overrides):
Andrew Geissler8f840682023-07-21 09:09:43 -0500520 return
521
Andrew Geissler517393d2023-01-13 08:55:19 -0600522 import re
523 # This list is manually constructed by searching the image folder of the
524 # glibc recipe for __USE_TIME_BITS64. There is no good way to do this
525 # automatically.
526 api32 = {
527 # /usr/include/time.h
528 "clock_getres", "clock_gettime", "clock_nanosleep", "clock_settime",
529 "ctime", "ctime_r", "difftime", "gmtime", "gmtime_r", "localtime",
530 "localtime_r", "mktime", "nanosleep", "time", "timegm", "timelocal",
531 "timer_gettime", "timer_settime", "timespec_get", "timespec_getres",
532 # /usr/include/bits/time.h
533 "clock_adjtime",
534 # /usr/include/signal.h
535 "sigtimedwait",
536 # /usr/include/sys/time.h
Patrick Williams56b44a92024-01-19 08:49:29 -0600537 "adjtime",
Andrew Geissler517393d2023-01-13 08:55:19 -0600538 "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:
Patrick Williams169d7bc2024-01-05 11:33:25 -0600908 for _, rdep_data in oe.packagedata.foreach_runtime_provider_pkgdata(d, rdepend):
Patrick Williams92b42cb2022-09-03 06:53:57 -0500909 if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
910 break
911 if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
912 continue
913 if rdep_data and 'PN' in rdep_data:
914 error_msg = "%s rdepends on %s, but it isn't a build dependency, missing %s in DEPENDS or PACKAGECONFIG?" % (pkg, rdepend, rdep_data['PN'])
915 else:
916 error_msg = "%s rdepends on %s, but it isn't a build dependency?" % (pkg, rdepend)
917 oe.qa.handle_error("build-deps", error_msg, d)
918
919 if "file-rdeps" not in skip:
920 ignored_file_rdeps = set(['/bin/sh', '/usr/bin/env', 'rtld(GNU_HASH)'])
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600921 if bb.utils.contains('DISTRO_FEATURES', 'usrmerge', True, False, d):
922 ignored_file_rdeps |= set(['/usr/bin/sh'])
Patrick Williams92b42cb2022-09-03 06:53:57 -0500923 if bb.data.inherits_class('nativesdk', d):
924 ignored_file_rdeps |= set(['/bin/bash', '/usr/bin/perl', 'perl'])
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600925 if bb.utils.contains('DISTRO_FEATURES', 'usrmerge', True, False, d):
926 ignored_file_rdeps |= set(['/usr/bin/bash'])
Patrick Williams92b42cb2022-09-03 06:53:57 -0500927 # For Saving the FILERDEPENDS
928 filerdepends = {}
929 rdep_data = oe.packagedata.read_subpkgdata(pkg, d)
930 for key in rdep_data:
931 if key.startswith("FILERDEPENDS:"):
932 for subkey in bb.utils.explode_deps(rdep_data[key]):
933 if subkey not in ignored_file_rdeps and \
934 not subkey.startswith('perl('):
935 # We already know it starts with FILERDEPENDS_
936 filerdepends[subkey] = key[13:]
937
938 if filerdepends:
939 done = rdepends[:]
940 # Add the rprovides of itself
941 if pkg not in done:
942 done.insert(0, pkg)
943
944 # The python is not a package, but python-core provides it, so
945 # skip checking /usr/bin/python if python is in the rdeps, in
946 # case there is a RDEPENDS:pkg = "python" in the recipe.
947 for py in [ d.getVar('MLPREFIX') + "python", "python" ]:
948 if py in done:
949 filerdepends.pop("/usr/bin/python",None)
950 done.remove(py)
951 for rdep in done:
952 # The file dependencies may contain package names, e.g.,
953 # perl
954 filerdepends.pop(rdep,None)
955
Patrick Williams169d7bc2024-01-05 11:33:25 -0600956 for _, rdep_data in oe.packagedata.foreach_runtime_provider_pkgdata(d, rdep, True):
957 for key in rdep_data:
958 if key.startswith("FILERPROVIDES:") or key.startswith("RPROVIDES:"):
959 for subkey in bb.utils.explode_deps(rdep_data[key]):
960 filerdepends.pop(subkey,None)
961 # Add the files list to the rprovides
962 if key.startswith("FILES_INFO:"):
963 # Use eval() to make it as a dict
964 for subkey in eval(rdep_data[key]):
965 filerdepends.pop(subkey,None)
966
Patrick Williams92b42cb2022-09-03 06:53:57 -0500967 if not filerdepends:
968 # Break if all the file rdepends are met
969 break
970 if filerdepends:
971 for key in filerdepends:
972 error_msg = "%s contained in package %s requires %s, but no providers found in RDEPENDS:%s?" % \
973 (filerdepends[key].replace(":%s" % pkg, "").replace("@underscore@", "_"), pkg, key, pkg)
974 oe.qa.handle_error("file-rdeps", error_msg, d)
975package_qa_check_rdepends[vardepsexclude] = "OVERRIDES"
976
977def package_qa_check_deps(pkg, pkgdest, d):
978
979 localdata = bb.data.createCopy(d)
980 localdata.setVar('OVERRIDES', pkg)
981
982 def check_valid_deps(var):
983 try:
984 rvar = bb.utils.explode_dep_versions2(localdata.getVar(var) or "")
985 except ValueError as e:
986 bb.fatal("%s:%s: %s" % (var, pkg, e))
987 for dep in rvar:
988 for v in rvar[dep]:
989 if v and not v.startswith(('< ', '= ', '> ', '<= ', '>=')):
990 error_msg = "%s:%s is invalid: %s (%s) only comparisons <, =, >, <=, and >= are allowed" % (var, pkg, dep, v)
991 oe.qa.handle_error("dep-cmp", error_msg, d)
992
993 check_valid_deps('RDEPENDS')
994 check_valid_deps('RRECOMMENDS')
995 check_valid_deps('RSUGGESTS')
996 check_valid_deps('RPROVIDES')
997 check_valid_deps('RREPLACES')
998 check_valid_deps('RCONFLICTS')
999
1000QAPKGTEST[usrmerge] = "package_qa_check_usrmerge"
1001def package_qa_check_usrmerge(pkg, d, messages):
1002
1003 pkgdest = d.getVar('PKGDEST')
1004 pkg_dir = pkgdest + os.sep + pkg + os.sep
1005 merged_dirs = ['bin', 'sbin', 'lib'] + d.getVar('MULTILIB_VARIANTS').split()
1006 for f in merged_dirs:
1007 if os.path.exists(pkg_dir + f) and not os.path.islink(pkg_dir + f):
1008 msg = "%s package is not obeying usrmerge distro feature. /%s should be relocated to /usr." % (pkg, f)
1009 oe.qa.add_message(messages, "usrmerge", msg)
1010 return False
1011 return True
1012
1013QAPKGTEST[perllocalpod] = "package_qa_check_perllocalpod"
1014def package_qa_check_perllocalpod(pkg, d, messages):
1015 """
1016 Check that the recipe didn't ship a perlocal.pod file, which shouldn't be
1017 installed in a distribution package. cpan.bbclass sets NO_PERLLOCAL=1 to
1018 handle this for most recipes.
1019 """
1020 import glob
1021 pkgd = oe.path.join(d.getVar('PKGDEST'), pkg)
1022 podpath = oe.path.join(pkgd, d.getVar("libdir"), "perl*", "*", "*", "perllocal.pod")
1023
1024 matches = glob.glob(podpath)
1025 if matches:
1026 matches = [package_qa_clean_path(path, d, pkg) for path in matches]
1027 msg = "%s contains perllocal.pod (%s), should not be installed" % (pkg, " ".join(matches))
1028 oe.qa.add_message(messages, "perllocalpod", msg)
1029
1030QAPKGTEST[expanded-d] = "package_qa_check_expanded_d"
1031def package_qa_check_expanded_d(package, d, messages):
1032 """
1033 Check for the expanded D (${D}) value in pkg_* and FILES
1034 variables, warn the user to use it correctly.
1035 """
1036 sane = True
1037 expanded_d = d.getVar('D')
1038
1039 for var in 'FILES','pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm':
1040 bbvar = d.getVar(var + ":" + package) or ""
1041 if expanded_d in bbvar:
1042 if var == 'FILES':
1043 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)
1044 sane = False
1045 else:
1046 oe.qa.add_message(messages, "expanded-d", "%s in %s recipe contains ${D}, it should be replaced by $D instead" % (var, package))
1047 sane = False
1048 return sane
1049
1050QAPKGTEST[unlisted-pkg-lics] = "package_qa_check_unlisted_pkg_lics"
1051def package_qa_check_unlisted_pkg_lics(package, d, messages):
1052 """
1053 Check that all licenses for a package are among the licenses for the recipe.
1054 """
1055 pkg_lics = d.getVar('LICENSE:' + package)
1056 if not pkg_lics:
1057 return True
1058
1059 recipe_lics_set = oe.license.list_licenses(d.getVar('LICENSE'))
1060 package_lics = oe.license.list_licenses(pkg_lics)
1061 unlisted = package_lics - recipe_lics_set
1062 if unlisted:
1063 oe.qa.add_message(messages, "unlisted-pkg-lics",
1064 "LICENSE:%s includes licenses (%s) that are not "
1065 "listed in LICENSE" % (package, ' '.join(unlisted)))
1066 return False
1067 obsolete = set(oe.license.obsolete_license_list()) & package_lics - recipe_lics_set
1068 if obsolete:
1069 oe.qa.add_message(messages, "obsolete-license",
1070 "LICENSE:%s includes obsolete licenses %s" % (package, ' '.join(obsolete)))
1071 return False
1072 return True
1073
1074QAPKGTEST[empty-dirs] = "package_qa_check_empty_dirs"
1075def package_qa_check_empty_dirs(pkg, d, messages):
1076 """
1077 Check for the existence of files in directories that are expected to be
1078 empty.
1079 """
1080
1081 pkgd = oe.path.join(d.getVar('PKGDEST'), pkg)
1082 for dir in (d.getVar('QA_EMPTY_DIRS') or "").split():
1083 empty_dir = oe.path.join(pkgd, dir)
1084 if os.path.exists(empty_dir) and os.listdir(empty_dir):
1085 recommendation = (d.getVar('QA_EMPTY_DIRS_RECOMMENDATION:' + dir) or
1086 "but it is expected to be empty")
1087 msg = "%s installs files in %s, %s" % (pkg, dir, recommendation)
1088 oe.qa.add_message(messages, "empty-dirs", msg)
1089
1090def package_qa_check_encoding(keys, encode, d):
1091 def check_encoding(key, enc):
1092 sane = True
1093 value = d.getVar(key)
1094 if value:
1095 try:
1096 s = value.encode(enc)
1097 except UnicodeDecodeError as e:
1098 error_msg = "%s has non %s characters" % (key,enc)
1099 sane = False
1100 oe.qa.handle_error("invalid-chars", error_msg, d)
1101 return sane
1102
1103 for key in keys:
1104 sane = check_encoding(key, encode)
1105 if not sane:
1106 break
1107
1108HOST_USER_UID := "${@os.getuid()}"
1109HOST_USER_GID := "${@os.getgid()}"
1110
1111QAPATHTEST[host-user-contaminated] = "package_qa_check_host_user"
1112def package_qa_check_host_user(path, name, d, elf, messages):
1113 """Check for paths outside of /home which are owned by the user running bitbake."""
1114
1115 if not os.path.lexists(path):
1116 return
1117
1118 dest = d.getVar('PKGDEST')
1119 pn = d.getVar('PN')
1120 home = os.path.join(dest, name, 'home')
1121 if path == home or path.startswith(home + os.sep):
1122 return
1123
1124 try:
1125 stat = os.lstat(path)
1126 except OSError as exc:
1127 import errno
1128 if exc.errno != errno.ENOENT:
1129 raise
1130 else:
1131 check_uid = int(d.getVar('HOST_USER_UID'))
1132 if stat.st_uid == check_uid:
1133 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))
1134 return False
1135
1136 check_gid = int(d.getVar('HOST_USER_GID'))
1137 if stat.st_gid == check_gid:
1138 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))
1139 return False
1140 return True
1141
1142QARECIPETEST[unhandled-features-check] = "package_qa_check_unhandled_features_check"
1143def package_qa_check_unhandled_features_check(pn, d, messages):
1144 if not bb.data.inherits_class('features_check', d):
1145 var_set = False
1146 for kind in ['DISTRO', 'MACHINE', 'COMBINED']:
1147 for var in ['ANY_OF_' + kind + '_FEATURES', 'REQUIRED_' + kind + '_FEATURES', 'CONFLICT_' + kind + '_FEATURES']:
1148 if d.getVar(var) is not None or d.hasOverrides(var):
1149 var_set = True
1150 if var_set:
1151 oe.qa.handle_error("unhandled-features-check", "%s: recipe doesn't inherit features_check" % pn, d)
1152
1153QARECIPETEST[missing-update-alternatives] = "package_qa_check_missing_update_alternatives"
1154def package_qa_check_missing_update_alternatives(pn, d, messages):
1155 # Look at all packages and find out if any of those sets ALTERNATIVE variable
1156 # without inheriting update-alternatives class
1157 for pkg in (d.getVar('PACKAGES') or '').split():
1158 if d.getVar('ALTERNATIVE:%s' % pkg) and not bb.data.inherits_class('update-alternatives', d):
1159 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)
1160
1161# The PACKAGE FUNC to scan each package
1162python do_package_qa () {
1163 import subprocess
1164 import oe.packagedata
1165
1166 bb.note("DO PACKAGE QA")
1167
1168 main_lic = d.getVar('LICENSE')
1169
1170 # Check for obsolete license references in main LICENSE (packages are checked below for any changes)
1171 main_licenses = oe.license.list_licenses(d.getVar('LICENSE'))
1172 obsolete = set(oe.license.obsolete_license_list()) & main_licenses
1173 if obsolete:
1174 oe.qa.handle_error("obsolete-license", "Recipe LICENSE includes obsolete licenses %s" % ' '.join(obsolete), d)
1175
1176 bb.build.exec_func("read_subpackage_metadata", d)
1177
1178 # Check non UTF-8 characters on recipe's metadata
1179 package_qa_check_encoding(['DESCRIPTION', 'SUMMARY', 'LICENSE', 'SECTION'], 'utf-8', d)
1180
1181 logdir = d.getVar('T')
1182 pn = d.getVar('PN')
1183
1184 # Scan the packages...
1185 pkgdest = d.getVar('PKGDEST')
1186 packages = set((d.getVar('PACKAGES') or '').split())
1187
1188 global pkgfiles
1189 pkgfiles = {}
1190 for pkg in packages:
1191 pkgfiles[pkg] = []
1192 pkgdir = os.path.join(pkgdest, pkg)
1193 for walkroot, dirs, files in os.walk(pkgdir):
1194 # Don't walk into top-level CONTROL or DEBIAN directories as these
1195 # are temporary directories created by do_package.
1196 if walkroot == pkgdir:
1197 for control in ("CONTROL", "DEBIAN"):
1198 if control in dirs:
1199 dirs.remove(control)
1200 for file in files:
1201 pkgfiles[pkg].append(os.path.join(walkroot, file))
1202
1203 # no packages should be scanned
1204 if not packages:
1205 return
1206
1207 import re
1208 # The package name matches the [a-z0-9.+-]+ regular expression
1209 pkgname_pattern = re.compile(r"^[a-z0-9.+-]+$")
1210
1211 taskdepdata = d.getVar("BB_TASKDEPDATA", False)
1212 taskdeps = set()
1213 for dep in taskdepdata:
1214 taskdeps.add(taskdepdata[dep][0])
1215
1216 def parse_test_matrix(matrix_name):
1217 testmatrix = d.getVarFlags(matrix_name) or {}
1218 g = globals()
1219 warnchecks = []
1220 for w in (d.getVar("WARN_QA") or "").split():
1221 if w in skip:
1222 continue
1223 if w in testmatrix and testmatrix[w] in g:
1224 warnchecks.append(g[testmatrix[w]])
1225
1226 errorchecks = []
1227 for e in (d.getVar("ERROR_QA") or "").split():
1228 if e in skip:
1229 continue
1230 if e in testmatrix and testmatrix[e] in g:
1231 errorchecks.append(g[testmatrix[e]])
1232 return warnchecks, errorchecks
1233
1234 for package in packages:
1235 skip = set((d.getVar('INSANE_SKIP') or "").split() +
1236 (d.getVar('INSANE_SKIP:' + package) or "").split())
1237 if skip:
1238 bb.note("Package %s skipping QA tests: %s" % (package, str(skip)))
1239
1240 bb.note("Checking Package: %s" % package)
1241 # Check package name
1242 if not pkgname_pattern.match(package):
1243 oe.qa.handle_error("pkgname",
1244 "%s doesn't match the [a-z0-9.+-]+ regex" % package, d)
1245
1246 warn_checks, error_checks = parse_test_matrix("QAPATHTEST")
1247 package_qa_walk(warn_checks, error_checks, package, d)
1248
1249 warn_checks, error_checks = parse_test_matrix("QAPKGTEST")
1250 package_qa_package(warn_checks, error_checks, package, d)
1251
1252 package_qa_check_rdepends(package, pkgdest, skip, taskdeps, packages, d)
1253 package_qa_check_deps(package, pkgdest, d)
1254
1255 warn_checks, error_checks = parse_test_matrix("QARECIPETEST")
1256 package_qa_recipe(warn_checks, error_checks, pn, d)
1257
1258 if 'libdir' in d.getVar("ALL_QA").split():
1259 package_qa_check_libdir(d)
1260
1261 oe.qa.exit_if_errors(d)
1262}
1263
1264# binutils is used for most checks, so need to set as dependency
1265# POPULATESYSROOTDEPS is defined in staging class.
1266do_package_qa[depends] += "${POPULATESYSROOTDEPS}"
1267do_package_qa[vardeps] = "${@bb.utils.contains('ERROR_QA', 'empty-dirs', 'QA_EMPTY_DIRS', '', d)}"
1268do_package_qa[vardepsexclude] = "BB_TASKDEPDATA"
1269do_package_qa[rdeptask] = "do_packagedata"
1270addtask do_package_qa after do_packagedata do_package before do_build
1271
1272# Add the package specific INSANE_SKIPs to the sstate dependencies
1273python() {
1274 pkgs = (d.getVar('PACKAGES') or '').split()
1275 for pkg in pkgs:
1276 d.appendVarFlag("do_package_qa", "vardeps", " INSANE_SKIP:{}".format(pkg))
1277}
1278
1279SSTATETASKS += "do_package_qa"
1280do_package_qa[sstate-inputdirs] = ""
1281do_package_qa[sstate-outputdirs] = ""
1282python do_package_qa_setscene () {
1283 sstate_setscene(d)
1284}
1285addtask do_package_qa_setscene
1286
1287python do_qa_sysroot() {
1288 bb.note("QA checking do_populate_sysroot")
1289 sysroot_destdir = d.expand('${SYSROOT_DESTDIR}')
1290 for sysroot_dir in d.expand('${SYSROOT_DIRS}').split():
1291 qa_check_staged(sysroot_destdir + sysroot_dir, d)
1292 oe.qa.exit_with_message_if_errors("do_populate_sysroot for this recipe installed files with QA issues", d)
1293}
1294do_populate_sysroot[postfuncs] += "do_qa_sysroot"
1295
1296python do_qa_patch() {
1297 import subprocess
1298
1299 ###########################################################################
1300 # Check patch.log for fuzz warnings
1301 #
1302 # Further information on why we check for patch fuzz warnings:
1303 # http://lists.openembedded.org/pipermail/openembedded-core/2018-March/148675.html
1304 # https://bugzilla.yoctoproject.org/show_bug.cgi?id=10450
1305 ###########################################################################
1306
1307 logdir = d.getVar('T')
1308 patchlog = os.path.join(logdir,"log.do_patch")
1309
1310 if os.path.exists(patchlog):
1311 fuzzheader = '--- Patch fuzz start ---'
1312 fuzzfooter = '--- Patch fuzz end ---'
1313 statement = "grep -e '%s' %s > /dev/null" % (fuzzheader, patchlog)
1314 if subprocess.call(statement, shell=True) == 0:
1315 msg = "Fuzz detected:\n\n"
1316 fuzzmsg = ""
1317 inFuzzInfo = False
1318 f = open(patchlog, "r")
1319 for line in f:
1320 if fuzzheader in line:
1321 inFuzzInfo = True
1322 fuzzmsg = ""
1323 elif fuzzfooter in line:
1324 fuzzmsg = fuzzmsg.replace('\n\n', '\n')
1325 msg += fuzzmsg
1326 msg += "\n"
1327 inFuzzInfo = False
1328 elif inFuzzInfo and not 'Now at patch' in line:
1329 fuzzmsg += line
1330 f.close()
1331 msg += "The context lines in the patches can be updated with devtool:\n"
1332 msg += "\n"
1333 msg += " devtool modify %s\n" % d.getVar('PN')
1334 msg += " devtool finish --force-patch-refresh %s <layer_path>\n\n" % d.getVar('PN')
1335 msg += "Don't forget to review changes done by devtool!\n"
Andrew Geisslerc5535c92023-01-27 16:10:19 -06001336 msg += "\nPatch log indicates that patches do not apply cleanly."
Patrick Williams92b42cb2022-09-03 06:53:57 -05001337 oe.qa.handle_error("patch-fuzz", msg, d)
1338
1339 # Check if the patch contains a correctly formatted and spelled Upstream-Status
1340 import re
1341 from oe import patch
1342
Patrick Williams92b42cb2022-09-03 06:53:57 -05001343 for url in patch.src_patches(d):
Andrew Geissler6aa7eec2023-03-03 12:41:14 -06001344 (_, _, fullpath, _, _, _) = bb.fetch.decodeurl(url)
Patrick Williams92b42cb2022-09-03 06:53:57 -05001345
Andrew Geissler6aa7eec2023-03-03 12:41:14 -06001346 msg = oe.qa.check_upstream_status(fullpath)
1347 if msg:
Patrick Williams520786c2023-06-25 16:20:36 -05001348 oe.qa.handle_error("patch-status", msg, d)
Andrew Geisslerc5535c92023-01-27 16:10:19 -06001349
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001350 ###########################################################################
1351 # Check for missing ptests
1352 ###########################################################################
1353 def match_line_in_files(toplevel, filename_glob, line_regex):
1354 import pathlib
1355 try:
1356 toppath = pathlib.Path(toplevel)
1357 for entry in toppath.glob(filename_glob):
1358 try:
1359 with open(entry, 'r', encoding='utf-8', errors='ignore') as f:
1360 for line in f.readlines():
1361 if re.match(line_regex, line):
1362 return True
1363 except FileNotFoundError:
1364 # Broken symlink in source
1365 pass
1366 except FileNotFoundError:
1367 # pathlib.Path.glob() might throw this when file/directory
1368 # disappear while scanning.
1369 bb.note("unimplemented-ptest: FileNotFoundError exception while scanning (disappearing file while scanning?). Check was ignored." % d.getVar('PN'))
1370 pass
1371 return False
1372
1373 srcdir = d.getVar('S')
1374 if not bb.utils.contains('DISTRO_FEATURES', 'ptest', True, False, d):
1375 pass
1376 elif bb.data.inherits_class('ptest', d):
1377 bb.note("Package %s QA: skipping unimplemented-ptest: ptest implementation detected" % d.getVar('PN'))
1378 elif srcdir == d.getVar('WORKDIR'):
1379 bb.note("Package %s QA: skipping unimplemented-ptest: This check is not supported for recipe with \"S = \"${WORKDIR}\"" % d.getVar('PN'))
1380
1381 # Detect perl Test:: based tests
1382 elif os.path.exists(os.path.join(srcdir, "t")) and any(filename.endswith('.t') for filename in os.listdir(os.path.join(srcdir, 't'))):
1383 oe.qa.handle_error("unimplemented-ptest", "%s: perl Test:: based tests detected" % d.getVar('PN'), d)
1384
1385 # Detect pytest-based tests
1386 elif match_line_in_files(srcdir, "**/*.py", r'\s*(?:import\s*pytest|from\s*pytest)'):
1387 oe.qa.handle_error("unimplemented-ptest", "%s: pytest-based tests detected" % d.getVar('PN'), d)
1388
1389 # Detect meson-based tests
1390 elif os.path.exists(os.path.join(srcdir, "meson.build")) and match_line_in_files(srcdir, "**/meson.build", r'\s*test\s*\('):
1391 oe.qa.handle_error("unimplemented-ptest", "%s: meson-based tests detected" % d.getVar('PN'), d)
1392
1393 # Detect cmake-based tests
1394 elif os.path.exists(os.path.join(srcdir, "CMakeLists.txt")) and match_line_in_files(srcdir, "**/CMakeLists.txt", r'\s*(?:add_test|enable_testing)\s*\('):
1395 oe.qa.handle_error("unimplemented-ptest", "%s: cmake-based tests detected" % d.getVar('PN'), d)
1396
1397 # Detect autotools-based·tests
1398 elif os.path.exists(os.path.join(srcdir, "Makefile.in")) and (match_line_in_files(srcdir, "**/Makefile.in", r'\s*TESTS\s*\+?=') or match_line_in_files(srcdir,"**/*.at",r'.*AT_INIT')):
1399 oe.qa.handle_error("unimplemented-ptest", "%s: autotools-based tests detected" % d.getVar('PN'), d)
1400
1401 # Last resort, detect a test directory in sources
1402 elif any(filename.lower() in ["test", "tests"] for filename in os.listdir(srcdir)):
1403 oe.qa.handle_error("unimplemented-ptest", "%s: test subdirectory detected" % d.getVar('PN'), d)
1404
Andrew Geisslerc5535c92023-01-27 16:10:19 -06001405 oe.qa.exit_if_errors(d)
Patrick Williams92b42cb2022-09-03 06:53:57 -05001406}
1407
1408python do_qa_configure() {
1409 import subprocess
1410
1411 ###########################################################################
1412 # Check config.log for cross compile issues
1413 ###########################################################################
1414
1415 configs = []
1416 workdir = d.getVar('WORKDIR')
1417
1418 skip = (d.getVar('INSANE_SKIP') or "").split()
1419 skip_configure_unsafe = False
1420 if 'configure-unsafe' in skip:
1421 bb.note("Recipe %s skipping qa checking: configure-unsafe" % d.getVar('PN'))
1422 skip_configure_unsafe = True
1423
1424 if bb.data.inherits_class('autotools', d) and not skip_configure_unsafe:
1425 bb.note("Checking autotools environment for common misconfiguration")
1426 for root, dirs, files in os.walk(workdir):
1427 statement = "grep -q -F -e 'is unsafe for cross-compilation' %s" % \
1428 os.path.join(root,"config.log")
1429 if "config.log" in files:
1430 if subprocess.call(statement, shell=True) == 0:
1431 error_msg = """This autoconf log indicates errors, it looked at host include and/or library paths while determining system capabilities.
1432Rerun configure task after fixing this."""
1433 oe.qa.handle_error("configure-unsafe", error_msg, d)
1434
1435 if "configure.ac" in files:
1436 configs.append(os.path.join(root,"configure.ac"))
1437 if "configure.in" in files:
1438 configs.append(os.path.join(root, "configure.in"))
1439
1440 ###########################################################################
1441 # Check gettext configuration and dependencies are correct
1442 ###########################################################################
1443
1444 skip_configure_gettext = False
1445 if 'configure-gettext' in skip:
1446 bb.note("Recipe %s skipping qa checking: configure-gettext" % d.getVar('PN'))
1447 skip_configure_gettext = True
1448
1449 cnf = d.getVar('EXTRA_OECONF') or ""
1450 if not ("gettext" in d.getVar('P') or "gcc-runtime" in d.getVar('P') or \
1451 "--disable-nls" in cnf or skip_configure_gettext):
1452 ml = d.getVar("MLPREFIX") or ""
1453 if bb.data.inherits_class('cross-canadian', d):
1454 gt = "nativesdk-gettext"
1455 else:
1456 gt = "gettext-native"
1457 deps = bb.utils.explode_deps(d.getVar('DEPENDS') or "")
1458 if gt not in deps:
1459 for config in configs:
1460 gnu = "grep \"^[[:space:]]*AM_GNU_GETTEXT\" %s >/dev/null" % config
1461 if subprocess.call(gnu, shell=True) == 0:
1462 error_msg = "AM_GNU_GETTEXT used but no inherit gettext"
1463 oe.qa.handle_error("configure-gettext", error_msg, d)
1464
1465 ###########################################################################
1466 # Check unrecognised configure options (with a white list)
1467 ###########################################################################
1468 if bb.data.inherits_class("autotools", d):
1469 bb.note("Checking configure output for unrecognised options")
1470 try:
1471 if bb.data.inherits_class("autotools", d):
1472 flag = "WARNING: unrecognized options:"
1473 log = os.path.join(d.getVar('B'), 'config.log')
1474 output = subprocess.check_output(['grep', '-F', flag, log]).decode("utf-8").replace(', ', ' ').replace('"', '')
1475 options = set()
1476 for line in output.splitlines():
1477 options |= set(line.partition(flag)[2].split())
1478 ignore_opts = set(d.getVar("UNKNOWN_CONFIGURE_OPT_IGNORE").split())
1479 options -= ignore_opts
1480 if options:
1481 pn = d.getVar('PN')
1482 error_msg = pn + ": configure was passed unrecognised options: " + " ".join(options)
1483 oe.qa.handle_error("unknown-configure-option", error_msg, d)
1484 except subprocess.CalledProcessError:
1485 pass
1486
1487 # Check invalid PACKAGECONFIG
1488 pkgconfig = (d.getVar("PACKAGECONFIG") or "").split()
1489 if pkgconfig:
1490 pkgconfigflags = d.getVarFlags("PACKAGECONFIG") or {}
1491 for pconfig in pkgconfig:
1492 if pconfig not in pkgconfigflags:
1493 pn = d.getVar('PN')
1494 error_msg = "%s: invalid PACKAGECONFIG: %s" % (pn, pconfig)
1495 oe.qa.handle_error("invalid-packageconfig", error_msg, d)
1496
1497 oe.qa.exit_if_errors(d)
1498}
1499
Patrick Williams92b42cb2022-09-03 06:53:57 -05001500python do_qa_unpack() {
1501 src_uri = d.getVar('SRC_URI')
1502 s_dir = d.getVar('S')
1503 if src_uri and not os.path.exists(s_dir):
1504 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))
Patrick Williams92b42cb2022-09-03 06:53:57 -05001505}
1506
Patrick Williams2a254922023-08-11 09:48:11 -05001507python do_recipe_qa() {
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001508 import re
1509
1510 def test_missing_metadata(pn, d):
Patrick Williams2a254922023-08-11 09:48:11 -05001511 fn = d.getVar("FILE")
Patrick Williams2a254922023-08-11 09:48:11 -05001512 srcfile = d.getVar('SRC_URI').split()
1513 # Check that SUMMARY is not the same as the default from bitbake.conf
1514 if d.getVar('SUMMARY') == d.expand("${PN} version ${PV}-${PR}"):
1515 oe.qa.handle_error("missing-metadata", "Recipe {} in {} does not contain a SUMMARY. Please add an entry.".format(pn, fn), d)
1516 if not d.getVar('HOMEPAGE'):
1517 if srcfile and srcfile[0].startswith('file') or not d.getVar('SRC_URI'):
1518 # We are only interested in recipes SRC_URI fetched from external sources
1519 pass
1520 else:
1521 oe.qa.handle_error("missing-metadata", "Recipe {} in {} does not contain a HOMEPAGE. Please add an entry.".format(pn, fn), d)
1522
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001523 def test_missing_maintainer(pn, d):
Patrick Williams2a254922023-08-11 09:48:11 -05001524 fn = d.getVar("FILE")
Patrick Williams2a254922023-08-11 09:48:11 -05001525 if pn.endswith("-native") or pn.startswith("nativesdk-") or "packagegroup-" in pn or "core-image-ptest-" in pn:
1526 return
1527 if not d.getVar('RECIPE_MAINTAINER'):
1528 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)
1529
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001530 def test_srcuri(pn, d):
1531 skip = (d.getVar('INSANE_SKIP') or "").split()
1532 if 'src-uri-bad' in skip:
1533 bb.note("Recipe %s skipping qa checking: src-uri-bad" % pn)
1534 return
1535
1536 if "${PN}" in d.getVar("SRC_URI", False):
1537 oe.qa.handle_error("src-uri-bad", "%s: SRC_URI uses PN not BPN" % pn, d)
1538
1539 for url in d.getVar("SRC_URI").split():
1540 # Search for github and gitlab URLs that pull unstable archives (comment for future greppers)
1541 if re.search(r"git(hu|la)b\.com/.+/.+/archive/.+", url) or "//codeload.github.com/" in url:
1542 oe.qa.handle_error("src-uri-bad", "%s: SRC_URI uses unstable GitHub/GitLab archives, convert recipe to use git protocol" % pn, d)
1543
1544 pn = d.getVar('PN')
1545 test_missing_metadata(pn, d)
1546 test_missing_maintainer(pn, d)
1547 test_srcuri(pn, d)
Patrick Williams2a254922023-08-11 09:48:11 -05001548 oe.qa.exit_if_errors(d)
1549}
1550
1551addtask do_recipe_qa before do_fetch do_package_qa do_build
1552
1553SSTATETASKS += "do_recipe_qa"
1554do_recipe_qa[sstate-inputdirs] = ""
1555do_recipe_qa[sstate-outputdirs] = ""
1556python do_recipe_qa_setscene () {
1557 sstate_setscene(d)
1558}
1559addtask do_recipe_qa_setscene
1560
Patrick Williams92b42cb2022-09-03 06:53:57 -05001561# Check for patch fuzz
1562do_patch[postfuncs] += "do_qa_patch "
1563
1564# Check broken config.log files, for packages requiring Gettext which
1565# don't have it in DEPENDS.
1566#addtask qa_configure after do_configure before do_compile
1567do_configure[postfuncs] += "do_qa_configure "
1568
1569# Check does S exist.
1570do_unpack[postfuncs] += "do_qa_unpack"
1571
1572python () {
1573 import re
Patrick Williams56b44a92024-01-19 08:49:29 -06001574
Patrick Williams92b42cb2022-09-03 06:53:57 -05001575 tests = d.getVar('ALL_QA').split()
1576 if "desktop" in tests:
1577 d.appendVar("PACKAGE_DEPENDS", " desktop-file-utils-native")
1578
1579 ###########################################################################
1580 # Check various variables
1581 ###########################################################################
1582
1583 # Checking ${FILESEXTRAPATHS}
1584 extrapaths = (d.getVar("FILESEXTRAPATHS") or "")
1585 if '__default' not in extrapaths.split(":"):
1586 msg = "FILESEXTRAPATHS-variable, must always use :prepend (or :append)\n"
1587 msg += "type of assignment, and don't forget the colon.\n"
1588 msg += "Please assign it with the format of:\n"
1589 msg += " FILESEXTRAPATHS:append := \":${THISDIR}/Your_Files_Path\" or\n"
1590 msg += " FILESEXTRAPATHS:prepend := \"${THISDIR}/Your_Files_Path:\"\n"
1591 msg += "in your bbappend file\n\n"
1592 msg += "Your incorrect assignment is:\n"
1593 msg += "%s\n" % extrapaths
1594 bb.warn(msg)
1595
1596 overrides = d.getVar('OVERRIDES').split(':')
1597 pn = d.getVar('PN')
1598 if pn in overrides:
1599 msg = 'Recipe %s has PN of "%s" which is in OVERRIDES, this can result in unexpected behaviour.' % (d.getVar("FILE"), pn)
1600 oe.qa.handle_error("pn-overrides", msg, d)
1601 prog = re.compile(r'[A-Z]')
1602 if prog.search(pn):
1603 oe.qa.handle_error("uppercase-pn", 'PN: %s is upper case, this can result in unexpected behavior.' % pn, d)
1604
1605 # Some people mistakenly use DEPENDS:${PN} instead of DEPENDS and wonder
1606 # why it doesn't work.
1607 if (d.getVar(d.expand('DEPENDS:${PN}'))):
1608 oe.qa.handle_error("pkgvarcheck", "recipe uses DEPENDS:${PN}, should use DEPENDS", d)
1609
Patrick Williams73bd93f2024-02-20 08:07:48 -06001610 # virtual/ is meaningless for these variables
1611 if "virtual-slash" in (d.getVar("ALL_QA") or "").split():
1612 for k in ['RDEPENDS', 'RPROVIDES']:
1613 for var in bb.utils.explode_deps(d.getVar(k + ':' + pn) or ""):
1614 if var.startswith("virtual/"):
1615 oe.qa.handle_error("virtual-slash", "%s is set to %s but the substring 'virtual/' holds no meaning in this context. It only works for build time dependencies, not runtime ones. It is suggested to use 'VIRTUAL-RUNTIME_' variables instead." % (k, var), d)
Patrick Williams56b44a92024-01-19 08:49:29 -06001616
Patrick Williams92b42cb2022-09-03 06:53:57 -05001617 issues = []
1618 if (d.getVar('PACKAGES') or "").split():
1619 for dep in (d.getVar('QADEPENDS') or "").split():
1620 d.appendVarFlag('do_package_qa', 'depends', " %s:do_populate_sysroot" % dep)
1621 for var in 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RCONFLICTS', 'RPROVIDES', 'RREPLACES', 'FILES', 'pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm', 'ALLOW_EMPTY':
1622 if d.getVar(var, False):
1623 issues.append(var)
1624
1625 fakeroot_tests = d.getVar('FAKEROOT_QA').split()
1626 if set(tests) & set(fakeroot_tests):
1627 d.setVarFlag('do_package_qa', 'fakeroot', '1')
1628 d.appendVarFlag('do_package_qa', 'depends', ' virtual/fakeroot-native:do_populate_sysroot')
1629 else:
1630 d.setVarFlag('do_package_qa', 'rdeptask', '')
1631 for i in issues:
1632 oe.qa.handle_error("pkgvarcheck", "%s: Variable %s is set as not being package specific, please fix this." % (d.getVar("FILE"), i), d)
1633
1634 if 'native-last' not in (d.getVar('INSANE_SKIP') or "").split():
1635 for native_class in ['native', 'nativesdk']:
1636 if bb.data.inherits_class(native_class, d):
1637
1638 inherited_classes = d.getVar('__inherit_cache', False) or []
1639 needle = "/" + native_class
1640
1641 bbclassextend = (d.getVar('BBCLASSEXTEND') or '').split()
1642 # BBCLASSEXTEND items are always added in the end
1643 skip_classes = bbclassextend
1644 if bb.data.inherits_class('native', d) or 'native' in bbclassextend:
1645 # native also inherits nopackages and relocatable bbclasses
1646 skip_classes.extend(['nopackages', 'relocatable'])
1647
1648 broken_order = []
1649 for class_item in reversed(inherited_classes):
1650 if needle not in class_item:
1651 for extend_item in skip_classes:
1652 if '/%s.bbclass' % extend_item in class_item:
1653 break
1654 else:
1655 pn = d.getVar('PN')
1656 broken_order.append(os.path.basename(class_item))
1657 else:
1658 break
1659 if broken_order:
1660 oe.qa.handle_error("native-last", "%s: native/nativesdk class is not inherited last, this can result in unexpected behaviour. "
1661 "Classes inherited after native/nativesdk: %s" % (pn, " ".join(broken_order)), d)
1662
1663 oe.qa.exit_if_errors(d)
1664}