blob: d625fd82f733127c47e62d2131978ad5775a7fc8 [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'#!'):
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 """
Andrew Geissler8f840682023-07-21 09:09:43 -0500517 thirtytwo_bit_time_archs = set(('arm','armeb','mipsarcho32','powerpc','x86'))
518 overrides = set(d.getVar('OVERRIDES').split(':'))
519 if not(thirtytwo_bit_time_archs & overrides):
520 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
537 "futimes", "futimesat", "getitimer", "gettimeofday", "lutimes",
538 "setitimer", "settimeofday", "utimes",
539 # /usr/include/sys/timex.h
540 "adjtimex", "ntp_adjtime", "ntp_gettime", "ntp_gettimex",
541 # /usr/include/sys/wait.h
542 "wait3", "wait4",
543 # /usr/include/sys/stat.h
544 "fstat", "fstat64", "fstatat", "fstatat64", "futimens", "lstat",
545 "lstat64", "stat", "stat64", "utimensat",
546 # /usr/include/sys/poll.h
547 "ppoll",
548 # /usr/include/sys/resource.h
549 "getrusage",
550 # /usr/include/sys/ioctl.h
551 "ioctl",
552 # /usr/include/sys/select.h
553 "select", "pselect",
554 # /usr/include/sys/prctl.h
555 "prctl",
556 # /usr/include/sys/epoll.h
557 "epoll_pwait2",
558 # /usr/include/sys/timerfd.h
559 "timerfd_gettime", "timerfd_settime",
560 # /usr/include/sys/socket.h
561 "getsockopt", "recvmmsg", "recvmsg", "sendmmsg", "sendmsg",
562 "setsockopt",
563 # /usr/include/sys/msg.h
564 "msgctl",
565 # /usr/include/sys/sem.h
566 "semctl", "semtimedop",
567 # /usr/include/sys/shm.h
568 "shmctl",
569 # /usr/include/pthread.h
570 "pthread_clockjoin_np", "pthread_cond_clockwait",
571 "pthread_cond_timedwait", "pthread_mutex_clocklock",
572 "pthread_mutex_timedlock", "pthread_rwlock_clockrdlock",
573 "pthread_rwlock_clockwrlock", "pthread_rwlock_timedrdlock",
574 "pthread_rwlock_timedwrlock", "pthread_timedjoin_np",
575 # /usr/include/semaphore.h
576 "sem_clockwait", "sem_timedwait",
577 # /usr/include/threads.h
578 "cnd_timedwait", "mtx_timedlock", "thrd_sleep",
579 # /usr/include/aio.h
580 "aio_cancel", "aio_error", "aio_read", "aio_return", "aio_suspend",
581 "aio_write", "lio_listio",
582 # /usr/include/mqueue.h
583 "mq_timedreceive", "mq_timedsend",
584 # /usr/include/glob.h
585 "glob", "glob64", "globfree", "globfree64",
586 # /usr/include/sched.h
587 "sched_rr_get_interval",
588 # /usr/include/fcntl.h
589 "fcntl", "fcntl64",
590 # /usr/include/utime.h
591 "utime",
592 # /usr/include/ftw.h
593 "ftw", "ftw64", "nftw", "nftw64",
594 # /usr/include/fts.h
595 "fts64_children", "fts64_close", "fts64_open", "fts64_read",
596 "fts64_set", "fts_children", "fts_close", "fts_open", "fts_read",
597 "fts_set",
598 # /usr/include/netdb.h
599 "gai_suspend",
600 }
601
602 ptrn = re.compile(
603 r'''
604 (?P<value>[\da-fA-F]+) \s+
605 (?P<flags>[lgu! ][w ][C ][W ][Ii ][dD ]F) \s+
606 (?P<section>\*UND\*) \s+
607 (?P<alignment>(?P<size>[\da-fA-F]+)) \s+
608 (?P<symbol>
609 ''' +
610 r'(?P<notag>' + r'|'.join(sorted(api32)) + r')' +
611 r'''
612 (@+(?P<tag>GLIBC_\d+\.\d+\S*)))
613 ''', re.VERBOSE
614 )
615
616 # elf is a oe.qa.ELFFile object
617 if elf is not None:
618 phdrs = elf.run_objdump("-tw", d)
619 syms = re.finditer(ptrn, phdrs)
620 usedapis = {sym.group('notag') for sym in syms}
621 if usedapis:
622 elfpath = package_qa_clean_path(path, d, packagename)
623 # Remove any .debug dir, heuristic that probably works
624 # At this point, any symbol information is stripped into the debug
625 # package, so that is the only place we will find them.
626 elfpath = elfpath.replace('.debug/', '')
Andrew Geissler028142b2023-05-05 11:29:21 -0500627 allowed = "32bit-time" in (d.getVar('INSANE_SKIP') or '').split()
628 if not allowed:
Andrew Geissler517393d2023-01-13 08:55:19 -0600629 msgformat = elfpath + " uses 32-bit api '%s'"
630 for sym in usedapis:
631 oe.qa.add_message(messages, '32bit-time', msgformat % sym)
632 oe.qa.add_message(
633 messages, '32bit-time',
Andrew Geissler028142b2023-05-05 11:29:21 -0500634 'Suppress with INSANE_SKIP = "32bit-time"'
Andrew Geissler517393d2023-01-13 08:55:19 -0600635 )
636
Patrick Williams92b42cb2022-09-03 06:53:57 -0500637# Check license variables
638do_populate_lic[postfuncs] += "populate_lic_qa_checksum"
639python populate_lic_qa_checksum() {
640 """
641 Check for changes in the license files.
642 """
643
644 lic_files = d.getVar('LIC_FILES_CHKSUM') or ''
645 lic = d.getVar('LICENSE')
646 pn = d.getVar('PN')
647
648 if lic == "CLOSED":
649 return
650
651 if not lic_files and d.getVar('SRC_URI'):
652 oe.qa.handle_error("license-checksum", pn + ": Recipe file fetches files and does not have license file information (LIC_FILES_CHKSUM)", d)
653
654 srcdir = d.getVar('S')
655 corebase_licensefile = d.getVar('COREBASE') + "/LICENSE"
656 for url in lic_files.split():
657 try:
658 (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(url)
659 except bb.fetch.MalformedUrl:
660 oe.qa.handle_error("license-checksum", pn + ": LIC_FILES_CHKSUM contains an invalid URL: " + url, d)
661 continue
662 srclicfile = os.path.join(srcdir, path)
663 if not os.path.isfile(srclicfile):
664 oe.qa.handle_error("license-checksum", pn + ": LIC_FILES_CHKSUM points to an invalid file: " + srclicfile, d)
665 continue
666
667 if (srclicfile == corebase_licensefile):
668 bb.warn("${COREBASE}/LICENSE is not a valid license file, please use '${COMMON_LICENSE_DIR}/MIT' for a MIT License file in LIC_FILES_CHKSUM. This will become an error in the future")
669
670 recipemd5 = parm.get('md5', '')
671 beginline, endline = 0, 0
672 if 'beginline' in parm:
673 beginline = int(parm['beginline'])
674 if 'endline' in parm:
675 endline = int(parm['endline'])
676
677 if (not beginline) and (not endline):
678 md5chksum = bb.utils.md5_file(srclicfile)
679 with open(srclicfile, 'r', errors='replace') as f:
680 license = f.read().splitlines()
681 else:
682 with open(srclicfile, 'rb') as f:
683 import hashlib
684 lineno = 0
685 license = []
Patrick Williams2390b1b2022-11-03 13:47:49 -0500686 try:
687 m = hashlib.new('MD5', usedforsecurity=False)
688 except TypeError:
689 m = hashlib.new('MD5')
Patrick Williams92b42cb2022-09-03 06:53:57 -0500690 for line in f:
691 lineno += 1
692 if (lineno >= beginline):
693 if ((lineno <= endline) or not endline):
694 m.update(line)
695 license.append(line.decode('utf-8', errors='replace').rstrip())
696 else:
697 break
698 md5chksum = m.hexdigest()
699 if recipemd5 == md5chksum:
700 bb.note (pn + ": md5 checksum matched for ", url)
701 else:
702 if recipemd5:
703 msg = pn + ": The LIC_FILES_CHKSUM does not match for " + url
704 msg = msg + "\n" + pn + ": The new md5 checksum is " + md5chksum
705 max_lines = int(d.getVar('QA_MAX_LICENSE_LINES') or 20)
706 if not license or license[-1] != '':
707 # Ensure that our license text ends with a line break
708 # (will be added with join() below).
709 license.append('')
710 remove = len(license) - max_lines
711 if remove > 0:
712 start = max_lines // 2
713 end = start + remove - 1
714 del license[start:end]
715 license.insert(start, '...')
716 msg = msg + "\n" + pn + ": Here is the selected license text:" + \
717 "\n" + \
718 "{:v^70}".format(" beginline=%d " % beginline if beginline else "") + \
719 "\n" + "\n".join(license) + \
720 "{:^^70}".format(" endline=%d " % endline if endline else "")
721 if beginline:
722 if endline:
723 srcfiledesc = "%s (lines %d through to %d)" % (srclicfile, beginline, endline)
724 else:
725 srcfiledesc = "%s (beginning on line %d)" % (srclicfile, beginline)
726 elif endline:
727 srcfiledesc = "%s (ending on line %d)" % (srclicfile, endline)
728 else:
729 srcfiledesc = srclicfile
730 msg = msg + "\n" + pn + ": Check if the license information has changed in %s to verify that the LICENSE value \"%s\" remains valid" % (srcfiledesc, lic)
731
732 else:
733 msg = pn + ": LIC_FILES_CHKSUM is not specified for " + url
734 msg = msg + "\n" + pn + ": The md5 checksum is " + md5chksum
735 oe.qa.handle_error("license-checksum", msg, d)
736
737 oe.qa.exit_if_errors(d)
738}
739
740def qa_check_staged(path,d):
741 """
742 Check staged la and pc files for common problems like references to the work
743 directory.
744
745 As this is run after every stage we should be able to find the one
746 responsible for the errors easily even if we look at every .pc and .la file.
747 """
748
749 tmpdir = d.getVar('TMPDIR')
750 workdir = os.path.join(tmpdir, "work")
751 recipesysroot = d.getVar("RECIPE_SYSROOT")
752
753 if bb.data.inherits_class("native", d) or bb.data.inherits_class("cross", d):
754 pkgconfigcheck = workdir
755 else:
756 pkgconfigcheck = tmpdir
757
758 skip = (d.getVar('INSANE_SKIP') or "").split()
759 skip_la = False
760 if 'la' in skip:
761 bb.note("Recipe %s skipping qa checking: la" % d.getVar('PN'))
762 skip_la = True
763
764 skip_pkgconfig = False
765 if 'pkgconfig' in skip:
766 bb.note("Recipe %s skipping qa checking: pkgconfig" % d.getVar('PN'))
767 skip_pkgconfig = True
768
769 skip_shebang_size = False
770 if 'shebang-size' in skip:
771 bb.note("Recipe %s skipping qa checkking: shebang-size" % d.getVar('PN'))
772 skip_shebang_size = True
773
774 # find all .la and .pc files
775 # read the content
776 # and check for stuff that looks wrong
777 for root, dirs, files in os.walk(path):
778 for file in files:
779 path = os.path.join(root,file)
780 if file.endswith(".la") and not skip_la:
781 with open(path) as f:
782 file_content = f.read()
783 file_content = file_content.replace(recipesysroot, "")
784 if workdir in file_content:
785 error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
786 oe.qa.handle_error("la", error_msg, d)
787 elif file.endswith(".pc") and not skip_pkgconfig:
788 with open(path) as f:
789 file_content = f.read()
790 file_content = file_content.replace(recipesysroot, "")
791 if pkgconfigcheck in file_content:
792 error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
793 oe.qa.handle_error("pkgconfig", error_msg, d)
794
795 if not skip_shebang_size:
796 errors = {}
797 package_qa_check_shebang_size(path, "", d, None, errors)
798 for e in errors:
799 oe.qa.handle_error(e, errors[e], d)
800
801
802# Run all package-wide warnfuncs and errorfuncs
803def package_qa_package(warnfuncs, errorfuncs, package, d):
804 warnings = {}
805 errors = {}
806
807 for func in warnfuncs:
808 func(package, d, warnings)
809 for func in errorfuncs:
810 func(package, d, errors)
811
812 for w in warnings:
813 oe.qa.handle_error(w, warnings[w], d)
814 for e in errors:
815 oe.qa.handle_error(e, errors[e], d)
816
817 return len(errors) == 0
818
819# Run all recipe-wide warnfuncs and errorfuncs
820def package_qa_recipe(warnfuncs, errorfuncs, pn, d):
821 warnings = {}
822 errors = {}
823
824 for func in warnfuncs:
825 func(pn, d, warnings)
826 for func in errorfuncs:
827 func(pn, d, errors)
828
829 for w in warnings:
830 oe.qa.handle_error(w, warnings[w], d)
831 for e in errors:
832 oe.qa.handle_error(e, errors[e], d)
833
834 return len(errors) == 0
835
836def prepopulate_objdump_p(elf, d):
837 output = elf.run_objdump("-p", d)
838 return (elf.name, output)
839
840# Walk over all files in a directory and call func
841def package_qa_walk(warnfuncs, errorfuncs, package, d):
842 #if this will throw an exception, then fix the dict above
843 target_os = d.getVar('HOST_OS')
844 target_arch = d.getVar('HOST_ARCH')
845
846 warnings = {}
847 errors = {}
848 elves = {}
849 for path in pkgfiles[package]:
850 elf = None
851 if os.path.isfile(path):
852 elf = oe.qa.ELFFile(path)
853 try:
854 elf.open()
855 elf.close()
856 except oe.qa.NotELFFileError:
857 elf = None
858 if elf:
859 elves[path] = elf
860
861 results = oe.utils.multiprocess_launch(prepopulate_objdump_p, elves.values(), d, extraargs=(d,))
862 for item in results:
863 elves[item[0]].set_objdump("-p", item[1])
864
865 for path in pkgfiles[package]:
866 if path in elves:
867 elves[path].open()
868 for func in warnfuncs:
869 func(path, package, d, elves.get(path), warnings)
870 for func in errorfuncs:
871 func(path, package, d, elves.get(path), errors)
872 if path in elves:
873 elves[path].close()
874
875 for w in warnings:
876 oe.qa.handle_error(w, warnings[w], d)
877 for e in errors:
878 oe.qa.handle_error(e, errors[e], d)
879
880def package_qa_check_rdepends(pkg, pkgdest, skip, taskdeps, packages, d):
881 # Don't do this check for kernel/module recipes, there aren't too many debug/development
882 # packages and you can get false positives e.g. on kernel-module-lirc-dev
883 if bb.data.inherits_class("kernel", d) or bb.data.inherits_class("module-base", d):
884 return
885
886 if not "-dbg" in pkg and not "packagegroup-" in pkg and not "-image" in pkg:
887 localdata = bb.data.createCopy(d)
888 localdata.setVar('OVERRIDES', localdata.getVar('OVERRIDES') + ':' + pkg)
889
890 # Now check the RDEPENDS
891 rdepends = bb.utils.explode_deps(localdata.getVar('RDEPENDS') or "")
892
893 # Now do the sanity check!!!
894 if "build-deps" not in skip:
895 for rdepend in rdepends:
896 if "-dbg" in rdepend and "debug-deps" not in skip:
897 error_msg = "%s rdepends on %s" % (pkg,rdepend)
898 oe.qa.handle_error("debug-deps", error_msg, d)
899 if (not "-dev" in pkg and not "-staticdev" in pkg) and rdepend.endswith("-dev") and "dev-deps" not in skip:
900 error_msg = "%s rdepends on %s" % (pkg, rdepend)
901 oe.qa.handle_error("dev-deps", error_msg, d)
902 if rdepend not in packages:
903 rdep_data = oe.packagedata.read_subpkgdata(rdepend, d)
904 if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
905 continue
906 if not rdep_data or not 'PN' in rdep_data:
Patrick Williams169d7bc2024-01-05 11:33:25 -0600907 for _, rdep_data in oe.packagedata.foreach_runtime_provider_pkgdata(d, rdepend):
Patrick Williams92b42cb2022-09-03 06:53:57 -0500908 if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
909 break
910 if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
911 continue
912 if rdep_data and 'PN' in rdep_data:
913 error_msg = "%s rdepends on %s, but it isn't a build dependency, missing %s in DEPENDS or PACKAGECONFIG?" % (pkg, rdepend, rdep_data['PN'])
914 else:
915 error_msg = "%s rdepends on %s, but it isn't a build dependency?" % (pkg, rdepend)
916 oe.qa.handle_error("build-deps", error_msg, d)
917
918 if "file-rdeps" not in skip:
919 ignored_file_rdeps = set(['/bin/sh', '/usr/bin/env', 'rtld(GNU_HASH)'])
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600920 if bb.utils.contains('DISTRO_FEATURES', 'usrmerge', True, False, d):
921 ignored_file_rdeps |= set(['/usr/bin/sh'])
Patrick Williams92b42cb2022-09-03 06:53:57 -0500922 if bb.data.inherits_class('nativesdk', d):
923 ignored_file_rdeps |= set(['/bin/bash', '/usr/bin/perl', 'perl'])
Patrick Williamsac13d5f2023-11-24 18:59:46 -0600924 if bb.utils.contains('DISTRO_FEATURES', 'usrmerge', True, False, d):
925 ignored_file_rdeps |= set(['/usr/bin/bash'])
Patrick Williams92b42cb2022-09-03 06:53:57 -0500926 # For Saving the FILERDEPENDS
927 filerdepends = {}
928 rdep_data = oe.packagedata.read_subpkgdata(pkg, d)
929 for key in rdep_data:
930 if key.startswith("FILERDEPENDS:"):
931 for subkey in bb.utils.explode_deps(rdep_data[key]):
932 if subkey not in ignored_file_rdeps and \
933 not subkey.startswith('perl('):
934 # We already know it starts with FILERDEPENDS_
935 filerdepends[subkey] = key[13:]
936
937 if filerdepends:
938 done = rdepends[:]
939 # Add the rprovides of itself
940 if pkg not in done:
941 done.insert(0, pkg)
942
943 # The python is not a package, but python-core provides it, so
944 # skip checking /usr/bin/python if python is in the rdeps, in
945 # case there is a RDEPENDS:pkg = "python" in the recipe.
946 for py in [ d.getVar('MLPREFIX') + "python", "python" ]:
947 if py in done:
948 filerdepends.pop("/usr/bin/python",None)
949 done.remove(py)
950 for rdep in done:
951 # The file dependencies may contain package names, e.g.,
952 # perl
953 filerdepends.pop(rdep,None)
954
Patrick Williams169d7bc2024-01-05 11:33:25 -0600955 for _, rdep_data in oe.packagedata.foreach_runtime_provider_pkgdata(d, rdep, True):
956 for key in rdep_data:
957 if key.startswith("FILERPROVIDES:") or key.startswith("RPROVIDES:"):
958 for subkey in bb.utils.explode_deps(rdep_data[key]):
959 filerdepends.pop(subkey,None)
960 # Add the files list to the rprovides
961 if key.startswith("FILES_INFO:"):
962 # Use eval() to make it as a dict
963 for subkey in eval(rdep_data[key]):
964 filerdepends.pop(subkey,None)
965
Patrick Williams92b42cb2022-09-03 06:53:57 -0500966 if not filerdepends:
967 # Break if all the file rdepends are met
968 break
969 if filerdepends:
970 for key in filerdepends:
971 error_msg = "%s contained in package %s requires %s, but no providers found in RDEPENDS:%s?" % \
972 (filerdepends[key].replace(":%s" % pkg, "").replace("@underscore@", "_"), pkg, key, pkg)
973 oe.qa.handle_error("file-rdeps", error_msg, d)
974package_qa_check_rdepends[vardepsexclude] = "OVERRIDES"
975
976def package_qa_check_deps(pkg, pkgdest, d):
977
978 localdata = bb.data.createCopy(d)
979 localdata.setVar('OVERRIDES', pkg)
980
981 def check_valid_deps(var):
982 try:
983 rvar = bb.utils.explode_dep_versions2(localdata.getVar(var) or "")
984 except ValueError as e:
985 bb.fatal("%s:%s: %s" % (var, pkg, e))
986 for dep in rvar:
987 for v in rvar[dep]:
988 if v and not v.startswith(('< ', '= ', '> ', '<= ', '>=')):
989 error_msg = "%s:%s is invalid: %s (%s) only comparisons <, =, >, <=, and >= are allowed" % (var, pkg, dep, v)
990 oe.qa.handle_error("dep-cmp", error_msg, d)
991
992 check_valid_deps('RDEPENDS')
993 check_valid_deps('RRECOMMENDS')
994 check_valid_deps('RSUGGESTS')
995 check_valid_deps('RPROVIDES')
996 check_valid_deps('RREPLACES')
997 check_valid_deps('RCONFLICTS')
998
999QAPKGTEST[usrmerge] = "package_qa_check_usrmerge"
1000def package_qa_check_usrmerge(pkg, d, messages):
1001
1002 pkgdest = d.getVar('PKGDEST')
1003 pkg_dir = pkgdest + os.sep + pkg + os.sep
1004 merged_dirs = ['bin', 'sbin', 'lib'] + d.getVar('MULTILIB_VARIANTS').split()
1005 for f in merged_dirs:
1006 if os.path.exists(pkg_dir + f) and not os.path.islink(pkg_dir + f):
1007 msg = "%s package is not obeying usrmerge distro feature. /%s should be relocated to /usr." % (pkg, f)
1008 oe.qa.add_message(messages, "usrmerge", msg)
1009 return False
1010 return True
1011
1012QAPKGTEST[perllocalpod] = "package_qa_check_perllocalpod"
1013def package_qa_check_perllocalpod(pkg, d, messages):
1014 """
1015 Check that the recipe didn't ship a perlocal.pod file, which shouldn't be
1016 installed in a distribution package. cpan.bbclass sets NO_PERLLOCAL=1 to
1017 handle this for most recipes.
1018 """
1019 import glob
1020 pkgd = oe.path.join(d.getVar('PKGDEST'), pkg)
1021 podpath = oe.path.join(pkgd, d.getVar("libdir"), "perl*", "*", "*", "perllocal.pod")
1022
1023 matches = glob.glob(podpath)
1024 if matches:
1025 matches = [package_qa_clean_path(path, d, pkg) for path in matches]
1026 msg = "%s contains perllocal.pod (%s), should not be installed" % (pkg, " ".join(matches))
1027 oe.qa.add_message(messages, "perllocalpod", msg)
1028
1029QAPKGTEST[expanded-d] = "package_qa_check_expanded_d"
1030def package_qa_check_expanded_d(package, d, messages):
1031 """
1032 Check for the expanded D (${D}) value in pkg_* and FILES
1033 variables, warn the user to use it correctly.
1034 """
1035 sane = True
1036 expanded_d = d.getVar('D')
1037
1038 for var in 'FILES','pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm':
1039 bbvar = d.getVar(var + ":" + package) or ""
1040 if expanded_d in bbvar:
1041 if var == 'FILES':
1042 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)
1043 sane = False
1044 else:
1045 oe.qa.add_message(messages, "expanded-d", "%s in %s recipe contains ${D}, it should be replaced by $D instead" % (var, package))
1046 sane = False
1047 return sane
1048
1049QAPKGTEST[unlisted-pkg-lics] = "package_qa_check_unlisted_pkg_lics"
1050def package_qa_check_unlisted_pkg_lics(package, d, messages):
1051 """
1052 Check that all licenses for a package are among the licenses for the recipe.
1053 """
1054 pkg_lics = d.getVar('LICENSE:' + package)
1055 if not pkg_lics:
1056 return True
1057
1058 recipe_lics_set = oe.license.list_licenses(d.getVar('LICENSE'))
1059 package_lics = oe.license.list_licenses(pkg_lics)
1060 unlisted = package_lics - recipe_lics_set
1061 if unlisted:
1062 oe.qa.add_message(messages, "unlisted-pkg-lics",
1063 "LICENSE:%s includes licenses (%s) that are not "
1064 "listed in LICENSE" % (package, ' '.join(unlisted)))
1065 return False
1066 obsolete = set(oe.license.obsolete_license_list()) & package_lics - recipe_lics_set
1067 if obsolete:
1068 oe.qa.add_message(messages, "obsolete-license",
1069 "LICENSE:%s includes obsolete licenses %s" % (package, ' '.join(obsolete)))
1070 return False
1071 return True
1072
1073QAPKGTEST[empty-dirs] = "package_qa_check_empty_dirs"
1074def package_qa_check_empty_dirs(pkg, d, messages):
1075 """
1076 Check for the existence of files in directories that are expected to be
1077 empty.
1078 """
1079
1080 pkgd = oe.path.join(d.getVar('PKGDEST'), pkg)
1081 for dir in (d.getVar('QA_EMPTY_DIRS') or "").split():
1082 empty_dir = oe.path.join(pkgd, dir)
1083 if os.path.exists(empty_dir) and os.listdir(empty_dir):
1084 recommendation = (d.getVar('QA_EMPTY_DIRS_RECOMMENDATION:' + dir) or
1085 "but it is expected to be empty")
1086 msg = "%s installs files in %s, %s" % (pkg, dir, recommendation)
1087 oe.qa.add_message(messages, "empty-dirs", msg)
1088
1089def package_qa_check_encoding(keys, encode, d):
1090 def check_encoding(key, enc):
1091 sane = True
1092 value = d.getVar(key)
1093 if value:
1094 try:
1095 s = value.encode(enc)
1096 except UnicodeDecodeError as e:
1097 error_msg = "%s has non %s characters" % (key,enc)
1098 sane = False
1099 oe.qa.handle_error("invalid-chars", error_msg, d)
1100 return sane
1101
1102 for key in keys:
1103 sane = check_encoding(key, encode)
1104 if not sane:
1105 break
1106
1107HOST_USER_UID := "${@os.getuid()}"
1108HOST_USER_GID := "${@os.getgid()}"
1109
1110QAPATHTEST[host-user-contaminated] = "package_qa_check_host_user"
1111def package_qa_check_host_user(path, name, d, elf, messages):
1112 """Check for paths outside of /home which are owned by the user running bitbake."""
1113
1114 if not os.path.lexists(path):
1115 return
1116
1117 dest = d.getVar('PKGDEST')
1118 pn = d.getVar('PN')
1119 home = os.path.join(dest, name, 'home')
1120 if path == home or path.startswith(home + os.sep):
1121 return
1122
1123 try:
1124 stat = os.lstat(path)
1125 except OSError as exc:
1126 import errno
1127 if exc.errno != errno.ENOENT:
1128 raise
1129 else:
1130 check_uid = int(d.getVar('HOST_USER_UID'))
1131 if stat.st_uid == check_uid:
1132 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))
1133 return False
1134
1135 check_gid = int(d.getVar('HOST_USER_GID'))
1136 if stat.st_gid == check_gid:
1137 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))
1138 return False
1139 return True
1140
1141QARECIPETEST[unhandled-features-check] = "package_qa_check_unhandled_features_check"
1142def package_qa_check_unhandled_features_check(pn, d, messages):
1143 if not bb.data.inherits_class('features_check', d):
1144 var_set = False
1145 for kind in ['DISTRO', 'MACHINE', 'COMBINED']:
1146 for var in ['ANY_OF_' + kind + '_FEATURES', 'REQUIRED_' + kind + '_FEATURES', 'CONFLICT_' + kind + '_FEATURES']:
1147 if d.getVar(var) is not None or d.hasOverrides(var):
1148 var_set = True
1149 if var_set:
1150 oe.qa.handle_error("unhandled-features-check", "%s: recipe doesn't inherit features_check" % pn, d)
1151
1152QARECIPETEST[missing-update-alternatives] = "package_qa_check_missing_update_alternatives"
1153def package_qa_check_missing_update_alternatives(pn, d, messages):
1154 # Look at all packages and find out if any of those sets ALTERNATIVE variable
1155 # without inheriting update-alternatives class
1156 for pkg in (d.getVar('PACKAGES') or '').split():
1157 if d.getVar('ALTERNATIVE:%s' % pkg) and not bb.data.inherits_class('update-alternatives', d):
1158 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)
1159
1160# The PACKAGE FUNC to scan each package
1161python do_package_qa () {
1162 import subprocess
1163 import oe.packagedata
1164
1165 bb.note("DO PACKAGE QA")
1166
1167 main_lic = d.getVar('LICENSE')
1168
1169 # Check for obsolete license references in main LICENSE (packages are checked below for any changes)
1170 main_licenses = oe.license.list_licenses(d.getVar('LICENSE'))
1171 obsolete = set(oe.license.obsolete_license_list()) & main_licenses
1172 if obsolete:
1173 oe.qa.handle_error("obsolete-license", "Recipe LICENSE includes obsolete licenses %s" % ' '.join(obsolete), d)
1174
1175 bb.build.exec_func("read_subpackage_metadata", d)
1176
1177 # Check non UTF-8 characters on recipe's metadata
1178 package_qa_check_encoding(['DESCRIPTION', 'SUMMARY', 'LICENSE', 'SECTION'], 'utf-8', d)
1179
1180 logdir = d.getVar('T')
1181 pn = d.getVar('PN')
1182
1183 # Scan the packages...
1184 pkgdest = d.getVar('PKGDEST')
1185 packages = set((d.getVar('PACKAGES') or '').split())
1186
1187 global pkgfiles
1188 pkgfiles = {}
1189 for pkg in packages:
1190 pkgfiles[pkg] = []
1191 pkgdir = os.path.join(pkgdest, pkg)
1192 for walkroot, dirs, files in os.walk(pkgdir):
1193 # Don't walk into top-level CONTROL or DEBIAN directories as these
1194 # are temporary directories created by do_package.
1195 if walkroot == pkgdir:
1196 for control in ("CONTROL", "DEBIAN"):
1197 if control in dirs:
1198 dirs.remove(control)
1199 for file in files:
1200 pkgfiles[pkg].append(os.path.join(walkroot, file))
1201
1202 # no packages should be scanned
1203 if not packages:
1204 return
1205
1206 import re
1207 # The package name matches the [a-z0-9.+-]+ regular expression
1208 pkgname_pattern = re.compile(r"^[a-z0-9.+-]+$")
1209
1210 taskdepdata = d.getVar("BB_TASKDEPDATA", False)
1211 taskdeps = set()
1212 for dep in taskdepdata:
1213 taskdeps.add(taskdepdata[dep][0])
1214
1215 def parse_test_matrix(matrix_name):
1216 testmatrix = d.getVarFlags(matrix_name) or {}
1217 g = globals()
1218 warnchecks = []
1219 for w in (d.getVar("WARN_QA") or "").split():
1220 if w in skip:
1221 continue
1222 if w in testmatrix and testmatrix[w] in g:
1223 warnchecks.append(g[testmatrix[w]])
1224
1225 errorchecks = []
1226 for e in (d.getVar("ERROR_QA") or "").split():
1227 if e in skip:
1228 continue
1229 if e in testmatrix and testmatrix[e] in g:
1230 errorchecks.append(g[testmatrix[e]])
1231 return warnchecks, errorchecks
1232
1233 for package in packages:
1234 skip = set((d.getVar('INSANE_SKIP') or "").split() +
1235 (d.getVar('INSANE_SKIP:' + package) or "").split())
1236 if skip:
1237 bb.note("Package %s skipping QA tests: %s" % (package, str(skip)))
1238
1239 bb.note("Checking Package: %s" % package)
1240 # Check package name
1241 if not pkgname_pattern.match(package):
1242 oe.qa.handle_error("pkgname",
1243 "%s doesn't match the [a-z0-9.+-]+ regex" % package, d)
1244
1245 warn_checks, error_checks = parse_test_matrix("QAPATHTEST")
1246 package_qa_walk(warn_checks, error_checks, package, d)
1247
1248 warn_checks, error_checks = parse_test_matrix("QAPKGTEST")
1249 package_qa_package(warn_checks, error_checks, package, d)
1250
1251 package_qa_check_rdepends(package, pkgdest, skip, taskdeps, packages, d)
1252 package_qa_check_deps(package, pkgdest, d)
1253
1254 warn_checks, error_checks = parse_test_matrix("QARECIPETEST")
1255 package_qa_recipe(warn_checks, error_checks, pn, d)
1256
1257 if 'libdir' in d.getVar("ALL_QA").split():
1258 package_qa_check_libdir(d)
1259
1260 oe.qa.exit_if_errors(d)
1261}
1262
1263# binutils is used for most checks, so need to set as dependency
1264# POPULATESYSROOTDEPS is defined in staging class.
1265do_package_qa[depends] += "${POPULATESYSROOTDEPS}"
1266do_package_qa[vardeps] = "${@bb.utils.contains('ERROR_QA', 'empty-dirs', 'QA_EMPTY_DIRS', '', d)}"
1267do_package_qa[vardepsexclude] = "BB_TASKDEPDATA"
1268do_package_qa[rdeptask] = "do_packagedata"
1269addtask do_package_qa after do_packagedata do_package before do_build
1270
1271# Add the package specific INSANE_SKIPs to the sstate dependencies
1272python() {
1273 pkgs = (d.getVar('PACKAGES') or '').split()
1274 for pkg in pkgs:
1275 d.appendVarFlag("do_package_qa", "vardeps", " INSANE_SKIP:{}".format(pkg))
1276}
1277
1278SSTATETASKS += "do_package_qa"
1279do_package_qa[sstate-inputdirs] = ""
1280do_package_qa[sstate-outputdirs] = ""
1281python do_package_qa_setscene () {
1282 sstate_setscene(d)
1283}
1284addtask do_package_qa_setscene
1285
1286python do_qa_sysroot() {
1287 bb.note("QA checking do_populate_sysroot")
1288 sysroot_destdir = d.expand('${SYSROOT_DESTDIR}')
1289 for sysroot_dir in d.expand('${SYSROOT_DIRS}').split():
1290 qa_check_staged(sysroot_destdir + sysroot_dir, d)
1291 oe.qa.exit_with_message_if_errors("do_populate_sysroot for this recipe installed files with QA issues", d)
1292}
1293do_populate_sysroot[postfuncs] += "do_qa_sysroot"
1294
1295python do_qa_patch() {
1296 import subprocess
1297
1298 ###########################################################################
1299 # Check patch.log for fuzz warnings
1300 #
1301 # Further information on why we check for patch fuzz warnings:
1302 # http://lists.openembedded.org/pipermail/openembedded-core/2018-March/148675.html
1303 # https://bugzilla.yoctoproject.org/show_bug.cgi?id=10450
1304 ###########################################################################
1305
1306 logdir = d.getVar('T')
1307 patchlog = os.path.join(logdir,"log.do_patch")
1308
1309 if os.path.exists(patchlog):
1310 fuzzheader = '--- Patch fuzz start ---'
1311 fuzzfooter = '--- Patch fuzz end ---'
1312 statement = "grep -e '%s' %s > /dev/null" % (fuzzheader, patchlog)
1313 if subprocess.call(statement, shell=True) == 0:
1314 msg = "Fuzz detected:\n\n"
1315 fuzzmsg = ""
1316 inFuzzInfo = False
1317 f = open(patchlog, "r")
1318 for line in f:
1319 if fuzzheader in line:
1320 inFuzzInfo = True
1321 fuzzmsg = ""
1322 elif fuzzfooter in line:
1323 fuzzmsg = fuzzmsg.replace('\n\n', '\n')
1324 msg += fuzzmsg
1325 msg += "\n"
1326 inFuzzInfo = False
1327 elif inFuzzInfo and not 'Now at patch' in line:
1328 fuzzmsg += line
1329 f.close()
1330 msg += "The context lines in the patches can be updated with devtool:\n"
1331 msg += "\n"
1332 msg += " devtool modify %s\n" % d.getVar('PN')
1333 msg += " devtool finish --force-patch-refresh %s <layer_path>\n\n" % d.getVar('PN')
1334 msg += "Don't forget to review changes done by devtool!\n"
Andrew Geisslerc5535c92023-01-27 16:10:19 -06001335 msg += "\nPatch log indicates that patches do not apply cleanly."
Patrick Williams92b42cb2022-09-03 06:53:57 -05001336 oe.qa.handle_error("patch-fuzz", msg, d)
1337
1338 # Check if the patch contains a correctly formatted and spelled Upstream-Status
1339 import re
1340 from oe import patch
1341
Patrick Williams92b42cb2022-09-03 06:53:57 -05001342 for url in patch.src_patches(d):
Andrew Geissler6aa7eec2023-03-03 12:41:14 -06001343 (_, _, fullpath, _, _, _) = bb.fetch.decodeurl(url)
Patrick Williams92b42cb2022-09-03 06:53:57 -05001344
Andrew Geissler6aa7eec2023-03-03 12:41:14 -06001345 msg = oe.qa.check_upstream_status(fullpath)
1346 if msg:
Patrick Williams520786c2023-06-25 16:20:36 -05001347 oe.qa.handle_error("patch-status", msg, d)
Andrew Geisslerc5535c92023-01-27 16:10:19 -06001348
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001349 ###########################################################################
1350 # Check for missing ptests
1351 ###########################################################################
1352 def match_line_in_files(toplevel, filename_glob, line_regex):
1353 import pathlib
1354 try:
1355 toppath = pathlib.Path(toplevel)
1356 for entry in toppath.glob(filename_glob):
1357 try:
1358 with open(entry, 'r', encoding='utf-8', errors='ignore') as f:
1359 for line in f.readlines():
1360 if re.match(line_regex, line):
1361 return True
1362 except FileNotFoundError:
1363 # Broken symlink in source
1364 pass
1365 except FileNotFoundError:
1366 # pathlib.Path.glob() might throw this when file/directory
1367 # disappear while scanning.
1368 bb.note("unimplemented-ptest: FileNotFoundError exception while scanning (disappearing file while scanning?). Check was ignored." % d.getVar('PN'))
1369 pass
1370 return False
1371
1372 srcdir = d.getVar('S')
1373 if not bb.utils.contains('DISTRO_FEATURES', 'ptest', True, False, d):
1374 pass
1375 elif bb.data.inherits_class('ptest', d):
1376 bb.note("Package %s QA: skipping unimplemented-ptest: ptest implementation detected" % d.getVar('PN'))
1377 elif srcdir == d.getVar('WORKDIR'):
1378 bb.note("Package %s QA: skipping unimplemented-ptest: This check is not supported for recipe with \"S = \"${WORKDIR}\"" % d.getVar('PN'))
1379
1380 # Detect perl Test:: based tests
1381 elif os.path.exists(os.path.join(srcdir, "t")) and any(filename.endswith('.t') for filename in os.listdir(os.path.join(srcdir, 't'))):
1382 oe.qa.handle_error("unimplemented-ptest", "%s: perl Test:: based tests detected" % d.getVar('PN'), d)
1383
1384 # Detect pytest-based tests
1385 elif match_line_in_files(srcdir, "**/*.py", r'\s*(?:import\s*pytest|from\s*pytest)'):
1386 oe.qa.handle_error("unimplemented-ptest", "%s: pytest-based tests detected" % d.getVar('PN'), d)
1387
1388 # Detect meson-based tests
1389 elif os.path.exists(os.path.join(srcdir, "meson.build")) and match_line_in_files(srcdir, "**/meson.build", r'\s*test\s*\('):
1390 oe.qa.handle_error("unimplemented-ptest", "%s: meson-based tests detected" % d.getVar('PN'), d)
1391
1392 # Detect cmake-based tests
1393 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*\('):
1394 oe.qa.handle_error("unimplemented-ptest", "%s: cmake-based tests detected" % d.getVar('PN'), d)
1395
1396 # Detect autotools-based·tests
1397 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')):
1398 oe.qa.handle_error("unimplemented-ptest", "%s: autotools-based tests detected" % d.getVar('PN'), d)
1399
1400 # Last resort, detect a test directory in sources
1401 elif any(filename.lower() in ["test", "tests"] for filename in os.listdir(srcdir)):
1402 oe.qa.handle_error("unimplemented-ptest", "%s: test subdirectory detected" % d.getVar('PN'), d)
1403
Andrew Geisslerc5535c92023-01-27 16:10:19 -06001404 oe.qa.exit_if_errors(d)
Patrick Williams92b42cb2022-09-03 06:53:57 -05001405}
1406
1407python do_qa_configure() {
1408 import subprocess
1409
1410 ###########################################################################
1411 # Check config.log for cross compile issues
1412 ###########################################################################
1413
1414 configs = []
1415 workdir = d.getVar('WORKDIR')
1416
1417 skip = (d.getVar('INSANE_SKIP') or "").split()
1418 skip_configure_unsafe = False
1419 if 'configure-unsafe' in skip:
1420 bb.note("Recipe %s skipping qa checking: configure-unsafe" % d.getVar('PN'))
1421 skip_configure_unsafe = True
1422
1423 if bb.data.inherits_class('autotools', d) and not skip_configure_unsafe:
1424 bb.note("Checking autotools environment for common misconfiguration")
1425 for root, dirs, files in os.walk(workdir):
1426 statement = "grep -q -F -e 'is unsafe for cross-compilation' %s" % \
1427 os.path.join(root,"config.log")
1428 if "config.log" in files:
1429 if subprocess.call(statement, shell=True) == 0:
1430 error_msg = """This autoconf log indicates errors, it looked at host include and/or library paths while determining system capabilities.
1431Rerun configure task after fixing this."""
1432 oe.qa.handle_error("configure-unsafe", error_msg, d)
1433
1434 if "configure.ac" in files:
1435 configs.append(os.path.join(root,"configure.ac"))
1436 if "configure.in" in files:
1437 configs.append(os.path.join(root, "configure.in"))
1438
1439 ###########################################################################
1440 # Check gettext configuration and dependencies are correct
1441 ###########################################################################
1442
1443 skip_configure_gettext = False
1444 if 'configure-gettext' in skip:
1445 bb.note("Recipe %s skipping qa checking: configure-gettext" % d.getVar('PN'))
1446 skip_configure_gettext = True
1447
1448 cnf = d.getVar('EXTRA_OECONF') or ""
1449 if not ("gettext" in d.getVar('P') or "gcc-runtime" in d.getVar('P') or \
1450 "--disable-nls" in cnf or skip_configure_gettext):
1451 ml = d.getVar("MLPREFIX") or ""
1452 if bb.data.inherits_class('cross-canadian', d):
1453 gt = "nativesdk-gettext"
1454 else:
1455 gt = "gettext-native"
1456 deps = bb.utils.explode_deps(d.getVar('DEPENDS') or "")
1457 if gt not in deps:
1458 for config in configs:
1459 gnu = "grep \"^[[:space:]]*AM_GNU_GETTEXT\" %s >/dev/null" % config
1460 if subprocess.call(gnu, shell=True) == 0:
1461 error_msg = "AM_GNU_GETTEXT used but no inherit gettext"
1462 oe.qa.handle_error("configure-gettext", error_msg, d)
1463
1464 ###########################################################################
1465 # Check unrecognised configure options (with a white list)
1466 ###########################################################################
1467 if bb.data.inherits_class("autotools", d):
1468 bb.note("Checking configure output for unrecognised options")
1469 try:
1470 if bb.data.inherits_class("autotools", d):
1471 flag = "WARNING: unrecognized options:"
1472 log = os.path.join(d.getVar('B'), 'config.log')
1473 output = subprocess.check_output(['grep', '-F', flag, log]).decode("utf-8").replace(', ', ' ').replace('"', '')
1474 options = set()
1475 for line in output.splitlines():
1476 options |= set(line.partition(flag)[2].split())
1477 ignore_opts = set(d.getVar("UNKNOWN_CONFIGURE_OPT_IGNORE").split())
1478 options -= ignore_opts
1479 if options:
1480 pn = d.getVar('PN')
1481 error_msg = pn + ": configure was passed unrecognised options: " + " ".join(options)
1482 oe.qa.handle_error("unknown-configure-option", error_msg, d)
1483 except subprocess.CalledProcessError:
1484 pass
1485
1486 # Check invalid PACKAGECONFIG
1487 pkgconfig = (d.getVar("PACKAGECONFIG") or "").split()
1488 if pkgconfig:
1489 pkgconfigflags = d.getVarFlags("PACKAGECONFIG") or {}
1490 for pconfig in pkgconfig:
1491 if pconfig not in pkgconfigflags:
1492 pn = d.getVar('PN')
1493 error_msg = "%s: invalid PACKAGECONFIG: %s" % (pn, pconfig)
1494 oe.qa.handle_error("invalid-packageconfig", error_msg, d)
1495
1496 oe.qa.exit_if_errors(d)
1497}
1498
Patrick Williams92b42cb2022-09-03 06:53:57 -05001499python do_qa_unpack() {
1500 src_uri = d.getVar('SRC_URI')
1501 s_dir = d.getVar('S')
1502 if src_uri and not os.path.exists(s_dir):
1503 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 -05001504}
1505
Patrick Williams2a254922023-08-11 09:48:11 -05001506python do_recipe_qa() {
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001507 import re
1508
1509 def test_missing_metadata(pn, d):
Patrick Williams2a254922023-08-11 09:48:11 -05001510 fn = d.getVar("FILE")
Patrick Williams2a254922023-08-11 09:48:11 -05001511 srcfile = d.getVar('SRC_URI').split()
1512 # Check that SUMMARY is not the same as the default from bitbake.conf
1513 if d.getVar('SUMMARY') == d.expand("${PN} version ${PV}-${PR}"):
1514 oe.qa.handle_error("missing-metadata", "Recipe {} in {} does not contain a SUMMARY. Please add an entry.".format(pn, fn), d)
1515 if not d.getVar('HOMEPAGE'):
1516 if srcfile and srcfile[0].startswith('file') or not d.getVar('SRC_URI'):
1517 # We are only interested in recipes SRC_URI fetched from external sources
1518 pass
1519 else:
1520 oe.qa.handle_error("missing-metadata", "Recipe {} in {} does not contain a HOMEPAGE. Please add an entry.".format(pn, fn), d)
1521
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001522 def test_missing_maintainer(pn, d):
Patrick Williams2a254922023-08-11 09:48:11 -05001523 fn = d.getVar("FILE")
Patrick Williams2a254922023-08-11 09:48:11 -05001524 if pn.endswith("-native") or pn.startswith("nativesdk-") or "packagegroup-" in pn or "core-image-ptest-" in pn:
1525 return
1526 if not d.getVar('RECIPE_MAINTAINER'):
1527 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)
1528
Patrick Williamsac13d5f2023-11-24 18:59:46 -06001529 def test_srcuri(pn, d):
1530 skip = (d.getVar('INSANE_SKIP') or "").split()
1531 if 'src-uri-bad' in skip:
1532 bb.note("Recipe %s skipping qa checking: src-uri-bad" % pn)
1533 return
1534
1535 if "${PN}" in d.getVar("SRC_URI", False):
1536 oe.qa.handle_error("src-uri-bad", "%s: SRC_URI uses PN not BPN" % pn, d)
1537
1538 for url in d.getVar("SRC_URI").split():
1539 # Search for github and gitlab URLs that pull unstable archives (comment for future greppers)
1540 if re.search(r"git(hu|la)b\.com/.+/.+/archive/.+", url) or "//codeload.github.com/" in url:
1541 oe.qa.handle_error("src-uri-bad", "%s: SRC_URI uses unstable GitHub/GitLab archives, convert recipe to use git protocol" % pn, d)
1542
1543 pn = d.getVar('PN')
1544 test_missing_metadata(pn, d)
1545 test_missing_maintainer(pn, d)
1546 test_srcuri(pn, d)
Patrick Williams2a254922023-08-11 09:48:11 -05001547 oe.qa.exit_if_errors(d)
1548}
1549
1550addtask do_recipe_qa before do_fetch do_package_qa do_build
1551
1552SSTATETASKS += "do_recipe_qa"
1553do_recipe_qa[sstate-inputdirs] = ""
1554do_recipe_qa[sstate-outputdirs] = ""
1555python do_recipe_qa_setscene () {
1556 sstate_setscene(d)
1557}
1558addtask do_recipe_qa_setscene
1559
Patrick Williams92b42cb2022-09-03 06:53:57 -05001560# Check for patch fuzz
1561do_patch[postfuncs] += "do_qa_patch "
1562
1563# Check broken config.log files, for packages requiring Gettext which
1564# don't have it in DEPENDS.
1565#addtask qa_configure after do_configure before do_compile
1566do_configure[postfuncs] += "do_qa_configure "
1567
1568# Check does S exist.
1569do_unpack[postfuncs] += "do_qa_unpack"
1570
1571python () {
1572 import re
1573
1574 tests = d.getVar('ALL_QA').split()
1575 if "desktop" in tests:
1576 d.appendVar("PACKAGE_DEPENDS", " desktop-file-utils-native")
1577
1578 ###########################################################################
1579 # Check various variables
1580 ###########################################################################
1581
1582 # Checking ${FILESEXTRAPATHS}
1583 extrapaths = (d.getVar("FILESEXTRAPATHS") or "")
1584 if '__default' not in extrapaths.split(":"):
1585 msg = "FILESEXTRAPATHS-variable, must always use :prepend (or :append)\n"
1586 msg += "type of assignment, and don't forget the colon.\n"
1587 msg += "Please assign it with the format of:\n"
1588 msg += " FILESEXTRAPATHS:append := \":${THISDIR}/Your_Files_Path\" or\n"
1589 msg += " FILESEXTRAPATHS:prepend := \"${THISDIR}/Your_Files_Path:\"\n"
1590 msg += "in your bbappend file\n\n"
1591 msg += "Your incorrect assignment is:\n"
1592 msg += "%s\n" % extrapaths
1593 bb.warn(msg)
1594
1595 overrides = d.getVar('OVERRIDES').split(':')
1596 pn = d.getVar('PN')
1597 if pn in overrides:
1598 msg = 'Recipe %s has PN of "%s" which is in OVERRIDES, this can result in unexpected behaviour.' % (d.getVar("FILE"), pn)
1599 oe.qa.handle_error("pn-overrides", msg, d)
1600 prog = re.compile(r'[A-Z]')
1601 if prog.search(pn):
1602 oe.qa.handle_error("uppercase-pn", 'PN: %s is upper case, this can result in unexpected behavior.' % pn, d)
1603
1604 # Some people mistakenly use DEPENDS:${PN} instead of DEPENDS and wonder
1605 # why it doesn't work.
1606 if (d.getVar(d.expand('DEPENDS:${PN}'))):
1607 oe.qa.handle_error("pkgvarcheck", "recipe uses DEPENDS:${PN}, should use DEPENDS", d)
1608
1609 issues = []
1610 if (d.getVar('PACKAGES') or "").split():
1611 for dep in (d.getVar('QADEPENDS') or "").split():
1612 d.appendVarFlag('do_package_qa', 'depends', " %s:do_populate_sysroot" % dep)
1613 for var in 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RCONFLICTS', 'RPROVIDES', 'RREPLACES', 'FILES', 'pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm', 'ALLOW_EMPTY':
1614 if d.getVar(var, False):
1615 issues.append(var)
1616
1617 fakeroot_tests = d.getVar('FAKEROOT_QA').split()
1618 if set(tests) & set(fakeroot_tests):
1619 d.setVarFlag('do_package_qa', 'fakeroot', '1')
1620 d.appendVarFlag('do_package_qa', 'depends', ' virtual/fakeroot-native:do_populate_sysroot')
1621 else:
1622 d.setVarFlag('do_package_qa', 'rdeptask', '')
1623 for i in issues:
1624 oe.qa.handle_error("pkgvarcheck", "%s: Variable %s is set as not being package specific, please fix this." % (d.getVar("FILE"), i), d)
1625
1626 if 'native-last' not in (d.getVar('INSANE_SKIP') or "").split():
1627 for native_class in ['native', 'nativesdk']:
1628 if bb.data.inherits_class(native_class, d):
1629
1630 inherited_classes = d.getVar('__inherit_cache', False) or []
1631 needle = "/" + native_class
1632
1633 bbclassextend = (d.getVar('BBCLASSEXTEND') or '').split()
1634 # BBCLASSEXTEND items are always added in the end
1635 skip_classes = bbclassextend
1636 if bb.data.inherits_class('native', d) or 'native' in bbclassextend:
1637 # native also inherits nopackages and relocatable bbclasses
1638 skip_classes.extend(['nopackages', 'relocatable'])
1639
1640 broken_order = []
1641 for class_item in reversed(inherited_classes):
1642 if needle not in class_item:
1643 for extend_item in skip_classes:
1644 if '/%s.bbclass' % extend_item in class_item:
1645 break
1646 else:
1647 pn = d.getVar('PN')
1648 broken_order.append(os.path.basename(class_item))
1649 else:
1650 break
1651 if broken_order:
1652 oe.qa.handle_error("native-last", "%s: native/nativesdk class is not inherited last, this can result in unexpected behaviour. "
1653 "Classes inherited after native/nativesdk: %s" % (pn, " ".join(broken_order)), d)
1654
1655 oe.qa.exit_if_errors(d)
1656}