blob: eb1942565223108852120cac17a0bd203d7e8498 [file] [log] [blame]
lh9ed821d2023-04-07 01:36:19 -07001# BB Class inspired by ebuild.sh
2#
3# This class will test files after installation for certain
4# security issues and other kind of issues.
5#
6# Checks we do:
7# -Check the ownership and permissions
8# -Check the RUNTIME path for the $TMPDIR
9# -Check if .la files wrongly point to workdir
10# -Check if .pc files wrongly point to workdir
11# -Check if packages contains .debug directories or .so files
12# where they should be in -dev or -dbg
13# -Check if config.log contains traces to broken autoconf tests
14# -Check invalid characters (non-utf8) on some package metadata
15# -Ensure that binaries in base_[bindir|sbindir|libdir] do not link
16# into exec_prefix
17# -Check that scripts in base_[bindir|sbindir|libdir] do not reference
18# files under exec_prefix
19# -Check if the package name is upper case
20
21QA_SANE = "True"
22
23# Elect whether a given type of error is a warning or error, they may
24# have been set by other files.
25WARN_QA ?= "ldflags useless-rpaths rpaths staticdev libdir xorg-driver-abi \
26 textrel already-stripped incompatible-license files-invalid \
27 installed-vs-shipped compile-host-path install-host-path \
28 pn-overrides infodir build-deps src-uri-bad \
29 unknown-configure-option symlink-to-sysroot multilib \
30 invalid-packageconfig host-user-contaminated uppercase-pn patch-fuzz \
31 mime mime-xdg unlisted-pkg-lics unhandled-features-check \
32 missing-update-alternatives \
33 "
34ERROR_QA ?= "dev-so debug-deps dev-deps debug-files arch pkgconfig la \
35 perms dep-cmp pkgvarcheck perm-config perm-line perm-link \
36 split-strip packages-list pkgv-undefined var-undefined \
37 version-going-backwards expanded-d invalid-chars \
38 license-checksum dev-elf file-rdeps configure-unsafe \
39 configure-gettext perllocalpod shebang-size \
40 "
41# Add usrmerge QA check based on distro feature
42ERROR_QA_append = "${@bb.utils.contains('DISTRO_FEATURES', 'usrmerge', ' usrmerge', '', d)}"
43
44FAKEROOT_QA = "host-user-contaminated"
45FAKEROOT_QA[doc] = "QA tests which need to run under fakeroot. If any \
46enabled tests are listed here, the do_package_qa task will run under fakeroot."
47
48ALL_QA = "${WARN_QA} ${ERROR_QA}"
49
50UNKNOWN_CONFIGURE_WHITELIST ?= "--enable-nls --disable-nls --disable-silent-rules --disable-dependency-tracking --with-libtool-sysroot --disable-static"
51
52def package_qa_clean_path(path, d, pkg=None):
53 """
54 Remove redundant paths from the path for display. If pkg isn't set then
55 TMPDIR is stripped, otherwise PKGDEST/pkg is stripped.
56 """
57 if pkg:
58 path = path.replace(os.path.join(d.getVar("PKGDEST"), pkg), "/")
59 return path.replace(d.getVar("TMPDIR"), "/").replace("//", "/")
60
61def package_qa_write_error(type, error, d):
62 logfile = d.getVar('QA_LOGFILE')
63 if logfile:
64 p = d.getVar('P')
65 with open(logfile, "a+") as f:
66 f.write("%s: %s [%s]\n" % (p, error, type))
67
68def package_qa_handle_error(error_class, error_msg, d):
69 if error_class in (d.getVar("ERROR_QA") or "").split():
70 package_qa_write_error(error_class, error_msg, d)
71 bb.error("QA Issue: %s [%s]" % (error_msg, error_class))
72 d.setVar("QA_SANE", False)
73 return False
74 elif error_class in (d.getVar("WARN_QA") or "").split():
75 package_qa_write_error(error_class, error_msg, d)
76 bb.warn("QA Issue: %s [%s]" % (error_msg, error_class))
77 else:
78 bb.note("QA Issue: %s [%s]" % (error_msg, error_class))
79 return True
80
81def package_qa_add_message(messages, section, new_msg):
82 if section not in messages:
83 messages[section] = new_msg
84 else:
85 messages[section] = messages[section] + "\n" + new_msg
86
87QAPATHTEST[shebang-size] = "package_qa_check_shebang_size"
88def package_qa_check_shebang_size(path, name, d, elf, messages):
89 if os.path.islink(path) or elf:
90 return
91
92 try:
93 with open(path, 'rb') as f:
94 stanza = f.readline(130)
95 except IOError:
96 return
97
98 if stanza.startswith(b'#!'):
99 #Shebang not found
100 try:
101 stanza = stanza.decode("utf-8")
102 except UnicodeDecodeError:
103 #If it is not a text file, it is not a script
104 return
105
106 if len(stanza) > 129:
107 package_qa_add_message(messages, "shebang-size", "%s: %s maximum shebang size exceeded, the maximum size is 128." % (name, package_qa_clean_path(path, d)))
108 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):
119 package_qa_add_message(messages, "libexec", "%s: %s is using libexec please relocate to %s" % (name, package_qa_clean_path(path, d), libexec))
120 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 package_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 package_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 package_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 package_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 package_qa_add_message(messages, "staticdev", "non -staticdev package contains static .a library: %s path '%s'" % \
211 (name, package_qa_clean_path(path,d, name)))
212
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 package_qa_add_message(messages, "mime", "package contains mime types but does not inherit mime: %s path '%s'" % \
222 (name, package_qa_clean_path(path,d)))
223
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:
242 wstr = "%s cannot open %s - is it a symlink with absolute path?\n" % (name, package_qa_clean_path(path,d))
243 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 package_qa_add_message(messages, "mime-xdg", wstr)
249 if mime_type_found:
250 package_qa_add_message(messages, "mime-xdg", "package contains desktop file with key 'MimeType' but does not inhert mime-xdg: %s path '%s'" % \
251 (name, package_qa_clean_path(path,d)))
252
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 package_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):
324 package_qa_add_message(messages, "debug-files", "non debug package contains .debug directory: %s path %s" % \
325 (name, package_qa_clean_path(path,d)))
326
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('TARGET_OS')
338 target_arch = d.getVar('TARGET_ARCH')
339 provides = d.getVar('PROVIDES')
340 bpn = d.getVar('BPN')
341
342 if target_arch == "allarch":
343 pn = d.getVar('PN')
344 package_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 package_qa_add_message(messages, "arch", "Architecture did not match (%s, expected %s) on %s" % \
368 (oe.qa.elf_machine_to_string(elf.machine()), oe.qa.elf_machine_to_string(machine), package_qa_clean_path(path,d)))
369 elif not ((bits == elf.abiSize()) or is_32 or is_bpf):
370 package_qa_add_message(messages, "arch", "Bit size did not match (%d to %d) %s on %s" % \
371 (bits, elf.abiSize(), bpn, package_qa_clean_path(path,d)))
372 elif not ((littleendian == elf.isLittleEndian()) or is_bpf):
373 package_qa_add_message(messages, "arch", "Endiannes did not match (%d to %d) on %s" % \
374 (littleendian, elf.isLittleEndian(), package_qa_clean_path(path,d)))
375
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 package_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 package_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 "DT_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 package_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 not found in the whitelist
454 """
455 # Ignore .debug files, not interesting
456 if path.find(".debug") != -1:
457 return
458
459 # Ignore symlinks
460 if os.path.islink(path):
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 package_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 package_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 package_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 package_qa_add_message(messages, "symlink-to-sysroot", "Symlink %s in %s points to TMPDIR" % (trimmed, name))
511
512# Check license variables
513do_populate_lic[postfuncs] += "populate_lic_qa_checksum"
514python populate_lic_qa_checksum() {
515 """
516 Check for changes in the license files.
517 """
518 sane = True
519
520 lic_files = d.getVar('LIC_FILES_CHKSUM') or ''
521 lic = d.getVar('LICENSE')
522 pn = d.getVar('PN')
523
524 if lic == "CLOSED":
525 return
526
527 if not lic_files and d.getVar('SRC_URI'):
528 sane &= package_qa_handle_error("license-checksum", pn + ": Recipe file fetches files and does not have license file information (LIC_FILES_CHKSUM)", d)
529
530 srcdir = d.getVar('S')
531 corebase_licensefile = d.getVar('COREBASE') + "/LICENSE"
532 for url in lic_files.split():
533 try:
534 (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(url)
535 except bb.fetch.MalformedUrl:
536 sane &= package_qa_handle_error("license-checksum", pn + ": LIC_FILES_CHKSUM contains an invalid URL: " + url, d)
537 continue
538 srclicfile = os.path.join(srcdir, path)
539 if not os.path.isfile(srclicfile):
540 sane &= package_qa_handle_error("license-checksum", pn + ": LIC_FILES_CHKSUM points to an invalid file: " + srclicfile, d)
541 continue
542
543 if (srclicfile == corebase_licensefile):
544 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")
545
546 recipemd5 = parm.get('md5', '')
547 beginline, endline = 0, 0
548 if 'beginline' in parm:
549 beginline = int(parm['beginline'])
550 if 'endline' in parm:
551 endline = int(parm['endline'])
552
553 if (not beginline) and (not endline):
554 md5chksum = bb.utils.md5_file(srclicfile)
555 with open(srclicfile, 'r', errors='replace') as f:
556 license = f.read().splitlines()
557 else:
558 with open(srclicfile, 'rb') as f:
559 import hashlib
560 lineno = 0
561 license = []
562 m = hashlib.md5()
563 for line in f:
564 lineno += 1
565 if (lineno >= beginline):
566 if ((lineno <= endline) or not endline):
567 m.update(line)
568 license.append(line.decode('utf-8', errors='replace').rstrip())
569 else:
570 break
571 md5chksum = m.hexdigest()
572 if recipemd5 == md5chksum:
573 bb.note (pn + ": md5 checksum matched for ", url)
574 else:
575 if recipemd5:
576 msg = pn + ": The LIC_FILES_CHKSUM does not match for " + url
577 msg = msg + "\n" + pn + ": The new md5 checksum is " + md5chksum
578 max_lines = int(d.getVar('QA_MAX_LICENSE_LINES') or 20)
579 if not license or license[-1] != '':
580 # Ensure that our license text ends with a line break
581 # (will be added with join() below).
582 license.append('')
583 remove = len(license) - max_lines
584 if remove > 0:
585 start = max_lines // 2
586 end = start + remove - 1
587 del license[start:end]
588 license.insert(start, '...')
589 msg = msg + "\n" + pn + ": Here is the selected license text:" + \
590 "\n" + \
591 "{:v^70}".format(" beginline=%d " % beginline if beginline else "") + \
592 "\n" + "\n".join(license) + \
593 "{:^^70}".format(" endline=%d " % endline if endline else "")
594 if beginline:
595 if endline:
596 srcfiledesc = "%s (lines %d through to %d)" % (srclicfile, beginline, endline)
597 else:
598 srcfiledesc = "%s (beginning on line %d)" % (srclicfile, beginline)
599 elif endline:
600 srcfiledesc = "%s (ending on line %d)" % (srclicfile, endline)
601 else:
602 srcfiledesc = srclicfile
603 msg = msg + "\n" + pn + ": Check if the license information has changed in %s to verify that the LICENSE value \"%s\" remains valid" % (srcfiledesc, lic)
604
605 else:
606 msg = pn + ": LIC_FILES_CHKSUM is not specified for " + url
607 msg = msg + "\n" + pn + ": The md5 checksum is " + md5chksum
608 sane &= package_qa_handle_error("license-checksum", msg, d)
609
610 if not sane:
611 bb.fatal("Fatal QA errors found, failing task.")
612}
613
614def qa_check_staged(path,d):
615 """
616 Check staged la and pc files for common problems like references to the work
617 directory.
618
619 As this is run after every stage we should be able to find the one
620 responsible for the errors easily even if we look at every .pc and .la file.
621 """
622
623 sane = True
624 tmpdir = d.getVar('TMPDIR')
625 workdir = os.path.join(tmpdir, "work")
626 recipesysroot = d.getVar("RECIPE_SYSROOT")
627
628 if bb.data.inherits_class("native", d) or bb.data.inherits_class("cross", d):
629 pkgconfigcheck = workdir
630 else:
631 pkgconfigcheck = tmpdir
632
633 skip = (d.getVar('INSANE_SKIP') or "").split()
634 skip_la = False
635 if 'la' in skip:
636 bb.note("Recipe %s skipping qa checking: la" % d.getVar('PN'))
637 skip_la = True
638
639 skip_pkgconfig = False
640 if 'pkgconfig' in skip:
641 bb.note("Recipe %s skipping qa checking: pkgconfig" % d.getVar('PN'))
642 skip_pkgconfig = True
643
644 # find all .la and .pc files
645 # read the content
646 # and check for stuff that looks wrong
647 for root, dirs, files in os.walk(path):
648 for file in files:
649 path = os.path.join(root,file)
650 if file.endswith(".la") and not skip_la:
651 with open(path) as f:
652 file_content = f.read()
653 file_content = file_content.replace(recipesysroot, "")
654 if workdir in file_content:
655 error_msg = "%s failed sanity test (workdir) in path %s" % (file,root)
656 sane &= package_qa_handle_error("la", error_msg, d)
657 elif file.endswith(".pc") and not skip_pkgconfig:
658 with open(path) as f:
659 file_content = f.read()
660 file_content = file_content.replace(recipesysroot, "")
661 if pkgconfigcheck in file_content:
662 error_msg = "%s failed sanity test (tmpdir) in path %s" % (file,root)
663 sane &= package_qa_handle_error("pkgconfig", error_msg, d)
664
665 return sane
666
667# Run all package-wide warnfuncs and errorfuncs
668def package_qa_package(warnfuncs, errorfuncs, package, d):
669 warnings = {}
670 errors = {}
671
672 for func in warnfuncs:
673 func(package, d, warnings)
674 for func in errorfuncs:
675 func(package, d, errors)
676
677 for w in warnings:
678 package_qa_handle_error(w, warnings[w], d)
679 for e in errors:
680 package_qa_handle_error(e, errors[e], d)
681
682 return len(errors) == 0
683
684# Run all recipe-wide warnfuncs and errorfuncs
685def package_qa_recipe(warnfuncs, errorfuncs, pn, d):
686 warnings = {}
687 errors = {}
688
689 for func in warnfuncs:
690 func(pn, d, warnings)
691 for func in errorfuncs:
692 func(pn, d, errors)
693
694 for w in warnings:
695 package_qa_handle_error(w, warnings[w], d)
696 for e in errors:
697 package_qa_handle_error(e, errors[e], d)
698
699 return len(errors) == 0
700
701# Walk over all files in a directory and call func
702def package_qa_walk(warnfuncs, errorfuncs, package, d):
703 import oe.qa
704
705 #if this will throw an exception, then fix the dict above
706 target_os = d.getVar('TARGET_OS')
707 target_arch = d.getVar('TARGET_ARCH')
708
709 warnings = {}
710 errors = {}
711 for path in pkgfiles[package]:
712 elf = None
713 if os.path.isfile(path):
714 elf = oe.qa.ELFFile(path)
715 try:
716 elf.open()
717 except oe.qa.NotELFFileError:
718 elf = None
719 for func in warnfuncs:
720 func(path, package, d, elf, warnings)
721 for func in errorfuncs:
722 func(path, package, d, elf, errors)
723
724 for w in warnings:
725 package_qa_handle_error(w, warnings[w], d)
726 for e in errors:
727 package_qa_handle_error(e, errors[e], d)
728
729def package_qa_check_rdepends(pkg, pkgdest, skip, taskdeps, packages, d):
730 # Don't do this check for kernel/module recipes, there aren't too many debug/development
731 # packages and you can get false positives e.g. on kernel-module-lirc-dev
732 if bb.data.inherits_class("kernel", d) or bb.data.inherits_class("module-base", d):
733 return
734
735 if not "-dbg" in pkg and not "packagegroup-" in pkg and not "-image" in pkg:
736 localdata = bb.data.createCopy(d)
737 localdata.setVar('OVERRIDES', localdata.getVar('OVERRIDES') + ':' + pkg)
738
739 # Now check the RDEPENDS
740 rdepends = bb.utils.explode_deps(localdata.getVar('RDEPENDS') or "")
741
742 # Now do the sanity check!!!
743 if "build-deps" not in skip:
744 for rdepend in rdepends:
745 if "-dbg" in rdepend and "debug-deps" not in skip:
746 error_msg = "%s rdepends on %s" % (pkg,rdepend)
747 package_qa_handle_error("debug-deps", error_msg, d)
748 if (not "-dev" in pkg and not "-staticdev" in pkg) and rdepend.endswith("-dev") and "dev-deps" not in skip:
749 error_msg = "%s rdepends on %s" % (pkg, rdepend)
750 package_qa_handle_error("dev-deps", error_msg, d)
751 if rdepend not in packages:
752 rdep_data = oe.packagedata.read_subpkgdata(rdepend, d)
753 if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
754 continue
755 if not rdep_data or not 'PN' in rdep_data:
756 pkgdata_dir = d.getVar("PKGDATA_DIR")
757 try:
758 possibles = os.listdir("%s/runtime-rprovides/%s/" % (pkgdata_dir, rdepend))
759 except OSError:
760 possibles = []
761 for p in possibles:
762 rdep_data = oe.packagedata.read_subpkgdata(p, d)
763 if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
764 break
765 if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps:
766 continue
767 if rdep_data and 'PN' in rdep_data:
768 error_msg = "%s rdepends on %s, but it isn't a build dependency, missing %s in DEPENDS or PACKAGECONFIG?" % (pkg, rdepend, rdep_data['PN'])
769 else:
770 error_msg = "%s rdepends on %s, but it isn't a build dependency?" % (pkg, rdepend)
771 package_qa_handle_error("build-deps", error_msg, d)
772
773 if "file-rdeps" not in skip:
774 ignored_file_rdeps = set(['/bin/sh', '/usr/bin/env', 'rtld(GNU_HASH)'])
775 if bb.data.inherits_class('nativesdk', d):
776 ignored_file_rdeps |= set(['/bin/bash', '/usr/bin/perl', 'perl'])
777 # For Saving the FILERDEPENDS
778 filerdepends = {}
779 rdep_data = oe.packagedata.read_subpkgdata(pkg, d)
780 for key in rdep_data:
781 if key.startswith("FILERDEPENDS_"):
782 for subkey in bb.utils.explode_deps(rdep_data[key]):
783 if subkey not in ignored_file_rdeps and \
784 not subkey.startswith('perl('):
785 # We already know it starts with FILERDEPENDS_
786 filerdepends[subkey] = key[13:]
787
788 if filerdepends:
789 done = rdepends[:]
790 # Add the rprovides of itself
791 if pkg not in done:
792 done.insert(0, pkg)
793
794 # The python is not a package, but python-core provides it, so
795 # skip checking /usr/bin/python if python is in the rdeps, in
796 # case there is a RDEPENDS_pkg = "python" in the recipe.
797 for py in [ d.getVar('MLPREFIX') + "python", "python" ]:
798 if py in done:
799 filerdepends.pop("/usr/bin/python",None)
800 done.remove(py)
801 for rdep in done:
802 # The file dependencies may contain package names, e.g.,
803 # perl
804 filerdepends.pop(rdep,None)
805
806 # For Saving the FILERPROVIDES, RPROVIDES and FILES_INFO
807 rdep_data = oe.packagedata.read_subpkgdata(rdep, d)
808 for key in rdep_data:
809 if key.startswith("FILERPROVIDES_") or key.startswith("RPROVIDES_"):
810 for subkey in bb.utils.explode_deps(rdep_data[key]):
811 filerdepends.pop(subkey,None)
812 # Add the files list to the rprovides
813 if key == "FILES_INFO":
814 # Use eval() to make it as a dict
815 for subkey in eval(rdep_data[key]):
816 filerdepends.pop(subkey,None)
817 if not filerdepends:
818 # Break if all the file rdepends are met
819 break
820 if filerdepends:
821 for key in filerdepends:
822 error_msg = "%s contained in package %s requires %s, but no providers found in RDEPENDS_%s?" % \
823 (filerdepends[key].replace("_%s" % pkg, "").replace("@underscore@", "_"), pkg, key, pkg)
824 package_qa_handle_error("file-rdeps", error_msg, d)
825package_qa_check_rdepends[vardepsexclude] = "OVERRIDES"
826
827def package_qa_check_deps(pkg, pkgdest, d):
828
829 localdata = bb.data.createCopy(d)
830 localdata.setVar('OVERRIDES', pkg)
831
832 def check_valid_deps(var):
833 try:
834 rvar = bb.utils.explode_dep_versions2(localdata.getVar(var) or "")
835 except ValueError as e:
836 bb.fatal("%s_%s: %s" % (var, pkg, e))
837 for dep in rvar:
838 for v in rvar[dep]:
839 if v and not v.startswith(('< ', '= ', '> ', '<= ', '>=')):
840 error_msg = "%s_%s is invalid: %s (%s) only comparisons <, =, >, <=, and >= are allowed" % (var, pkg, dep, v)
841 package_qa_handle_error("dep-cmp", error_msg, d)
842
843 check_valid_deps('RDEPENDS')
844 check_valid_deps('RRECOMMENDS')
845 check_valid_deps('RSUGGESTS')
846 check_valid_deps('RPROVIDES')
847 check_valid_deps('RREPLACES')
848 check_valid_deps('RCONFLICTS')
849
850QAPKGTEST[usrmerge] = "package_qa_check_usrmerge"
851def package_qa_check_usrmerge(pkg, d, messages):
852 pkgdest = d.getVar('PKGDEST')
853 pkg_dir = pkgdest + os.sep + pkg + os.sep
854 merged_dirs = ['bin', 'sbin', 'lib'] + d.getVar('MULTILIB_VARIANTS').split()
855 for f in merged_dirs:
856 if os.path.exists(pkg_dir + f) and not os.path.islink(pkg_dir + f):
857 msg = "%s package is not obeying usrmerge distro feature. /%s should be relocated to /usr." % (pkg, f)
858 package_qa_add_message(messages, "usrmerge", msg)
859 return False
860 return True
861
862QAPKGTEST[perllocalpod] = "package_qa_check_perllocalpod"
863def package_qa_check_perllocalpod(pkg, d, messages):
864 """
865 Check that the recipe didn't ship a perlocal.pod file, which shouldn't be
866 installed in a distribution package. cpan.bbclass sets NO_PERLLOCAL=1 to
867 handle this for most recipes.
868 """
869 import glob
870 pkgd = oe.path.join(d.getVar('PKGDEST'), pkg)
871 podpath = oe.path.join(pkgd, d.getVar("libdir"), "perl*", "*", "*", "perllocal.pod")
872
873 matches = glob.glob(podpath)
874 if matches:
875 matches = [package_qa_clean_path(path, d, pkg) for path in matches]
876 msg = "%s contains perllocal.pod (%s), should not be installed" % (pkg, " ".join(matches))
877 package_qa_add_message(messages, "perllocalpod", msg)
878
879QAPKGTEST[expanded-d] = "package_qa_check_expanded_d"
880def package_qa_check_expanded_d(package, d, messages):
881 """
882 Check for the expanded D (${D}) value in pkg_* and FILES
883 variables, warn the user to use it correctly.
884 """
885 sane = True
886 expanded_d = d.getVar('D')
887
888 for var in 'FILES','pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm':
889 bbvar = d.getVar(var + "_" + package) or ""
890 if expanded_d in bbvar:
891 if var == 'FILES':
892 package_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)
893 sane = False
894 else:
895 package_qa_add_message(messages, "expanded-d", "%s in %s recipe contains ${D}, it should be replaced by $D instead" % (var, package))
896 sane = False
897 return sane
898
899QAPKGTEST[unlisted-pkg-lics] = "package_qa_check_unlisted_pkg_lics"
900def package_qa_check_unlisted_pkg_lics(package, d, messages):
901 """
902 Check that all licenses for a package are among the licenses for the recipe.
903 """
904 pkg_lics = d.getVar('LICENSE_' + package)
905 if not pkg_lics:
906 return True
907
908 recipe_lics_set = oe.license.list_licenses(d.getVar('LICENSE'))
909 unlisted = oe.license.list_licenses(pkg_lics) - recipe_lics_set
910 if not unlisted:
911 return True
912
913 package_qa_add_message(messages, "unlisted-pkg-lics",
914 "LICENSE_%s includes licenses (%s) that are not "
915 "listed in LICENSE" % (package, ' '.join(unlisted)))
916 return False
917
918def package_qa_check_encoding(keys, encode, d):
919 def check_encoding(key, enc):
920 sane = True
921 value = d.getVar(key)
922 if value:
923 try:
924 s = value.encode(enc)
925 except UnicodeDecodeError as e:
926 error_msg = "%s has non %s characters" % (key,enc)
927 sane = False
928 package_qa_handle_error("invalid-chars", error_msg, d)
929 return sane
930
931 for key in keys:
932 sane = check_encoding(key, encode)
933 if not sane:
934 break
935
936HOST_USER_UID := "${@os.getuid()}"
937HOST_USER_GID := "${@os.getgid()}"
938
939QAPATHTEST[host-user-contaminated] = "package_qa_check_host_user"
940def package_qa_check_host_user(path, name, d, elf, messages):
941 """Check for paths outside of /home which are owned by the user running bitbake."""
942
943 if not os.path.lexists(path):
944 return
945
946 dest = d.getVar('PKGDEST')
947 pn = d.getVar('PN')
948 home = os.path.join(dest, 'home')
949 if path == home or path.startswith(home + os.sep):
950 return
951
952 try:
953 stat = os.lstat(path)
954 except OSError as exc:
955 import errno
956 if exc.errno != errno.ENOENT:
957 raise
958 else:
959 check_uid = int(d.getVar('HOST_USER_UID'))
960 if stat.st_uid == check_uid:
961 package_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))
962 return False
963
964 check_gid = int(d.getVar('HOST_USER_GID'))
965 if stat.st_gid == check_gid:
966 package_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))
967 return False
968 return True
969
970QARECIPETEST[src-uri-bad] = "package_qa_check_src_uri"
971def package_qa_check_src_uri(pn, d, messages):
972 import re
973
974 if "${PN}" in d.getVar("SRC_URI", False):
975 package_qa_handle_error("src-uri-bad", "%s: SRC_URI uses PN not BPN" % pn, d)
976
977 for url in d.getVar("SRC_URI").split():
978 if re.search(r"git(hu|la)b\.com/.+/.+/archive/.+", url):
979 package_qa_handle_error("src-uri-bad", "%s: SRC_URI uses unstable GitHub/GitLab archives, convert recipe to use git protocol" % pn, d)
980
981QARECIPETEST[unhandled-features-check] = "package_qa_check_unhandled_features_check"
982def package_qa_check_unhandled_features_check(pn, d, messages):
983 if not bb.data.inherits_class('features_check', d):
984 var_set = False
985 for kind in ['DISTRO', 'MACHINE', 'COMBINED']:
986 for var in ['ANY_OF_' + kind + '_FEATURES', 'REQUIRED_' + kind + '_FEATURES', 'CONFLICT_' + kind + '_FEATURES']:
987 if d.getVar(var) is not None or d.overridedata.get(var) is not None:
988 var_set = True
989 if var_set:
990 package_qa_handle_error("unhandled-features-check", "%s: recipe doesn't inherit features_check" % pn, d)
991
992QARECIPETEST[missing-update-alternatives] = "package_qa_check_missing_update_alternatives"
993def package_qa_check_missing_update_alternatives(pn, d, messages):
994 # Look at all packages and find out if any of those sets ALTERNATIVE variable
995 # without inheriting update-alternatives class
996 for pkg in (d.getVar('PACKAGES') or '').split():
997 if d.getVar('ALTERNATIVE_%s' % pkg) and not bb.data.inherits_class('update-alternatives', d):
998 package_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)
999
1000# The PACKAGE FUNC to scan each package
1001python do_package_qa () {
1002 import subprocess
1003 import oe.packagedata
1004
1005 bb.note("DO PACKAGE QA")
1006
1007 bb.build.exec_func("read_subpackage_metadata", d)
1008
1009 # Check non UTF-8 characters on recipe's metadata
1010 package_qa_check_encoding(['DESCRIPTION', 'SUMMARY', 'LICENSE', 'SECTION'], 'utf-8', d)
1011
1012 logdir = d.getVar('T')
1013 pn = d.getVar('PN')
1014
1015 # Scan the packages...
1016 pkgdest = d.getVar('PKGDEST')
1017 packages = set((d.getVar('PACKAGES') or '').split())
1018
1019 global pkgfiles
1020 pkgfiles = {}
1021 for pkg in packages:
1022 pkgfiles[pkg] = []
1023 pkgdir = os.path.join(pkgdest, pkg)
1024 for walkroot, dirs, files in os.walk(pkgdir):
1025 # Don't walk into top-level CONTROL or DEBIAN directories as these
1026 # are temporary directories created by do_package.
1027 if walkroot == pkgdir:
1028 for control in ("CONTROL", "DEBIAN"):
1029 if control in dirs:
1030 dirs.remove(control)
1031 for file in files:
1032 pkgfiles[pkg].append(os.path.join(walkroot, file))
1033
1034 # no packages should be scanned
1035 if not packages:
1036 return
1037
1038 import re
1039 # The package name matches the [a-z0-9.+-]+ regular expression
1040 pkgname_pattern = re.compile(r"^[a-z0-9.+-]+$")
1041
1042 taskdepdata = d.getVar("BB_TASKDEPDATA", False)
1043 taskdeps = set()
1044 for dep in taskdepdata:
1045 taskdeps.add(taskdepdata[dep][0])
1046
1047 def parse_test_matrix(matrix_name):
1048 testmatrix = d.getVarFlags(matrix_name) or {}
1049 g = globals()
1050 warnchecks = []
1051 for w in (d.getVar("WARN_QA") or "").split():
1052 if w in skip:
1053 continue
1054 if w in testmatrix and testmatrix[w] in g:
1055 warnchecks.append(g[testmatrix[w]])
1056
1057 errorchecks = []
1058 for e in (d.getVar("ERROR_QA") or "").split():
1059 if e in skip:
1060 continue
1061 if e in testmatrix and testmatrix[e] in g:
1062 errorchecks.append(g[testmatrix[e]])
1063 return warnchecks, errorchecks
1064
1065 for package in packages:
1066 skip = set((d.getVar('INSANE_SKIP') or "").split() +
1067 (d.getVar('INSANE_SKIP_' + package) or "").split())
1068 if skip:
1069 bb.note("Package %s skipping QA tests: %s" % (package, str(skip)))
1070
1071 bb.note("Checking Package: %s" % package)
1072 # Check package name
1073 if not pkgname_pattern.match(package):
1074 package_qa_handle_error("pkgname",
1075 "%s doesn't match the [a-z0-9.+-]+ regex" % package, d)
1076
1077 warn_checks, error_checks = parse_test_matrix("QAPATHTEST")
1078 package_qa_walk(warn_checks, error_checks, package, d)
1079
1080 warn_checks, error_checks = parse_test_matrix("QAPKGTEST")
1081 package_qa_package(warn_checks, error_checks, package, d)
1082
1083 package_qa_check_rdepends(package, pkgdest, skip, taskdeps, packages, d)
1084 package_qa_check_deps(package, pkgdest, d)
1085
1086 warn_checks, error_checks = parse_test_matrix("QARECIPETEST")
1087 package_qa_recipe(warn_checks, error_checks, pn, d)
1088
1089 if 'libdir' in d.getVar("ALL_QA").split():
1090 package_qa_check_libdir(d)
1091
1092 qa_sane = d.getVar("QA_SANE")
1093 if not qa_sane:
1094 bb.fatal("QA run found fatal errors. Please consider fixing them.")
1095 bb.note("DONE with PACKAGE QA")
1096}
1097
1098# binutils is used for most checks, so need to set as dependency
1099# POPULATESYSROOTDEPS is defined in staging class.
1100do_package_qa[depends] += "${POPULATESYSROOTDEPS}"
1101do_package_qa[vardepsexclude] = "BB_TASKDEPDATA"
1102do_package_qa[rdeptask] = "do_packagedata"
1103addtask do_package_qa after do_packagedata do_package before do_build
1104
1105# Add the package specific INSANE_SKIPs to the sstate dependencies
1106python() {
1107 pkgs = (d.getVar('PACKAGES') or '').split()
1108 for pkg in pkgs:
1109 d.appendVarFlag("do_package_qa", "vardeps", " INSANE_SKIP_{}".format(pkg))
1110}
1111
1112SSTATETASKS += "do_package_qa"
1113do_package_qa[sstate-inputdirs] = ""
1114do_package_qa[sstate-outputdirs] = ""
1115python do_package_qa_setscene () {
1116 sstate_setscene(d)
1117}
1118addtask do_package_qa_setscene
1119
1120python do_qa_staging() {
1121 bb.note("QA checking staging")
1122 if not qa_check_staged(d.expand('${SYSROOT_DESTDIR}${libdir}'), d):
1123 bb.fatal("QA staging was broken by the package built above")
1124}
1125
1126python do_qa_patch() {
1127 import subprocess
1128
1129 ###########################################################################
1130 # Check patch.log for fuzz warnings
1131 #
1132 # Further information on why we check for patch fuzz warnings:
1133 # http://lists.openembedded.org/pipermail/openembedded-core/2018-March/148675.html
1134 # https://bugzilla.yoctoproject.org/show_bug.cgi?id=10450
1135 ###########################################################################
1136
1137 logdir = d.getVar('T')
1138 patchlog = os.path.join(logdir,"log.do_patch")
1139
1140 if os.path.exists(patchlog):
1141 fuzzheader = '--- Patch fuzz start ---'
1142 fuzzfooter = '--- Patch fuzz end ---'
1143 statement = "grep -e '%s' %s > /dev/null" % (fuzzheader, patchlog)
1144 if subprocess.call(statement, shell=True) == 0:
1145 msg = "Fuzz detected:\n\n"
1146 fuzzmsg = ""
1147 inFuzzInfo = False
1148 f = open(patchlog, "r")
1149 for line in f:
1150 if fuzzheader in line:
1151 inFuzzInfo = True
1152 fuzzmsg = ""
1153 elif fuzzfooter in line:
1154 fuzzmsg = fuzzmsg.replace('\n\n', '\n')
1155 msg += fuzzmsg
1156 msg += "\n"
1157 inFuzzInfo = False
1158 elif inFuzzInfo and not 'Now at patch' in line:
1159 fuzzmsg += line
1160 f.close()
1161 msg += "The context lines in the patches can be updated with devtool:\n"
1162 msg += "\n"
1163 msg += " devtool modify %s\n" % d.getVar('PN')
1164 msg += " devtool finish --force-patch-refresh %s <layer_path>\n\n" % d.getVar('PN')
1165 msg += "Don't forget to review changes done by devtool!\n"
1166 if 'patch-fuzz' in d.getVar('ERROR_QA'):
1167 bb.error(msg)
1168 elif 'patch-fuzz' in d.getVar('WARN_QA'):
1169 bb.warn(msg)
1170 msg = "Patch log indicates that patches do not apply cleanly."
1171 package_qa_handle_error("patch-fuzz", msg, d)
1172}
1173
1174python do_qa_configure() {
1175 import subprocess
1176
1177 ###########################################################################
1178 # Check config.log for cross compile issues
1179 ###########################################################################
1180
1181 configs = []
1182 workdir = d.getVar('WORKDIR')
1183
1184 skip = (d.getVar('INSANE_SKIP') or "").split()
1185 skip_configure_unsafe = False
1186 if 'configure-unsafe' in skip:
1187 bb.note("Recipe %s skipping qa checking: configure-unsafe" % d.getVar('PN'))
1188 skip_configure_unsafe = True
1189
1190 if bb.data.inherits_class('autotools', d) and not skip_configure_unsafe:
1191 bb.note("Checking autotools environment for common misconfiguration")
1192 for root, dirs, files in os.walk(workdir):
1193 statement = "grep -q -F -e 'is unsafe for cross-compilation' %s" % \
1194 os.path.join(root,"config.log")
1195 if "config.log" in files:
1196 if subprocess.call(statement, shell=True) == 0:
1197 error_msg = """This autoconf log indicates errors, it looked at host include and/or library paths while determining system capabilities.
1198Rerun configure task after fixing this."""
1199 package_qa_handle_error("configure-unsafe", error_msg, d)
1200
1201 if "configure.ac" in files:
1202 configs.append(os.path.join(root,"configure.ac"))
1203 if "configure.in" in files:
1204 configs.append(os.path.join(root, "configure.in"))
1205
1206 ###########################################################################
1207 # Check gettext configuration and dependencies are correct
1208 ###########################################################################
1209
1210 skip_configure_gettext = False
1211 if 'configure-gettext' in skip:
1212 bb.note("Recipe %s skipping qa checking: configure-gettext" % d.getVar('PN'))
1213 skip_configure_gettext = True
1214
1215 cnf = d.getVar('EXTRA_OECONF') or ""
1216 if not ("gettext" in d.getVar('P') or "gcc-runtime" in d.getVar('P') or \
1217 "--disable-nls" in cnf or skip_configure_gettext):
1218 ml = d.getVar("MLPREFIX") or ""
1219 if bb.data.inherits_class('cross-canadian', d):
1220 gt = "nativesdk-gettext"
1221 else:
1222 gt = "gettext-native"
1223 deps = bb.utils.explode_deps(d.getVar('DEPENDS') or "")
1224 if gt not in deps:
1225 for config in configs:
1226 gnu = "grep \"^[[:space:]]*AM_GNU_GETTEXT\" %s >/dev/null" % config
1227 if subprocess.call(gnu, shell=True) == 0:
1228 error_msg = "AM_GNU_GETTEXT used but no inherit gettext"
1229 package_qa_handle_error("configure-gettext", error_msg, d)
1230
1231 ###########################################################################
1232 # Check unrecognised configure options (with a white list)
1233 ###########################################################################
1234 if bb.data.inherits_class("autotools", d) or bb.data.inherits_class("meson", d):
1235 bb.note("Checking configure output for unrecognised options")
1236 try:
1237 if bb.data.inherits_class("autotools", d):
1238 flag = "WARNING: unrecognized options:"
1239 log = os.path.join(d.getVar('B'), 'config.log')
1240 if bb.data.inherits_class("meson", d):
1241 flag = "WARNING: Unknown options:"
1242 log = os.path.join(d.getVar('T'), 'log.do_configure')
1243 output = subprocess.check_output(['grep', '-F', flag, log]).decode("utf-8").replace(', ', ' ').replace('"', '')
1244 options = set()
1245 for line in output.splitlines():
1246 options |= set(line.partition(flag)[2].split())
1247 whitelist = set(d.getVar("UNKNOWN_CONFIGURE_WHITELIST").split())
1248 options -= whitelist
1249 if options:
1250 pn = d.getVar('PN')
1251 error_msg = pn + ": configure was passed unrecognised options: " + " ".join(options)
1252 package_qa_handle_error("unknown-configure-option", error_msg, d)
1253 except subprocess.CalledProcessError:
1254 pass
1255
1256 # Check invalid PACKAGECONFIG
1257 pkgconfig = (d.getVar("PACKAGECONFIG") or "").split()
1258 if pkgconfig:
1259 pkgconfigflags = d.getVarFlags("PACKAGECONFIG") or {}
1260 for pconfig in pkgconfig:
1261 if pconfig not in pkgconfigflags:
1262 pn = d.getVar('PN')
1263 error_msg = "%s: invalid PACKAGECONFIG: %s" % (pn, pconfig)
1264 package_qa_handle_error("invalid-packageconfig", error_msg, d)
1265
1266 qa_sane = d.getVar("QA_SANE")
1267 if not qa_sane:
1268 bb.fatal("Fatal QA errors found, failing task.")
1269}
1270
1271python do_qa_unpack() {
1272 src_uri = d.getVar('SRC_URI')
1273 s_dir = d.getVar('S')
1274 if src_uri and not os.path.exists(s_dir):
1275 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))
1276}
1277
1278# The Staging Func, to check all staging
1279#addtask qa_staging after do_populate_sysroot before do_build
1280do_populate_sysroot[postfuncs] += "do_qa_staging "
1281
1282# Check for patch fuzz
1283do_patch[postfuncs] += "do_qa_patch "
1284
1285# Check broken config.log files, for packages requiring Gettext which
1286# don't have it in DEPENDS.
1287#addtask qa_configure after do_configure before do_compile
1288do_configure[postfuncs] += "do_qa_configure "
1289
1290# Check does S exist.
1291do_unpack[postfuncs] += "do_qa_unpack"
1292
1293python () {
1294 import re
1295
1296 tests = d.getVar('ALL_QA').split()
1297 if "desktop" in tests:
1298 d.appendVar("PACKAGE_DEPENDS", " desktop-file-utils-native")
1299
1300 ###########################################################################
1301 # Check various variables
1302 ###########################################################################
1303
1304 # Checking ${FILESEXTRAPATHS}
1305 extrapaths = (d.getVar("FILESEXTRAPATHS") or "")
1306 if '__default' not in extrapaths.split(":"):
1307 msg = "FILESEXTRAPATHS-variable, must always use _prepend (or _append)\n"
1308 msg += "type of assignment, and don't forget the colon.\n"
1309 msg += "Please assign it with the format of:\n"
1310 msg += " FILESEXTRAPATHS_append := \":${THISDIR}/Your_Files_Path\" or\n"
1311 msg += " FILESEXTRAPATHS_prepend := \"${THISDIR}/Your_Files_Path:\"\n"
1312 msg += "in your bbappend file\n\n"
1313 msg += "Your incorrect assignment is:\n"
1314 msg += "%s\n" % extrapaths
1315 bb.warn(msg)
1316
1317 overrides = d.getVar('OVERRIDES').split(':')
1318 pn = d.getVar('PN')
1319 if pn in overrides:
1320 msg = 'Recipe %s has PN of "%s" which is in OVERRIDES, this can result in unexpected behaviour.' % (d.getVar("FILE"), pn)
1321 package_qa_handle_error("pn-overrides", msg, d)
1322 prog = re.compile(r'[A-Z]')
1323 if prog.search(pn):
1324 package_qa_handle_error("uppercase-pn", 'PN: %s is upper case, this can result in unexpected behavior.' % pn, d)
1325
1326 # Some people mistakenly use DEPENDS_${PN} instead of DEPENDS and wonder
1327 # why it doesn't work.
1328 if (d.getVar(d.expand('DEPENDS_${PN}'))):
1329 package_qa_handle_error("pkgvarcheck", "recipe uses DEPENDS_${PN}, should use DEPENDS", d)
1330
1331 issues = []
1332 if (d.getVar('PACKAGES') or "").split():
1333 for dep in (d.getVar('QADEPENDS') or "").split():
1334 d.appendVarFlag('do_package_qa', 'depends', " %s:do_populate_sysroot" % dep)
1335 for var in 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RCONFLICTS', 'RPROVIDES', 'RREPLACES', 'FILES', 'pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm', 'ALLOW_EMPTY':
1336 if d.getVar(var, False):
1337 issues.append(var)
1338
1339 fakeroot_tests = d.getVar('FAKEROOT_QA').split()
1340 if set(tests) & set(fakeroot_tests):
1341 d.setVarFlag('do_package_qa', 'fakeroot', '1')
1342 d.appendVarFlag('do_package_qa', 'depends', ' virtual/fakeroot-native:do_populate_sysroot')
1343 else:
1344 d.setVarFlag('do_package_qa', 'rdeptask', '')
1345 for i in issues:
1346 package_qa_handle_error("pkgvarcheck", "%s: Variable %s is set as not being package specific, please fix this." % (d.getVar("FILE"), i), d)
1347 qa_sane = d.getVar("QA_SANE")
1348 if not qa_sane:
1349 bb.fatal("Fatal QA errors found, failing task.")
1350}