def do_package_qa(d): import subprocess import oe.packagedata bb.note("DO PACKAGE QA") bb.build.exec_func("read_subpackage_metadata", d) # Check non UTF-8 characters on recipe's metadata package_qa_check_encoding(['DESCRIPTION', 'SUMMARY', 'LICENSE', 'SECTION'], 'utf-8', d) logdir = d.getVar('T') pn = d.getVar('PN') # Scan the packages... pkgdest = d.getVar('PKGDEST') packages = set((d.getVar('PACKAGES') or '').split()) global pkgfiles pkgfiles = {} for pkg in packages: pkgfiles[pkg] = [] pkgdir = os.path.join(pkgdest, pkg) for walkroot, dirs, files in os.walk(pkgdir): # Don't walk into top-level CONTROL or DEBIAN directories as these # are temporary directories created by do_package. if walkroot == pkgdir: for control in ("CONTROL", "DEBIAN"): if control in dirs: dirs.remove(control) for file in files: pkgfiles[pkg].append(os.path.join(walkroot, file)) # no packages should be scanned if not packages: return import re # The package name matches the [a-z0-9.+-]+ regular expression pkgname_pattern = re.compile(r"^[a-z0-9.+-]+$") taskdepdata = d.getVar("BB_TASKDEPDATA", False) taskdeps = set() for dep in taskdepdata: taskdeps.add(taskdepdata[dep][0]) def parse_test_matrix(matrix_name): testmatrix = d.getVarFlags(matrix_name) or {} g = globals() warnchecks = [] for w in (d.getVar("WARN_QA") or "").split(): if w in skip: continue if w in testmatrix and testmatrix[w] in g: warnchecks.append(g[testmatrix[w]]) errorchecks = [] for e in (d.getVar("ERROR_QA") or "").split(): if e in skip: continue if e in testmatrix and testmatrix[e] in g: errorchecks.append(g[testmatrix[e]]) return warnchecks, errorchecks for package in packages: skip = set((d.getVar('INSANE_SKIP') or "").split() + (d.getVar('INSANE_SKIP_' + package) or "").split()) if skip: bb.note("Package %s skipping QA tests: %s" % (package, str(skip))) bb.note("Checking Package: %s" % package) # Check package name if not pkgname_pattern.match(package): package_qa_handle_error("pkgname", "%s doesn't match the [a-z0-9.+-]+ regex" % package, d) warn_checks, error_checks = parse_test_matrix("QAPATHTEST") package_qa_walk(warn_checks, error_checks, package, d) warn_checks, error_checks = parse_test_matrix("QAPKGTEST") package_qa_package(warn_checks, error_checks, package, d) package_qa_check_rdepends(package, pkgdest, skip, taskdeps, packages, d) package_qa_check_deps(package, pkgdest, d) warn_checks, error_checks = parse_test_matrix("QARECIPETEST") package_qa_recipe(warn_checks, error_checks, pn, d) if 'libdir' in d.getVar("ALL_QA").split(): package_qa_check_libdir(d) qa_sane = d.getVar("QA_SANE") if not qa_sane: bb.fatal("QA run found fatal errors. Please consider fixing them.") bb.note("DONE with PACKAGE QA") do_package_qa(d) def package_qa_check_rdepends(pkg, pkgdest, skip, taskdeps, packages, d): # Don't do this check for kernel/module recipes, there aren't too many debug/development # packages and you can get false positives e.g. on kernel-module-lirc-dev if bb.data.inherits_class("kernel", d) or bb.data.inherits_class("module-base", d): return if not "-dbg" in pkg and not "packagegroup-" in pkg and not "-image" in pkg: localdata = bb.data.createCopy(d) localdata.setVar('OVERRIDES', localdata.getVar('OVERRIDES') + ':' + pkg) # Now check the RDEPENDS rdepends = bb.utils.explode_deps(localdata.getVar('RDEPENDS') or "") # Now do the sanity check!!! if "build-deps" not in skip: for rdepend in rdepends: if "-dbg" in rdepend and "debug-deps" not in skip: error_msg = "%s rdepends on %s" % (pkg,rdepend) package_qa_handle_error("debug-deps", error_msg, d) if (not "-dev" in pkg and not "-staticdev" in pkg) and rdepend.endswith("-dev") and "dev-deps" not in skip: error_msg = "%s rdepends on %s" % (pkg, rdepend) package_qa_handle_error("dev-deps", error_msg, d) if rdepend not in packages: rdep_data = oe.packagedata.read_subpkgdata(rdepend, d) if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps: continue if not rdep_data or not 'PN' in rdep_data: pkgdata_dir = d.getVar("PKGDATA_DIR") try: possibles = os.listdir("%s/runtime-rprovides/%s/" % (pkgdata_dir, rdepend)) except OSError: possibles = [] for p in possibles: rdep_data = oe.packagedata.read_subpkgdata(p, d) if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps: break if rdep_data and 'PN' in rdep_data and rdep_data['PN'] in taskdeps: continue if rdep_data and 'PN' in rdep_data: error_msg = "%s rdepends on %s, but it isn't a build dependency, missing %s in DEPENDS or PACKAGECONFIG?" % (pkg, rdepend, rdep_data['PN']) else: error_msg = "%s rdepends on %s, but it isn't a build dependency?" % (pkg, rdepend) package_qa_handle_error("build-deps", error_msg, d) if "file-rdeps" not in skip: ignored_file_rdeps = set(['/bin/sh', '/usr/bin/env', 'rtld(GNU_HASH)']) if bb.data.inherits_class('nativesdk', d): ignored_file_rdeps |= set(['/bin/bash', '/usr/bin/perl', 'perl']) # For Saving the FILERDEPENDS filerdepends = {} rdep_data = oe.packagedata.read_subpkgdata(pkg, d) for key in rdep_data: if key.startswith("FILERDEPENDS_"): for subkey in bb.utils.explode_deps(rdep_data[key]): if subkey not in ignored_file_rdeps and \ not subkey.startswith('perl('): # We already know it starts with FILERDEPENDS_ filerdepends[subkey] = key[13:] if filerdepends: done = rdepends[:] # Add the rprovides of itself if pkg not in done: done.insert(0, pkg) # The python is not a package, but python-core provides it, so # skip checking /usr/bin/python if python is in the rdeps, in # case there is a RDEPENDS_pkg = "python" in the recipe. for py in [ d.getVar('MLPREFIX') + "python", "python" ]: if py in done: filerdepends.pop("/usr/bin/python",None) done.remove(py) for rdep in done: # The file dependencies may contain package names, e.g., # perl filerdepends.pop(rdep,None) # For Saving the FILERPROVIDES, RPROVIDES and FILES_INFO rdep_data = oe.packagedata.read_subpkgdata(rdep, d) for key in rdep_data: if key.startswith("FILERPROVIDES_") or key.startswith("RPROVIDES_"): for subkey in bb.utils.explode_deps(rdep_data[key]): filerdepends.pop(subkey,None) # Add the files list to the rprovides if key == "FILES_INFO": # Use eval() to make it as a dict for subkey in eval(rdep_data[key]): filerdepends.pop(subkey,None) if not filerdepends: # Break if all the file rdepends are met break if filerdepends: for key in filerdepends: error_msg = "%s contained in package %s requires %s, but no providers found in RDEPENDS_%s?" % \ (filerdepends[key].replace("_%s" % pkg, "").replace("@underscore@", "_"), pkg, key, pkg) package_qa_handle_error("file-rdeps", error_msg, d) def package_qa_check_encoding(keys, encode, d): def check_encoding(key, enc): sane = True value = d.getVar(key) if value: try: s = value.encode(enc) except UnicodeDecodeError as e: error_msg = "%s has non %s characters" % (key,enc) sane = False package_qa_handle_error("invalid-chars", error_msg, d) return sane for key in keys: sane = check_encoding(key, encode) if not sane: break def package_qa_walk(warnfuncs, errorfuncs, package, d): import oe.qa #if this will throw an exception, then fix the dict above target_os = d.getVar('TARGET_OS') target_arch = d.getVar('TARGET_ARCH') warnings = {} errors = {} for path in pkgfiles[package]: elf = None if os.path.isfile(path): elf = oe.qa.ELFFile(path) try: elf.open() except oe.qa.NotELFFileError: elf = None for func in warnfuncs: func(path, package, d, elf, warnings) for func in errorfuncs: func(path, package, d, elf, errors) for w in warnings: package_qa_handle_error(w, warnings[w], d) for e in errors: package_qa_handle_error(e, errors[e], d) def package_qa_check_libdir(d): """ Check for wrong library installation paths. For instance, catch recipes installing /lib/bar.so when ${base_libdir}="lib32" or installing in /usr/lib64 when ${libdir}="/usr/lib" """ import re pkgdest = d.getVar('PKGDEST') base_libdir = d.getVar("base_libdir") + os.sep libdir = d.getVar("libdir") + os.sep libexecdir = d.getVar("libexecdir") + os.sep exec_prefix = d.getVar("exec_prefix") + os.sep messages = [] # The re's are purposely fuzzy, as some there are some .so.x.y.z files # that don't follow the standard naming convention. It checks later # that they are actual ELF files lib_re = re.compile(r"^/lib.+\.so(\..+)?$") exec_re = re.compile(r"^%s.*/lib.+\.so(\..+)?$" % exec_prefix) for root, dirs, files in os.walk(pkgdest): if root == pkgdest: # Skip subdirectories for any packages with libdir in INSANE_SKIP skippackages = [] for package in dirs: if 'libdir' in (d.getVar('INSANE_SKIP_' + package) or "").split(): bb.note("Package %s skipping libdir QA test" % (package)) skippackages.append(package) elif d.getVar('PACKAGE_DEBUG_SPLIT_STYLE') == 'debug-file-directory' and package.endswith("-dbg"): bb.note("Package %s skipping libdir QA test for PACKAGE_DEBUG_SPLIT_STYLE equals debug-file-directory" % (package)) skippackages.append(package) for package in skippackages: dirs.remove(package) for file in files: full_path = os.path.join(root, file) rel_path = os.path.relpath(full_path, pkgdest) if os.sep in rel_path: package, rel_path = rel_path.split(os.sep, 1) rel_path = os.sep + rel_path if lib_re.match(rel_path): if base_libdir not in rel_path: # make sure it's an actual ELF file elf = oe.qa.ELFFile(full_path) try: elf.open() messages.append("%s: found library in wrong location: %s" % (package, rel_path)) except (oe.qa.NotELFFileError): pass if exec_re.match(rel_path): if libdir not in rel_path and libexecdir not in rel_path: # make sure it's an actual ELF file elf = oe.qa.ELFFile(full_path) try: elf.open() messages.append("%s: found library in wrong location: %s" % (package, rel_path)) except (oe.qa.NotELFFileError): pass if messages: package_qa_handle_error("libdir", "\n".join(messages), d) def package_qa_package(warnfuncs, errorfuncs, package, d): warnings = {} errors = {} for func in warnfuncs: func(package, d, warnings) for func in errorfuncs: func(package, d, errors) for w in warnings: package_qa_handle_error(w, warnings[w], d) for e in errors: package_qa_handle_error(e, errors[e], d) return len(errors) == 0 # Run all recipe-wide warnfuncs and errorfuncs def read_subpackage_metadata(d): import oe.packagedata vars = { "PN" : d.getVar('PN'), "PE" : d.getVar('PE'), "PV" : d.getVar('PV'), "PR" : d.getVar('PR'), } data = oe.packagedata.read_pkgdata(vars["PN"], d) for key in data.keys(): d.setVar(key, data[key]) for pkg in d.getVar('PACKAGES').split(): sdata = oe.packagedata.read_subpkgdata(pkg, d) for key in sdata.keys(): if key in vars: if sdata[key] != vars[key]: if key == "PN": bb.fatal("Recipe %s is trying to create package %s which was already written by recipe %s. This will cause corruption, please resolve this and only provide the package from one recipe or the other or only build one of the recipes." % (vars[key], pkg, sdata[key])) bb.fatal("Recipe %s is trying to change %s from '%s' to '%s'. This will cause do_package_write_* failures since the incorrect data will be used and they will be unable to find the right workdir." % (vars["PN"], key, vars[key], sdata[key])) continue # # If we set unsuffixed variables here there is a chance they could clobber override versions # of that variable, e.g. DESCRIPTION could clobber DESCRIPTION_ # We therefore don't clobber for the unsuffixed variable versions # if key.endswith("_" + pkg): d.setVar(key, sdata[key]) else: d.setVar(key, sdata[key], parsing=True) def package_qa_recipe(warnfuncs, errorfuncs, pn, d): warnings = {} errors = {} for func in warnfuncs: func(pn, d, warnings) for func in errorfuncs: func(pn, d, errors) for w in warnings: package_qa_handle_error(w, warnings[w], d) for e in errors: package_qa_handle_error(e, errors[e], d) return len(errors) == 0 # Walk over all files in a directory and call func def package_qa_check_deps(pkg, pkgdest, d): localdata = bb.data.createCopy(d) localdata.setVar('OVERRIDES', pkg) def check_valid_deps(var): try: rvar = bb.utils.explode_dep_versions2(localdata.getVar(var) or "") except ValueError as e: bb.fatal("%s_%s: %s" % (var, pkg, e)) for dep in rvar: for v in rvar[dep]: if v and not v.startswith(('< ', '= ', '> ', '<= ', '>=')): error_msg = "%s_%s is invalid: %s (%s) only comparisons <, =, >, <=, and >= are allowed" % (var, pkg, dep, v) package_qa_handle_error("dep-cmp", error_msg, d) check_valid_deps('RDEPENDS') check_valid_deps('RRECOMMENDS') check_valid_deps('RSUGGESTS') check_valid_deps('RPROVIDES') check_valid_deps('RREPLACES') check_valid_deps('RCONFLICTS') def package_qa_handle_error(error_class, error_msg, d): if error_class in (d.getVar("ERROR_QA") or "").split(): package_qa_write_error(error_class, error_msg, d) bb.error("QA Issue: %s [%s]" % (error_msg, error_class)) d.setVar("QA_SANE", False) return False elif error_class in (d.getVar("WARN_QA") or "").split(): package_qa_write_error(error_class, error_msg, d) bb.warn("QA Issue: %s [%s]" % (error_msg, error_class)) else: bb.note("QA Issue: %s [%s]" % (error_msg, error_class)) return True def package_qa_write_error(type, error, d): logfile = d.getVar('QA_LOGFILE') if logfile: p = d.getVar('P') with open(logfile, "a+") as f: f.write("%s: %s [%s]\n" % (p, error, type))