Squashed 'yocto-poky/' content from commit ea562de
git-subtree-dir: yocto-poky
git-subtree-split: ea562de57590c966cd5a75fda8defecd397e6436
diff --git a/meta/lib/oe/__init__.py b/meta/lib/oe/__init__.py
new file mode 100644
index 0000000..3ad9513
--- /dev/null
+++ b/meta/lib/oe/__init__.py
@@ -0,0 +1,2 @@
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
diff --git a/meta/lib/oe/buildhistory_analysis.py b/meta/lib/oe/buildhistory_analysis.py
new file mode 100644
index 0000000..5395c76
--- /dev/null
+++ b/meta/lib/oe/buildhistory_analysis.py
@@ -0,0 +1,456 @@
+# Report significant differences in the buildhistory repository since a specific revision
+#
+# Copyright (C) 2012 Intel Corporation
+# Author: Paul Eggleton <paul.eggleton@linux.intel.com>
+#
+# Note: requires GitPython 0.3.1+
+#
+# You can use this from the command line by running scripts/buildhistory-diff
+#
+
+import sys
+import os.path
+import difflib
+import git
+import re
+import bb.utils
+
+
+# How to display fields
+list_fields = ['DEPENDS', 'RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RREPLACES', 'RCONFLICTS', 'FILES', 'FILELIST', 'USER_CLASSES', 'IMAGE_CLASSES', 'IMAGE_FEATURES', 'IMAGE_LINGUAS', 'IMAGE_INSTALL', 'BAD_RECOMMENDATIONS', 'PACKAGE_EXCLUDE']
+list_order_fields = ['PACKAGES']
+defaultval_map = {'PKG': 'PKG', 'PKGE': 'PE', 'PKGV': 'PV', 'PKGR': 'PR'}
+numeric_fields = ['PKGSIZE', 'IMAGESIZE']
+# Fields to monitor
+monitor_fields = ['RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RREPLACES', 'RCONFLICTS', 'PACKAGES', 'FILELIST', 'PKGSIZE', 'IMAGESIZE', 'PKG']
+ver_monitor_fields = ['PKGE', 'PKGV', 'PKGR']
+# Percentage change to alert for numeric fields
+monitor_numeric_threshold = 10
+# Image files to monitor (note that image-info.txt is handled separately)
+img_monitor_files = ['installed-package-names.txt', 'files-in-image.txt']
+# Related context fields for reporting (note: PE, PV & PR are always reported for monitored package fields)
+related_fields = {}
+related_fields['RDEPENDS'] = ['DEPENDS']
+related_fields['RRECOMMENDS'] = ['DEPENDS']
+related_fields['FILELIST'] = ['FILES']
+related_fields['PKGSIZE'] = ['FILELIST']
+related_fields['files-in-image.txt'] = ['installed-package-names.txt', 'USER_CLASSES', 'IMAGE_CLASSES', 'ROOTFS_POSTPROCESS_COMMAND', 'IMAGE_POSTPROCESS_COMMAND']
+related_fields['installed-package-names.txt'] = ['IMAGE_FEATURES', 'IMAGE_LINGUAS', 'IMAGE_INSTALL', 'BAD_RECOMMENDATIONS', 'NO_RECOMMENDATIONS', 'PACKAGE_EXCLUDE']
+
+
+class ChangeRecord:
+ def __init__(self, path, fieldname, oldvalue, newvalue, monitored):
+ self.path = path
+ self.fieldname = fieldname
+ self.oldvalue = oldvalue
+ self.newvalue = newvalue
+ self.monitored = monitored
+ self.related = []
+ self.filechanges = None
+
+ def __str__(self):
+ return self._str_internal(True)
+
+ def _str_internal(self, outer):
+ if outer:
+ if '/image-files/' in self.path:
+ prefix = '%s: ' % self.path.split('/image-files/')[0]
+ else:
+ prefix = '%s: ' % self.path
+ else:
+ prefix = ''
+
+ def pkglist_combine(depver):
+ pkglist = []
+ for k,v in depver.iteritems():
+ if v:
+ pkglist.append("%s (%s)" % (k,v))
+ else:
+ pkglist.append(k)
+ return pkglist
+
+ if self.fieldname in list_fields or self.fieldname in list_order_fields:
+ if self.fieldname in ['RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RREPLACES', 'RCONFLICTS']:
+ (depvera, depverb) = compare_pkg_lists(self.oldvalue, self.newvalue)
+ aitems = pkglist_combine(depvera)
+ bitems = pkglist_combine(depverb)
+ else:
+ aitems = self.oldvalue.split()
+ bitems = self.newvalue.split()
+ removed = list(set(aitems) - set(bitems))
+ added = list(set(bitems) - set(aitems))
+
+ if removed or added:
+ if removed and not bitems:
+ out = '%s: removed all items "%s"' % (self.fieldname, ' '.join(removed))
+ else:
+ out = '%s:%s%s' % (self.fieldname, ' removed "%s"' % ' '.join(removed) if removed else '', ' added "%s"' % ' '.join(added) if added else '')
+ else:
+ out = '%s changed order' % self.fieldname
+ elif self.fieldname in numeric_fields:
+ aval = int(self.oldvalue or 0)
+ bval = int(self.newvalue or 0)
+ if aval != 0:
+ percentchg = ((bval - aval) / float(aval)) * 100
+ else:
+ percentchg = 100
+ out = '%s changed from %s to %s (%s%d%%)' % (self.fieldname, self.oldvalue or "''", self.newvalue or "''", '+' if percentchg > 0 else '', percentchg)
+ elif self.fieldname in defaultval_map:
+ out = '%s changed from %s to %s' % (self.fieldname, self.oldvalue, self.newvalue)
+ if self.fieldname == 'PKG' and '[default]' in self.newvalue:
+ out += ' - may indicate debian renaming failure'
+ elif self.fieldname in ['pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm']:
+ if self.oldvalue and self.newvalue:
+ out = '%s changed:\n ' % self.fieldname
+ elif self.newvalue:
+ out = '%s added:\n ' % self.fieldname
+ elif self.oldvalue:
+ out = '%s cleared:\n ' % self.fieldname
+ alines = self.oldvalue.splitlines()
+ blines = self.newvalue.splitlines()
+ diff = difflib.unified_diff(alines, blines, self.fieldname, self.fieldname, lineterm='')
+ out += '\n '.join(list(diff)[2:])
+ out += '\n --'
+ elif self.fieldname in img_monitor_files or '/image-files/' in self.path:
+ fieldname = self.fieldname
+ if '/image-files/' in self.path:
+ fieldname = os.path.join('/' + self.path.split('/image-files/')[1], self.fieldname)
+ out = 'Changes to %s:\n ' % fieldname
+ else:
+ if outer:
+ prefix = 'Changes to %s ' % self.path
+ out = '(%s):\n ' % self.fieldname
+ if self.filechanges:
+ out += '\n '.join(['%s' % i for i in self.filechanges])
+ else:
+ alines = self.oldvalue.splitlines()
+ blines = self.newvalue.splitlines()
+ diff = difflib.unified_diff(alines, blines, fieldname, fieldname, lineterm='')
+ out += '\n '.join(list(diff))
+ out += '\n --'
+ else:
+ out = '%s changed from "%s" to "%s"' % (self.fieldname, self.oldvalue, self.newvalue)
+
+ if self.related:
+ for chg in self.related:
+ if not outer and chg.fieldname in ['PE', 'PV', 'PR']:
+ continue
+ for line in chg._str_internal(False).splitlines():
+ out += '\n * %s' % line
+
+ return '%s%s' % (prefix, out)
+
+class FileChange:
+ changetype_add = 'A'
+ changetype_remove = 'R'
+ changetype_type = 'T'
+ changetype_perms = 'P'
+ changetype_ownergroup = 'O'
+ changetype_link = 'L'
+
+ def __init__(self, path, changetype, oldvalue = None, newvalue = None):
+ self.path = path
+ self.changetype = changetype
+ self.oldvalue = oldvalue
+ self.newvalue = newvalue
+
+ def _ftype_str(self, ftype):
+ if ftype == '-':
+ return 'file'
+ elif ftype == 'd':
+ return 'directory'
+ elif ftype == 'l':
+ return 'symlink'
+ elif ftype == 'c':
+ return 'char device'
+ elif ftype == 'b':
+ return 'block device'
+ elif ftype == 'p':
+ return 'fifo'
+ elif ftype == 's':
+ return 'socket'
+ else:
+ return 'unknown (%s)' % ftype
+
+ def __str__(self):
+ if self.changetype == self.changetype_add:
+ return '%s was added' % self.path
+ elif self.changetype == self.changetype_remove:
+ return '%s was removed' % self.path
+ elif self.changetype == self.changetype_type:
+ return '%s changed type from %s to %s' % (self.path, self._ftype_str(self.oldvalue), self._ftype_str(self.newvalue))
+ elif self.changetype == self.changetype_perms:
+ return '%s changed permissions from %s to %s' % (self.path, self.oldvalue, self.newvalue)
+ elif self.changetype == self.changetype_ownergroup:
+ return '%s changed owner/group from %s to %s' % (self.path, self.oldvalue, self.newvalue)
+ elif self.changetype == self.changetype_link:
+ return '%s changed symlink target from %s to %s' % (self.path, self.oldvalue, self.newvalue)
+ else:
+ return '%s changed (unknown)' % self.path
+
+
+def blob_to_dict(blob):
+ alines = blob.data_stream.read().splitlines()
+ adict = {}
+ for line in alines:
+ splitv = [i.strip() for i in line.split('=',1)]
+ if len(splitv) > 1:
+ adict[splitv[0]] = splitv[1]
+ return adict
+
+
+def file_list_to_dict(lines):
+ adict = {}
+ for line in lines:
+ # Leave the last few fields intact so we handle file names containing spaces
+ splitv = line.split(None,4)
+ # Grab the path and remove the leading .
+ path = splitv[4][1:].strip()
+ # Handle symlinks
+ if(' -> ' in path):
+ target = path.split(' -> ')[1]
+ path = path.split(' -> ')[0]
+ adict[path] = splitv[0:3] + [target]
+ else:
+ adict[path] = splitv[0:3]
+ return adict
+
+
+def compare_file_lists(alines, blines):
+ adict = file_list_to_dict(alines)
+ bdict = file_list_to_dict(blines)
+ filechanges = []
+ for path, splitv in adict.iteritems():
+ newsplitv = bdict.pop(path, None)
+ if newsplitv:
+ # Check type
+ oldvalue = splitv[0][0]
+ newvalue = newsplitv[0][0]
+ if oldvalue != newvalue:
+ filechanges.append(FileChange(path, FileChange.changetype_type, oldvalue, newvalue))
+ # Check permissions
+ oldvalue = splitv[0][1:]
+ newvalue = newsplitv[0][1:]
+ if oldvalue != newvalue:
+ filechanges.append(FileChange(path, FileChange.changetype_perms, oldvalue, newvalue))
+ # Check owner/group
+ oldvalue = '%s/%s' % (splitv[1], splitv[2])
+ newvalue = '%s/%s' % (newsplitv[1], newsplitv[2])
+ if oldvalue != newvalue:
+ filechanges.append(FileChange(path, FileChange.changetype_ownergroup, oldvalue, newvalue))
+ # Check symlink target
+ if newsplitv[0][0] == 'l':
+ if len(splitv) > 3:
+ oldvalue = splitv[3]
+ else:
+ oldvalue = None
+ newvalue = newsplitv[3]
+ if oldvalue != newvalue:
+ filechanges.append(FileChange(path, FileChange.changetype_link, oldvalue, newvalue))
+ else:
+ filechanges.append(FileChange(path, FileChange.changetype_remove))
+
+ # Whatever is left over has been added
+ for path in bdict:
+ filechanges.append(FileChange(path, FileChange.changetype_add))
+
+ return filechanges
+
+
+def compare_lists(alines, blines):
+ removed = list(set(alines) - set(blines))
+ added = list(set(blines) - set(alines))
+
+ filechanges = []
+ for pkg in removed:
+ filechanges.append(FileChange(pkg, FileChange.changetype_remove))
+ for pkg in added:
+ filechanges.append(FileChange(pkg, FileChange.changetype_add))
+
+ return filechanges
+
+
+def compare_pkg_lists(astr, bstr):
+ depvera = bb.utils.explode_dep_versions2(astr)
+ depverb = bb.utils.explode_dep_versions2(bstr)
+
+ # Strip out changes where the version has increased
+ remove = []
+ for k in depvera:
+ if k in depverb:
+ dva = depvera[k]
+ dvb = depverb[k]
+ if dva and dvb and len(dva) == len(dvb):
+ # Since length is the same, sort so that prefixes (e.g. >=) will line up
+ dva.sort()
+ dvb.sort()
+ removeit = True
+ for dvai, dvbi in zip(dva, dvb):
+ if dvai != dvbi:
+ aiprefix = dvai.split(' ')[0]
+ biprefix = dvbi.split(' ')[0]
+ if aiprefix == biprefix and aiprefix in ['>=', '=']:
+ if bb.utils.vercmp(bb.utils.split_version(dvai), bb.utils.split_version(dvbi)) > 0:
+ removeit = False
+ break
+ else:
+ removeit = False
+ break
+ if removeit:
+ remove.append(k)
+
+ for k in remove:
+ depvera.pop(k)
+ depverb.pop(k)
+
+ return (depvera, depverb)
+
+
+def compare_dict_blobs(path, ablob, bblob, report_all, report_ver):
+ adict = blob_to_dict(ablob)
+ bdict = blob_to_dict(bblob)
+
+ pkgname = os.path.basename(path)
+
+ defaultvals = {}
+ defaultvals['PKG'] = pkgname
+ defaultvals['PKGE'] = '0'
+
+ changes = []
+ keys = list(set(adict.keys()) | set(bdict.keys()) | set(defaultval_map.keys()))
+ for key in keys:
+ astr = adict.get(key, '')
+ bstr = bdict.get(key, '')
+ if key in ver_monitor_fields:
+ monitored = report_ver or astr or bstr
+ else:
+ monitored = key in monitor_fields
+ mapped_key = defaultval_map.get(key, '')
+ if mapped_key:
+ if not astr:
+ astr = '%s [default]' % adict.get(mapped_key, defaultvals.get(key, ''))
+ if not bstr:
+ bstr = '%s [default]' % bdict.get(mapped_key, defaultvals.get(key, ''))
+
+ if astr != bstr:
+ if (not report_all) and key in numeric_fields:
+ aval = int(astr or 0)
+ bval = int(bstr or 0)
+ if aval != 0:
+ percentchg = ((bval - aval) / float(aval)) * 100
+ else:
+ percentchg = 100
+ if abs(percentchg) < monitor_numeric_threshold:
+ continue
+ elif (not report_all) and key in list_fields:
+ if key == "FILELIST" and path.endswith("-dbg") and bstr.strip() != '':
+ continue
+ if key in ['RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RREPLACES', 'RCONFLICTS']:
+ (depvera, depverb) = compare_pkg_lists(astr, bstr)
+ if depvera == depverb:
+ continue
+ alist = astr.split()
+ alist.sort()
+ blist = bstr.split()
+ blist.sort()
+ # We don't care about the removal of self-dependencies
+ if pkgname in alist and not pkgname in blist:
+ alist.remove(pkgname)
+ if ' '.join(alist) == ' '.join(blist):
+ continue
+
+ chg = ChangeRecord(path, key, astr, bstr, monitored)
+ changes.append(chg)
+ return changes
+
+
+def process_changes(repopath, revision1, revision2='HEAD', report_all=False, report_ver=False):
+ repo = git.Repo(repopath)
+ assert repo.bare == False
+ commit = repo.commit(revision1)
+ diff = commit.diff(revision2)
+
+ changes = []
+ for d in diff.iter_change_type('M'):
+ path = os.path.dirname(d.a_blob.path)
+ if path.startswith('packages/'):
+ filename = os.path.basename(d.a_blob.path)
+ if filename == 'latest':
+ changes.extend(compare_dict_blobs(path, d.a_blob, d.b_blob, report_all, report_ver))
+ elif filename.startswith('latest.'):
+ chg = ChangeRecord(path, filename, d.a_blob.data_stream.read(), d.b_blob.data_stream.read(), True)
+ changes.append(chg)
+ elif path.startswith('images/'):
+ filename = os.path.basename(d.a_blob.path)
+ if filename in img_monitor_files:
+ if filename == 'files-in-image.txt':
+ alines = d.a_blob.data_stream.read().splitlines()
+ blines = d.b_blob.data_stream.read().splitlines()
+ filechanges = compare_file_lists(alines,blines)
+ if filechanges:
+ chg = ChangeRecord(path, filename, None, None, True)
+ chg.filechanges = filechanges
+ changes.append(chg)
+ elif filename == 'installed-package-names.txt':
+ alines = d.a_blob.data_stream.read().splitlines()
+ blines = d.b_blob.data_stream.read().splitlines()
+ filechanges = compare_lists(alines,blines)
+ if filechanges:
+ chg = ChangeRecord(path, filename, None, None, True)
+ chg.filechanges = filechanges
+ changes.append(chg)
+ else:
+ chg = ChangeRecord(path, filename, d.a_blob.data_stream.read(), d.b_blob.data_stream.read(), True)
+ changes.append(chg)
+ elif filename == 'image-info.txt':
+ changes.extend(compare_dict_blobs(path, d.a_blob, d.b_blob, report_all, report_ver))
+ elif '/image-files/' in path:
+ chg = ChangeRecord(path, filename, d.a_blob.data_stream.read(), d.b_blob.data_stream.read(), True)
+ changes.append(chg)
+
+ # Look for added preinst/postinst/prerm/postrm
+ # (without reporting newly added recipes)
+ addedpkgs = []
+ addedchanges = []
+ for d in diff.iter_change_type('A'):
+ path = os.path.dirname(d.b_blob.path)
+ if path.startswith('packages/'):
+ filename = os.path.basename(d.b_blob.path)
+ if filename == 'latest':
+ addedpkgs.append(path)
+ elif filename.startswith('latest.'):
+ chg = ChangeRecord(path, filename[7:], '', d.b_blob.data_stream.read(), True)
+ addedchanges.append(chg)
+ for chg in addedchanges:
+ found = False
+ for pkg in addedpkgs:
+ if chg.path.startswith(pkg):
+ found = True
+ break
+ if not found:
+ changes.append(chg)
+
+ # Look for cleared preinst/postinst/prerm/postrm
+ for d in diff.iter_change_type('D'):
+ path = os.path.dirname(d.a_blob.path)
+ if path.startswith('packages/'):
+ filename = os.path.basename(d.a_blob.path)
+ if filename != 'latest' and filename.startswith('latest.'):
+ chg = ChangeRecord(path, filename[7:], d.a_blob.data_stream.read(), '', True)
+ changes.append(chg)
+
+ # Link related changes
+ for chg in changes:
+ if chg.monitored:
+ for chg2 in changes:
+ # (Check dirname in the case of fields from recipe info files)
+ if chg.path == chg2.path or os.path.dirname(chg.path) == chg2.path:
+ if chg2.fieldname in related_fields.get(chg.fieldname, []):
+ chg.related.append(chg2)
+ elif chg.path == chg2.path and chg.path.startswith('packages/') and chg2.fieldname in ['PE', 'PV', 'PR']:
+ chg.related.append(chg2)
+
+ if report_all:
+ return changes
+ else:
+ return [chg for chg in changes if chg.monitored]
diff --git a/meta/lib/oe/cachedpath.py b/meta/lib/oe/cachedpath.py
new file mode 100644
index 0000000..0840cc4
--- /dev/null
+++ b/meta/lib/oe/cachedpath.py
@@ -0,0 +1,233 @@
+#
+# Based on standard python library functions but avoid
+# repeated stat calls. Its assumed the files will not change from under us
+# so we can cache stat calls.
+#
+
+import os
+import errno
+import stat as statmod
+
+class CachedPath(object):
+ def __init__(self):
+ self.statcache = {}
+ self.lstatcache = {}
+ self.normpathcache = {}
+ return
+
+ def updatecache(self, x):
+ x = self.normpath(x)
+ if x in self.statcache:
+ del self.statcache[x]
+ if x in self.lstatcache:
+ del self.lstatcache[x]
+
+ def normpath(self, path):
+ if path in self.normpathcache:
+ return self.normpathcache[path]
+ newpath = os.path.normpath(path)
+ self.normpathcache[path] = newpath
+ return newpath
+
+ def _callstat(self, path):
+ if path in self.statcache:
+ return self.statcache[path]
+ try:
+ st = os.stat(path)
+ self.statcache[path] = st
+ return st
+ except os.error:
+ self.statcache[path] = False
+ return False
+
+ # We might as well call lstat and then only
+ # call stat as well in the symbolic link case
+ # since this turns out to be much more optimal
+ # in real world usage of this cache
+ def callstat(self, path):
+ path = self.normpath(path)
+ self.calllstat(path)
+ return self.statcache[path]
+
+ def calllstat(self, path):
+ path = self.normpath(path)
+ if path in self.lstatcache:
+ return self.lstatcache[path]
+ #bb.error("LStatpath:" + path)
+ try:
+ lst = os.lstat(path)
+ self.lstatcache[path] = lst
+ if not statmod.S_ISLNK(lst.st_mode):
+ self.statcache[path] = lst
+ else:
+ self._callstat(path)
+ return lst
+ except (os.error, AttributeError):
+ self.lstatcache[path] = False
+ self.statcache[path] = False
+ return False
+
+ # This follows symbolic links, so both islink() and isdir() can be true
+ # for the same path ono systems that support symlinks
+ def isfile(self, path):
+ """Test whether a path is a regular file"""
+ st = self.callstat(path)
+ if not st:
+ return False
+ return statmod.S_ISREG(st.st_mode)
+
+ # Is a path a directory?
+ # This follows symbolic links, so both islink() and isdir()
+ # can be true for the same path on systems that support symlinks
+ def isdir(self, s):
+ """Return true if the pathname refers to an existing directory."""
+ st = self.callstat(s)
+ if not st:
+ return False
+ return statmod.S_ISDIR(st.st_mode)
+
+ def islink(self, path):
+ """Test whether a path is a symbolic link"""
+ st = self.calllstat(path)
+ if not st:
+ return False
+ return statmod.S_ISLNK(st.st_mode)
+
+ # Does a path exist?
+ # This is false for dangling symbolic links on systems that support them.
+ def exists(self, path):
+ """Test whether a path exists. Returns False for broken symbolic links"""
+ if self.callstat(path):
+ return True
+ return False
+
+ def lexists(self, path):
+ """Test whether a path exists. Returns True for broken symbolic links"""
+ if self.calllstat(path):
+ return True
+ return False
+
+ def stat(self, path):
+ return self.callstat(path)
+
+ def lstat(self, path):
+ return self.calllstat(path)
+
+ def walk(self, top, topdown=True, onerror=None, followlinks=False):
+ # Matches os.walk, not os.path.walk()
+
+ # We may not have read permission for top, in which case we can't
+ # get a list of the files the directory contains. os.path.walk
+ # always suppressed the exception then, rather than blow up for a
+ # minor reason when (say) a thousand readable directories are still
+ # left to visit. That logic is copied here.
+ try:
+ names = os.listdir(top)
+ except os.error as err:
+ if onerror is not None:
+ onerror(err)
+ return
+
+ dirs, nondirs = [], []
+ for name in names:
+ if self.isdir(os.path.join(top, name)):
+ dirs.append(name)
+ else:
+ nondirs.append(name)
+
+ if topdown:
+ yield top, dirs, nondirs
+ for name in dirs:
+ new_path = os.path.join(top, name)
+ if followlinks or not self.islink(new_path):
+ for x in self.walk(new_path, topdown, onerror, followlinks):
+ yield x
+ if not topdown:
+ yield top, dirs, nondirs
+
+ ## realpath() related functions
+ def __is_path_below(self, file, root):
+ return (file + os.path.sep).startswith(root)
+
+ def __realpath_rel(self, start, rel_path, root, loop_cnt, assume_dir):
+ """Calculates real path of symlink 'start' + 'rel_path' below
+ 'root'; no part of 'start' below 'root' must contain symlinks. """
+ have_dir = True
+
+ for d in rel_path.split(os.path.sep):
+ if not have_dir and not assume_dir:
+ raise OSError(errno.ENOENT, "no such directory %s" % start)
+
+ if d == os.path.pardir: # '..'
+ if len(start) >= len(root):
+ # do not follow '..' before root
+ start = os.path.dirname(start)
+ else:
+ # emit warning?
+ pass
+ else:
+ (start, have_dir) = self.__realpath(os.path.join(start, d),
+ root, loop_cnt, assume_dir)
+
+ assert(self.__is_path_below(start, root))
+
+ return start
+
+ def __realpath(self, file, root, loop_cnt, assume_dir):
+ while self.islink(file) and len(file) >= len(root):
+ if loop_cnt == 0:
+ raise OSError(errno.ELOOP, file)
+
+ loop_cnt -= 1
+ target = os.path.normpath(os.readlink(file))
+
+ if not os.path.isabs(target):
+ tdir = os.path.dirname(file)
+ assert(self.__is_path_below(tdir, root))
+ else:
+ tdir = root
+
+ file = self.__realpath_rel(tdir, target, root, loop_cnt, assume_dir)
+
+ try:
+ is_dir = self.isdir(file)
+ except:
+ is_dir = False
+
+ return (file, is_dir)
+
+ def realpath(self, file, root, use_physdir = True, loop_cnt = 100, assume_dir = False):
+ """ Returns the canonical path of 'file' with assuming a
+ toplevel 'root' directory. When 'use_physdir' is set, all
+ preceding path components of 'file' will be resolved first;
+ this flag should be set unless it is guaranteed that there is
+ no symlink in the path. When 'assume_dir' is not set, missing
+ path components will raise an ENOENT error"""
+
+ root = os.path.normpath(root)
+ file = os.path.normpath(file)
+
+ if not root.endswith(os.path.sep):
+ # letting root end with '/' makes some things easier
+ root = root + os.path.sep
+
+ if not self.__is_path_below(file, root):
+ raise OSError(errno.EINVAL, "file '%s' is not below root" % file)
+
+ try:
+ if use_physdir:
+ file = self.__realpath_rel(root, file[(len(root) - 1):], root, loop_cnt, assume_dir)
+ else:
+ file = self.__realpath(file, root, loop_cnt, assume_dir)[0]
+ except OSError as e:
+ if e.errno == errno.ELOOP:
+ # make ELOOP more readable; without catching it, there will
+ # be printed a backtrace with 100s of OSError exceptions
+ # else
+ raise OSError(errno.ELOOP,
+ "too much recursions while resolving '%s'; loop in '%s'" %
+ (file, e.strerror))
+
+ raise
+
+ return file
diff --git a/meta/lib/oe/classextend.py b/meta/lib/oe/classextend.py
new file mode 100644
index 0000000..5107ecd
--- /dev/null
+++ b/meta/lib/oe/classextend.py
@@ -0,0 +1,120 @@
+class ClassExtender(object):
+ def __init__(self, extname, d):
+ self.extname = extname
+ self.d = d
+ self.pkgs_mapping = []
+
+ def extend_name(self, name):
+ if name.startswith("kernel-") or name == "virtual/kernel":
+ return name
+ if name.startswith("rtld"):
+ return name
+ if name.endswith("-crosssdk"):
+ return name
+ if name.endswith("-" + self.extname):
+ name = name.replace("-" + self.extname, "")
+ if name.startswith("virtual/"):
+ subs = name.split("/", 1)[1]
+ if not subs.startswith(self.extname):
+ return "virtual/" + self.extname + "-" + subs
+ return name
+ if not name.startswith(self.extname):
+ return self.extname + "-" + name
+ return name
+
+ def map_variable(self, varname, setvar = True):
+ var = self.d.getVar(varname, True)
+ if not var:
+ return ""
+ var = var.split()
+ newvar = []
+ for v in var:
+ newvar.append(self.extend_name(v))
+ newdata = " ".join(newvar)
+ if setvar:
+ self.d.setVar(varname, newdata)
+ return newdata
+
+ def map_regexp_variable(self, varname, setvar = True):
+ var = self.d.getVar(varname, True)
+ if not var:
+ return ""
+ var = var.split()
+ newvar = []
+ for v in var:
+ if v.startswith("^" + self.extname):
+ newvar.append(v)
+ elif v.startswith("^"):
+ newvar.append("^" + self.extname + "-" + v[1:])
+ else:
+ newvar.append(self.extend_name(v))
+ newdata = " ".join(newvar)
+ if setvar:
+ self.d.setVar(varname, newdata)
+ return newdata
+
+ def map_depends(self, dep):
+ if dep.endswith(("-native", "-native-runtime")) or ('nativesdk-' in dep) or ('cross-canadian' in dep) or ('-crosssdk-' in dep):
+ return dep
+ else:
+ # Do not extend for that already have multilib prefix
+ var = self.d.getVar("MULTILIB_VARIANTS", True)
+ if var:
+ var = var.split()
+ for v in var:
+ if dep.startswith(v):
+ return dep
+ return self.extend_name(dep)
+
+ def map_depends_variable(self, varname, suffix = ""):
+ # We need to preserve EXTENDPKGV so it can be expanded correctly later
+ if suffix:
+ varname = varname + "_" + suffix
+ orig = self.d.getVar("EXTENDPKGV", False)
+ self.d.setVar("EXTENDPKGV", "EXTENDPKGV")
+ deps = self.d.getVar(varname, True)
+ if not deps:
+ self.d.setVar("EXTENDPKGV", orig)
+ return
+ deps = bb.utils.explode_dep_versions2(deps)
+ newdeps = {}
+ for dep in deps:
+ newdeps[self.map_depends(dep)] = deps[dep]
+
+ self.d.setVar(varname, bb.utils.join_deps(newdeps, False).replace("EXTENDPKGV", "${EXTENDPKGV}"))
+ self.d.setVar("EXTENDPKGV", orig)
+
+ def map_packagevars(self):
+ for pkg in (self.d.getVar("PACKAGES", True).split() + [""]):
+ self.map_depends_variable("RDEPENDS", pkg)
+ self.map_depends_variable("RRECOMMENDS", pkg)
+ self.map_depends_variable("RSUGGESTS", pkg)
+ self.map_depends_variable("RPROVIDES", pkg)
+ self.map_depends_variable("RREPLACES", pkg)
+ self.map_depends_variable("RCONFLICTS", pkg)
+ self.map_depends_variable("PKG", pkg)
+
+ def rename_packages(self):
+ for pkg in (self.d.getVar("PACKAGES", True) or "").split():
+ if pkg.startswith(self.extname):
+ self.pkgs_mapping.append([pkg.split(self.extname + "-")[1], pkg])
+ continue
+ self.pkgs_mapping.append([pkg, self.extend_name(pkg)])
+
+ self.d.setVar("PACKAGES", " ".join([row[1] for row in self.pkgs_mapping]))
+
+ def rename_package_variables(self, variables):
+ for pkg_mapping in self.pkgs_mapping:
+ for subs in variables:
+ self.d.renameVar("%s_%s" % (subs, pkg_mapping[0]), "%s_%s" % (subs, pkg_mapping[1]))
+
+class NativesdkClassExtender(ClassExtender):
+ def map_depends(self, dep):
+ if dep.startswith(self.extname):
+ return dep
+ if dep.endswith(("-gcc-initial", "-gcc", "-g++")):
+ return dep + "-crosssdk"
+ elif dep.endswith(("-native", "-native-runtime")) or ('nativesdk-' in dep) or ('-cross-' in dep) or ('-crosssdk-' in dep):
+ return dep
+ else:
+ return self.extend_name(dep)
diff --git a/meta/lib/oe/classutils.py b/meta/lib/oe/classutils.py
new file mode 100644
index 0000000..58188fd
--- /dev/null
+++ b/meta/lib/oe/classutils.py
@@ -0,0 +1,43 @@
+class ClassRegistry(type):
+ """Maintain a registry of classes, indexed by name.
+
+Note that this implementation requires that the names be unique, as it uses
+a dictionary to hold the classes by name.
+
+The name in the registry can be overridden via the 'name' attribute of the
+class, and the 'priority' attribute controls priority. The prioritized()
+method returns the registered classes in priority order.
+
+Subclasses of ClassRegistry may define an 'implemented' property to exert
+control over whether the class will be added to the registry (e.g. to keep
+abstract base classes out of the registry)."""
+ priority = 0
+ class __metaclass__(type):
+ """Give each ClassRegistry their own registry"""
+ def __init__(cls, name, bases, attrs):
+ cls.registry = {}
+ type.__init__(cls, name, bases, attrs)
+
+ def __init__(cls, name, bases, attrs):
+ super(ClassRegistry, cls).__init__(name, bases, attrs)
+ try:
+ if not cls.implemented:
+ return
+ except AttributeError:
+ pass
+
+ try:
+ cls.name
+ except AttributeError:
+ cls.name = name
+ cls.registry[cls.name] = cls
+
+ @classmethod
+ def prioritized(tcls):
+ return sorted(tcls.registry.values(),
+ key=lambda v: v.priority, reverse=True)
+
+ def unregister(cls):
+ for key in cls.registry.keys():
+ if cls.registry[key] is cls:
+ del cls.registry[key]
diff --git a/meta/lib/oe/copy_buildsystem.py b/meta/lib/oe/copy_buildsystem.py
new file mode 100644
index 0000000..979578c
--- /dev/null
+++ b/meta/lib/oe/copy_buildsystem.py
@@ -0,0 +1,101 @@
+# This class should provide easy access to the different aspects of the
+# buildsystem such as layers, bitbake location, etc.
+import stat
+import shutil
+
+def _smart_copy(src, dest):
+ # smart_copy will choose the correct function depending on whether the
+ # source is a file or a directory.
+ mode = os.stat(src).st_mode
+ if stat.S_ISDIR(mode):
+ shutil.copytree(src, dest, symlinks=True)
+ else:
+ shutil.copyfile(src, dest)
+ shutil.copymode(src, dest)
+
+class BuildSystem(object):
+ def __init__(self, d):
+ self.d = d
+ self.layerdirs = d.getVar('BBLAYERS', True).split()
+
+ def copy_bitbake_and_layers(self, destdir):
+ # Copy in all metadata layers + bitbake (as repositories)
+ layers_copied = []
+ bb.utils.mkdirhier(destdir)
+ layers = list(self.layerdirs)
+
+ corebase = self.d.getVar('COREBASE', True)
+ layers.append(corebase)
+
+ corebase_files = self.d.getVar('COREBASE_FILES', True).split()
+ corebase_files = [corebase + '/' +x for x in corebase_files]
+ # Make sure bitbake goes in
+ bitbake_dir = bb.__file__.rsplit('/', 3)[0]
+ corebase_files.append(bitbake_dir)
+
+ for layer in layers:
+ layerconf = os.path.join(layer, 'conf', 'layer.conf')
+ if os.path.exists(layerconf):
+ with open(layerconf, 'r') as f:
+ if f.readline().startswith("# ### workspace layer auto-generated by devtool ###"):
+ bb.warn("Skipping local workspace layer %s" % layer)
+ continue
+
+ # If the layer was already under corebase, leave it there
+ # since layers such as meta have issues when moved.
+ layerdestpath = destdir
+ if corebase == os.path.dirname(layer):
+ layerdestpath += '/' + os.path.basename(corebase)
+ layerdestpath += '/' + os.path.basename(layer)
+
+ layer_relative = os.path.relpath(layerdestpath,
+ destdir)
+ layers_copied.append(layer_relative)
+
+ # Treat corebase as special since it typically will contain
+ # build directories or other custom items.
+ if corebase == layer:
+ bb.utils.mkdirhier(layerdestpath)
+ for f in corebase_files:
+ f_basename = os.path.basename(f)
+ destname = os.path.join(layerdestpath, f_basename)
+ _smart_copy(f, destname)
+ else:
+ if os.path.exists(layerdestpath):
+ bb.note("Skipping layer %s, already handled" % layer)
+ else:
+ _smart_copy(layer, layerdestpath)
+
+ return layers_copied
+
+def generate_locked_sigs(sigfile, d):
+ bb.utils.mkdirhier(os.path.dirname(sigfile))
+ depd = d.getVar('BB_TASKDEPDATA', True)
+ tasks = ['%s.%s' % (v[2], v[1]) for v in depd.itervalues()]
+ bb.parse.siggen.dump_lockedsigs(sigfile, tasks)
+
+def prune_lockedsigs(allowed_tasks, excluded_targets, lockedsigs, pruned_output):
+ with open(lockedsigs, 'r') as infile:
+ bb.utils.mkdirhier(os.path.dirname(pruned_output))
+ with open(pruned_output, 'w') as f:
+ invalue = False
+ for line in infile:
+ if invalue:
+ if line.endswith('\\\n'):
+ splitval = line.strip().split(':')
+ if splitval[1] in allowed_tasks and not splitval[0] in excluded_targets:
+ f.write(line)
+ else:
+ f.write(line)
+ invalue = False
+ elif line.startswith('SIGGEN_LOCKEDSIGS'):
+ invalue = True
+ f.write(line)
+
+def create_locked_sstate_cache(lockedsigs, input_sstate_cache, output_sstate_cache, d, fixedlsbstring=""):
+ bb.note('Generating sstate-cache...')
+
+ bb.process.run("gen-lockedsig-cache %s %s %s" % (lockedsigs, input_sstate_cache, output_sstate_cache))
+ if fixedlsbstring:
+ os.rename(output_sstate_cache + '/' + d.getVar('NATIVELSBSTRING', True),
+ output_sstate_cache + '/' + fixedlsbstring)
diff --git a/meta/lib/oe/data.py b/meta/lib/oe/data.py
new file mode 100644
index 0000000..4cc0e02
--- /dev/null
+++ b/meta/lib/oe/data.py
@@ -0,0 +1,17 @@
+import oe.maketype
+
+def typed_value(key, d):
+ """Construct a value for the specified metadata variable, using its flags
+ to determine the type and parameters for construction."""
+ var_type = d.getVarFlag(key, 'type')
+ flags = d.getVarFlags(key)
+ if flags is not None:
+ flags = dict((flag, d.expand(value))
+ for flag, value in flags.iteritems())
+ else:
+ flags = {}
+
+ try:
+ return oe.maketype.create(d.getVar(key, True) or '', var_type, **flags)
+ except (TypeError, ValueError), exc:
+ bb.msg.fatal("Data", "%s: %s" % (key, str(exc)))
diff --git a/meta/lib/oe/distro_check.py b/meta/lib/oe/distro_check.py
new file mode 100644
index 0000000..8ed5b0e
--- /dev/null
+++ b/meta/lib/oe/distro_check.py
@@ -0,0 +1,383 @@
+def get_links_from_url(url):
+ "Return all the href links found on the web location"
+
+ import urllib, sgmllib
+
+ class LinksParser(sgmllib.SGMLParser):
+ def parse(self, s):
+ "Parse the given string 's'."
+ self.feed(s)
+ self.close()
+
+ def __init__(self, verbose=0):
+ "Initialise an object passing 'verbose' to the superclass."
+ sgmllib.SGMLParser.__init__(self, verbose)
+ self.hyperlinks = []
+
+ def start_a(self, attributes):
+ "Process a hyperlink and its 'attributes'."
+ for name, value in attributes:
+ if name == "href":
+ self.hyperlinks.append(value.strip('/'))
+
+ def get_hyperlinks(self):
+ "Return the list of hyperlinks."
+ return self.hyperlinks
+
+ sock = urllib.urlopen(url)
+ webpage = sock.read()
+ sock.close()
+
+ linksparser = LinksParser()
+ linksparser.parse(webpage)
+ return linksparser.get_hyperlinks()
+
+def find_latest_numeric_release(url):
+ "Find the latest listed numeric release on the given url"
+ max=0
+ maxstr=""
+ for link in get_links_from_url(url):
+ try:
+ release = float(link)
+ except:
+ release = 0
+ if release > max:
+ max = release
+ maxstr = link
+ return maxstr
+
+def is_src_rpm(name):
+ "Check if the link is pointing to a src.rpm file"
+ if name[-8:] == ".src.rpm":
+ return True
+ else:
+ return False
+
+def package_name_from_srpm(srpm):
+ "Strip out the package name from the src.rpm filename"
+ strings = srpm.split('-')
+ package_name = strings[0]
+ for i in range(1, len (strings) - 1):
+ str = strings[i]
+ if not str[0].isdigit():
+ package_name += '-' + str
+ return package_name
+
+def clean_package_list(package_list):
+ "Removes multiple entries of packages and sorts the list"
+ set = {}
+ map(set.__setitem__, package_list, [])
+ return set.keys()
+
+
+def get_latest_released_meego_source_package_list():
+ "Returns list of all the name os packages in the latest meego distro"
+
+ package_names = []
+ try:
+ f = open("/tmp/Meego-1.1", "r")
+ for line in f:
+ package_names.append(line[:-1] + ":" + "main") # Also strip the '\n' at the end
+ except IOError: pass
+ package_list=clean_package_list(package_names)
+ return "1.0", package_list
+
+def get_source_package_list_from_url(url, section):
+ "Return a sectioned list of package names from a URL list"
+
+ bb.note("Reading %s: %s" % (url, section))
+ links = get_links_from_url(url)
+ srpms = filter(is_src_rpm, links)
+ names_list = map(package_name_from_srpm, srpms)
+
+ new_pkgs = []
+ for pkgs in names_list:
+ new_pkgs.append(pkgs + ":" + section)
+
+ return new_pkgs
+
+def get_latest_released_fedora_source_package_list():
+ "Returns list of all the name os packages in the latest fedora distro"
+ latest = find_latest_numeric_release("http://archive.fedoraproject.org/pub/fedora/linux/releases/")
+
+ package_names = get_source_package_list_from_url("http://archive.fedoraproject.org/pub/fedora/linux/releases/%s/Fedora/source/SRPMS/" % latest, "main")
+
+# package_names += get_source_package_list_from_url("http://download.fedora.redhat.com/pub/fedora/linux/releases/%s/Everything/source/SPRMS/" % latest, "everything")
+ package_names += get_source_package_list_from_url("http://archive.fedoraproject.org/pub/fedora/linux/updates/%s/SRPMS/" % latest, "updates")
+
+ package_list=clean_package_list(package_names)
+
+ return latest, package_list
+
+def get_latest_released_opensuse_source_package_list():
+ "Returns list of all the name os packages in the latest opensuse distro"
+ latest = find_latest_numeric_release("http://download.opensuse.org/source/distribution/")
+
+ package_names = get_source_package_list_from_url("http://download.opensuse.org/source/distribution/%s/repo/oss/suse/src/" % latest, "main")
+ package_names += get_source_package_list_from_url("http://download.opensuse.org/update/%s/rpm/src/" % latest, "updates")
+
+ package_list=clean_package_list(package_names)
+ return latest, package_list
+
+def get_latest_released_mandriva_source_package_list():
+ "Returns list of all the name os packages in the latest mandriva distro"
+ latest = find_latest_numeric_release("http://distrib-coffee.ipsl.jussieu.fr/pub/linux/MandrivaLinux/official/")
+ package_names = get_source_package_list_from_url("http://distrib-coffee.ipsl.jussieu.fr/pub/linux/MandrivaLinux/official/%s/SRPMS/main/release/" % latest, "main")
+# package_names += get_source_package_list_from_url("http://distrib-coffee.ipsl.jussieu.fr/pub/linux/MandrivaLinux/official/%s/SRPMS/contrib/release/" % latest, "contrib")
+ package_names += get_source_package_list_from_url("http://distrib-coffee.ipsl.jussieu.fr/pub/linux/MandrivaLinux/official/%s/SRPMS/main/updates/" % latest, "updates")
+
+ package_list=clean_package_list(package_names)
+ return latest, package_list
+
+def find_latest_debian_release(url):
+ "Find the latest listed debian release on the given url"
+
+ releases = []
+ for link in get_links_from_url(url):
+ if link[:6] == "Debian":
+ if ';' not in link:
+ releases.append(link)
+ releases.sort()
+ try:
+ return releases.pop()[6:]
+ except:
+ return "_NotFound_"
+
+def get_debian_style_source_package_list(url, section):
+ "Return the list of package-names stored in the debian style Sources.gz file"
+ import urllib
+ sock = urllib.urlopen(url)
+ import tempfile
+ tmpfile = tempfile.NamedTemporaryFile(mode='wb', prefix='oecore.', suffix='.tmp', delete=False)
+ tmpfilename=tmpfile.name
+ tmpfile.write(sock.read())
+ sock.close()
+ tmpfile.close()
+ import gzip
+ bb.note("Reading %s: %s" % (url, section))
+
+ f = gzip.open(tmpfilename)
+ package_names = []
+ for line in f:
+ if line[:9] == "Package: ":
+ package_names.append(line[9:-1] + ":" + section) # Also strip the '\n' at the end
+ os.unlink(tmpfilename)
+
+ return package_names
+
+def get_latest_released_debian_source_package_list():
+ "Returns list of all the name os packages in the latest debian distro"
+ latest = find_latest_debian_release("http://ftp.debian.org/debian/dists/")
+ url = "http://ftp.debian.org/debian/dists/stable/main/source/Sources.gz"
+ package_names = get_debian_style_source_package_list(url, "main")
+# url = "http://ftp.debian.org/debian/dists/stable/contrib/source/Sources.gz"
+# package_names += get_debian_style_source_package_list(url, "contrib")
+ url = "http://ftp.debian.org/debian/dists/stable-proposed-updates/main/source/Sources.gz"
+ package_names += get_debian_style_source_package_list(url, "updates")
+ package_list=clean_package_list(package_names)
+ return latest, package_list
+
+def find_latest_ubuntu_release(url):
+ "Find the latest listed ubuntu release on the given url"
+ url += "?C=M;O=D" # Descending Sort by Last Modified
+ for link in get_links_from_url(url):
+ if link[-8:] == "-updates":
+ return link[:-8]
+ return "_NotFound_"
+
+def get_latest_released_ubuntu_source_package_list():
+ "Returns list of all the name os packages in the latest ubuntu distro"
+ latest = find_latest_ubuntu_release("http://archive.ubuntu.com/ubuntu/dists/")
+ url = "http://archive.ubuntu.com/ubuntu/dists/%s/main/source/Sources.gz" % latest
+ package_names = get_debian_style_source_package_list(url, "main")
+# url = "http://archive.ubuntu.com/ubuntu/dists/%s/multiverse/source/Sources.gz" % latest
+# package_names += get_debian_style_source_package_list(url, "multiverse")
+# url = "http://archive.ubuntu.com/ubuntu/dists/%s/universe/source/Sources.gz" % latest
+# package_names += get_debian_style_source_package_list(url, "universe")
+ url = "http://archive.ubuntu.com/ubuntu/dists/%s-updates/main/source/Sources.gz" % latest
+ package_names += get_debian_style_source_package_list(url, "updates")
+ package_list=clean_package_list(package_names)
+ return latest, package_list
+
+def create_distro_packages_list(distro_check_dir):
+ pkglst_dir = os.path.join(distro_check_dir, "package_lists")
+ if not os.path.isdir (pkglst_dir):
+ os.makedirs(pkglst_dir)
+ # first clear old stuff
+ for file in os.listdir(pkglst_dir):
+ os.unlink(os.path.join(pkglst_dir, file))
+
+ per_distro_functions = [
+ ["Debian", get_latest_released_debian_source_package_list],
+ ["Ubuntu", get_latest_released_ubuntu_source_package_list],
+ ["Fedora", get_latest_released_fedora_source_package_list],
+ ["OpenSuSE", get_latest_released_opensuse_source_package_list],
+ ["Mandriva", get_latest_released_mandriva_source_package_list],
+ ["Meego", get_latest_released_meego_source_package_list]
+ ]
+
+ from datetime import datetime
+ begin = datetime.now()
+ for distro in per_distro_functions:
+ name = distro[0]
+ release, package_list = distro[1]()
+ bb.note("Distro: %s, Latest Release: %s, # src packages: %d" % (name, release, len(package_list)))
+ package_list_file = os.path.join(pkglst_dir, name + "-" + release)
+ f = open(package_list_file, "w+b")
+ for pkg in package_list:
+ f.write(pkg + "\n")
+ f.close()
+ end = datetime.now()
+ delta = end - begin
+ bb.note("package_list generatiosn took this much time: %d seconds" % delta.seconds)
+
+def update_distro_data(distro_check_dir, datetime):
+ """
+ If distro packages list data is old then rebuild it.
+ The operations has to be protected by a lock so that
+ only one thread performes it at a time.
+ """
+ if not os.path.isdir (distro_check_dir):
+ try:
+ bb.note ("Making new directory: %s" % distro_check_dir)
+ os.makedirs (distro_check_dir)
+ except OSError:
+ raise Exception('Unable to create directory %s' % (distro_check_dir))
+
+
+ datetime_file = os.path.join(distro_check_dir, "build_datetime")
+ saved_datetime = "_invalid_"
+ import fcntl
+ try:
+ if not os.path.exists(datetime_file):
+ open(datetime_file, 'w+b').close() # touch the file so that the next open won't fail
+
+ f = open(datetime_file, "r+b")
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ saved_datetime = f.read()
+ if saved_datetime[0:8] != datetime[0:8]:
+ bb.note("The build datetime did not match: saved:%s current:%s" % (saved_datetime, datetime))
+ bb.note("Regenerating distro package lists")
+ create_distro_packages_list(distro_check_dir)
+ f.seek(0)
+ f.write(datetime)
+
+ except OSError:
+ raise Exception('Unable to read/write this file: %s' % (datetime_file))
+ finally:
+ fcntl.lockf(f, fcntl.LOCK_UN)
+ f.close()
+
+def compare_in_distro_packages_list(distro_check_dir, d):
+ if not os.path.isdir(distro_check_dir):
+ raise Exception("compare_in_distro_packages_list: invalid distro_check_dir passed")
+
+ localdata = bb.data.createCopy(d)
+ pkglst_dir = os.path.join(distro_check_dir, "package_lists")
+ matching_distros = []
+ pn = d.getVar('PN', True)
+ recipe_name = d.getVar('PN', True)
+ bb.note("Checking: %s" % pn)
+
+ trim_dict = dict({"-native":"-native", "-cross":"-cross", "-initial":"-initial"})
+
+ if pn.find("-native") != -1:
+ pnstripped = pn.split("-native")
+ localdata.setVar('OVERRIDES', "pn-" + pnstripped[0] + ":" + d.getVar('OVERRIDES', True))
+ bb.data.update_data(localdata)
+ recipe_name = pnstripped[0]
+
+ if pn.startswith("nativesdk-"):
+ pnstripped = pn.split("nativesdk-")
+ localdata.setVar('OVERRIDES', "pn-" + pnstripped[1] + ":" + d.getVar('OVERRIDES', True))
+ bb.data.update_data(localdata)
+ recipe_name = pnstripped[1]
+
+ if pn.find("-cross") != -1:
+ pnstripped = pn.split("-cross")
+ localdata.setVar('OVERRIDES', "pn-" + pnstripped[0] + ":" + d.getVar('OVERRIDES', True))
+ bb.data.update_data(localdata)
+ recipe_name = pnstripped[0]
+
+ if pn.find("-initial") != -1:
+ pnstripped = pn.split("-initial")
+ localdata.setVar('OVERRIDES', "pn-" + pnstripped[0] + ":" + d.getVar('OVERRIDES', True))
+ bb.data.update_data(localdata)
+ recipe_name = pnstripped[0]
+
+ bb.note("Recipe: %s" % recipe_name)
+ tmp = localdata.getVar('DISTRO_PN_ALIAS', True)
+
+ distro_exceptions = dict({"OE-Core":'OE-Core', "OpenedHand":'OpenedHand', "Intel":'Intel', "Upstream":'Upstream', "Windriver":'Windriver', "OSPDT":'OSPDT Approved', "Poky":'poky'})
+
+ if tmp:
+ list = tmp.split(' ')
+ for str in list:
+ if str and str.find("=") == -1 and distro_exceptions[str]:
+ matching_distros.append(str)
+
+ distro_pn_aliases = {}
+ if tmp:
+ list = tmp.split(' ')
+ for str in list:
+ if str.find("=") != -1:
+ (dist, pn_alias) = str.split('=')
+ distro_pn_aliases[dist.strip().lower()] = pn_alias.strip()
+
+ for file in os.listdir(pkglst_dir):
+ (distro, distro_release) = file.split("-")
+ f = open(os.path.join(pkglst_dir, file), "rb")
+ for line in f:
+ (pkg, section) = line.split(":")
+ if distro.lower() in distro_pn_aliases:
+ pn = distro_pn_aliases[distro.lower()]
+ else:
+ pn = recipe_name
+ if pn == pkg:
+ matching_distros.append(distro + "-" + section[:-1]) # strip the \n at the end
+ f.close()
+ break
+ f.close()
+
+
+ if tmp != None:
+ list = tmp.split(' ')
+ for item in list:
+ matching_distros.append(item)
+ bb.note("Matching: %s" % matching_distros)
+ return matching_distros
+
+def create_log_file(d, logname):
+ import subprocess
+ logpath = d.getVar('LOG_DIR', True)
+ bb.utils.mkdirhier(logpath)
+ logfn, logsuffix = os.path.splitext(logname)
+ logfile = os.path.join(logpath, "%s.%s%s" % (logfn, d.getVar('DATETIME', True), logsuffix))
+ if not os.path.exists(logfile):
+ slogfile = os.path.join(logpath, logname)
+ if os.path.exists(slogfile):
+ os.remove(slogfile)
+ subprocess.call("touch %s" % logfile, shell=True)
+ os.symlink(logfile, slogfile)
+ d.setVar('LOG_FILE', logfile)
+ return logfile
+
+
+def save_distro_check_result(result, datetime, result_file, d):
+ pn = d.getVar('PN', True)
+ logdir = d.getVar('LOG_DIR', True)
+ if not logdir:
+ bb.error("LOG_DIR variable is not defined, can't write the distro_check results")
+ return
+ if not os.path.isdir(logdir):
+ os.makedirs(logdir)
+ line = pn
+ for i in result:
+ line = line + "," + i
+ f = open(result_file, "a")
+ import fcntl
+ fcntl.lockf(f, fcntl.LOCK_EX)
+ f.seek(0, os.SEEK_END) # seek to the end of file
+ f.write(line + "\n")
+ fcntl.lockf(f, fcntl.LOCK_UN)
+ f.close()
diff --git a/meta/lib/oe/image.py b/meta/lib/oe/image.py
new file mode 100644
index 0000000..2361955
--- /dev/null
+++ b/meta/lib/oe/image.py
@@ -0,0 +1,386 @@
+from oe.utils import execute_pre_post_process
+import os
+import subprocess
+import multiprocessing
+
+
+def generate_image(arg):
+ (type, subimages, create_img_cmd) = arg
+
+ bb.note("Running image creation script for %s: %s ..." %
+ (type, create_img_cmd))
+
+ try:
+ output = subprocess.check_output(create_img_cmd,
+ stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ return("Error: The image creation script '%s' returned %d:\n%s" %
+ (e.cmd, e.returncode, e.output))
+
+ bb.note("Script output:\n%s" % output)
+
+ return None
+
+
+"""
+This class will help compute IMAGE_FSTYPE dependencies and group them in batches
+that can be executed in parallel.
+
+The next example is for illustration purposes, highly unlikely to happen in real life.
+It's just one of the test cases I used to test the algorithm:
+
+For:
+IMAGE_FSTYPES = "i1 i2 i3 i4 i5"
+IMAGE_TYPEDEP_i4 = "i2"
+IMAGE_TYPEDEP_i5 = "i6 i4"
+IMAGE_TYPEDEP_i6 = "i7"
+IMAGE_TYPEDEP_i7 = "i2"
+
+We get the following list of batches that can be executed in parallel, having the
+dependencies satisfied:
+
+[['i1', 'i3', 'i2'], ['i4', 'i7'], ['i6'], ['i5']]
+"""
+class ImageDepGraph(object):
+ def __init__(self, d):
+ self.d = d
+ self.graph = dict()
+ self.deps_array = dict()
+
+ def _construct_dep_graph(self, image_fstypes):
+ graph = dict()
+
+ def add_node(node):
+ base_type = self._image_base_type(node)
+ deps = (self.d.getVar('IMAGE_TYPEDEP_' + node, True) or "")
+ base_deps = (self.d.getVar('IMAGE_TYPEDEP_' + base_type, True) or "")
+ if deps != "" or base_deps != "":
+ graph[node] = deps
+
+ for dep in deps.split() + base_deps.split():
+ if not dep in graph:
+ add_node(dep)
+ else:
+ graph[node] = ""
+
+ for fstype in image_fstypes:
+ add_node(fstype)
+
+ return graph
+
+ def _clean_graph(self):
+ # Live and VMDK/VDI images will be processed via inheriting
+ # bbclass and does not get processed here. Remove them from the fstypes
+ # graph. Their dependencies are already added, so no worries here.
+ remove_list = (self.d.getVar('IMAGE_TYPES_MASKED', True) or "").split()
+
+ for item in remove_list:
+ self.graph.pop(item, None)
+
+ def _image_base_type(self, type):
+ ctypes = self.d.getVar('COMPRESSIONTYPES', True).split()
+ if type in ["vmdk", "vdi", "qcow2", "live", "iso", "hddimg"]:
+ type = "ext4"
+ basetype = type
+ for ctype in ctypes:
+ if type.endswith("." + ctype):
+ basetype = type[:-len("." + ctype)]
+ break
+
+ return basetype
+
+ def _compute_dependencies(self):
+ """
+ returns dict object of nodes with [no_of_depends_on, no_of_depended_by]
+ for each node
+ """
+ deps_array = dict()
+ for node in self.graph:
+ deps_array[node] = [0, 0]
+
+ for node in self.graph:
+ deps = self.graph[node].split()
+ deps_array[node][0] += len(deps)
+ for dep in deps:
+ deps_array[dep][1] += 1
+
+ return deps_array
+
+ def _sort_graph(self):
+ sorted_list = []
+ group = []
+ for node in self.graph:
+ if node not in self.deps_array:
+ continue
+
+ depends_on = self.deps_array[node][0]
+
+ if depends_on == 0:
+ group.append(node)
+
+ if len(group) == 0 and len(self.deps_array) != 0:
+ bb.fatal("possible fstype circular dependency...")
+
+ sorted_list.append(group)
+
+ # remove added nodes from deps_array
+ for item in group:
+ for node in self.graph:
+ if item in self.graph[node].split():
+ self.deps_array[node][0] -= 1
+
+ self.deps_array.pop(item, None)
+
+ if len(self.deps_array):
+ # recursive call, to find the next group
+ sorted_list += self._sort_graph()
+
+ return sorted_list
+
+ def group_fstypes(self, image_fstypes):
+ self.graph = self._construct_dep_graph(image_fstypes)
+
+ self._clean_graph()
+
+ self.deps_array = self._compute_dependencies()
+
+ alltypes = [node for node in self.graph]
+
+ return (alltypes, self._sort_graph())
+
+
+class Image(ImageDepGraph):
+ def __init__(self, d):
+ self.d = d
+
+ super(Image, self).__init__(d)
+
+ def _get_rootfs_size(self):
+ """compute the rootfs size"""
+ rootfs_alignment = int(self.d.getVar('IMAGE_ROOTFS_ALIGNMENT', True))
+ overhead_factor = float(self.d.getVar('IMAGE_OVERHEAD_FACTOR', True))
+ rootfs_req_size = int(self.d.getVar('IMAGE_ROOTFS_SIZE', True))
+ rootfs_extra_space = eval(self.d.getVar('IMAGE_ROOTFS_EXTRA_SPACE', True))
+ rootfs_maxsize = self.d.getVar('IMAGE_ROOTFS_MAXSIZE', True)
+
+ output = subprocess.check_output(['du', '-ks',
+ self.d.getVar('IMAGE_ROOTFS', True)])
+ size_kb = int(output.split()[0])
+ base_size = size_kb * overhead_factor
+ base_size = (base_size, rootfs_req_size)[base_size < rootfs_req_size] + \
+ rootfs_extra_space
+
+ if base_size != int(base_size):
+ base_size = int(base_size + 1)
+
+ base_size += rootfs_alignment - 1
+ base_size -= base_size % rootfs_alignment
+
+ # Check the rootfs size against IMAGE_ROOTFS_MAXSIZE (if set)
+ if rootfs_maxsize:
+ rootfs_maxsize_int = int(rootfs_maxsize)
+ if base_size > rootfs_maxsize_int:
+ bb.fatal("The rootfs size %d(K) overrides the max size %d(K)" % \
+ (base_size, rootfs_maxsize_int))
+
+ return base_size
+
+ def _create_symlinks(self, subimages):
+ """create symlinks to the newly created image"""
+ deploy_dir = self.d.getVar('DEPLOY_DIR_IMAGE', True)
+ img_name = self.d.getVar('IMAGE_NAME', True)
+ link_name = self.d.getVar('IMAGE_LINK_NAME', True)
+ manifest_name = self.d.getVar('IMAGE_MANIFEST', True)
+
+ os.chdir(deploy_dir)
+
+ if link_name:
+ for type in subimages:
+ if os.path.exists(img_name + ".rootfs." + type):
+ dst = link_name + "." + type
+ src = img_name + ".rootfs." + type
+ bb.note("Creating symlink: %s -> %s" % (dst, src))
+ os.symlink(src, dst)
+
+ if manifest_name is not None and \
+ os.path.exists(manifest_name) and \
+ not os.path.exists(link_name + ".manifest"):
+ os.symlink(os.path.basename(manifest_name),
+ link_name + ".manifest")
+
+ def _remove_old_symlinks(self):
+ """remove the symlinks to old binaries"""
+
+ if self.d.getVar('IMAGE_LINK_NAME', True):
+ deploy_dir = self.d.getVar('DEPLOY_DIR_IMAGE', True)
+ for img in os.listdir(deploy_dir):
+ if img.find(self.d.getVar('IMAGE_LINK_NAME', True)) == 0:
+ img = os.path.join(deploy_dir, img)
+ if os.path.islink(img):
+ if self.d.getVar('RM_OLD_IMAGE', True) == "1" and \
+ os.path.exists(os.path.realpath(img)):
+ os.remove(os.path.realpath(img))
+
+ os.remove(img)
+
+ """
+ This function will just filter out the compressed image types from the
+ fstype groups returning a (filtered_fstype_groups, cimages) tuple.
+ """
+ def _filter_out_commpressed(self, fstype_groups):
+ ctypes = self.d.getVar('COMPRESSIONTYPES', True).split()
+ cimages = {}
+
+ filtered_groups = []
+ for group in fstype_groups:
+ filtered_group = []
+ for type in group:
+ basetype = None
+ for ctype in ctypes:
+ if type.endswith("." + ctype):
+ basetype = type[:-len("." + ctype)]
+ if basetype not in filtered_group:
+ filtered_group.append(basetype)
+ if basetype not in cimages:
+ cimages[basetype] = []
+ if ctype not in cimages[basetype]:
+ cimages[basetype].append(ctype)
+ break
+ if not basetype and type not in filtered_group:
+ filtered_group.append(type)
+
+ filtered_groups.append(filtered_group)
+
+ return (filtered_groups, cimages)
+
+ def _get_image_types(self):
+ """returns a (types, cimages) tuple"""
+
+ alltypes, fstype_groups = self.group_fstypes(self.d.getVar('IMAGE_FSTYPES', True).split())
+
+ filtered_groups, cimages = self._filter_out_commpressed(fstype_groups)
+
+ return (alltypes, filtered_groups, cimages)
+
+ def _write_script(self, type, cmds):
+ tempdir = self.d.getVar('T', True)
+ script_name = os.path.join(tempdir, "create_image." + type)
+ rootfs_size = self._get_rootfs_size()
+
+ self.d.setVar('img_creation_func', '\n'.join(cmds))
+ self.d.setVarFlag('img_creation_func', 'func', 1)
+ self.d.setVarFlag('img_creation_func', 'fakeroot', 1)
+ self.d.setVar('ROOTFS_SIZE', str(rootfs_size))
+
+ with open(script_name, "w+") as script:
+ script.write("%s" % bb.build.shell_trap_code())
+ script.write("export ROOTFS_SIZE=%d\n" % rootfs_size)
+ bb.data.emit_func('img_creation_func', script, self.d)
+ script.write("img_creation_func\n")
+
+ os.chmod(script_name, 0775)
+
+ return script_name
+
+ def _get_imagecmds(self):
+ old_overrides = self.d.getVar('OVERRIDES', 0)
+
+ alltypes, fstype_groups, cimages = self._get_image_types()
+
+ image_cmd_groups = []
+
+ bb.note("The image creation groups are: %s" % str(fstype_groups))
+ for fstype_group in fstype_groups:
+ image_cmds = []
+ for type in fstype_group:
+ cmds = []
+ subimages = []
+
+ localdata = bb.data.createCopy(self.d)
+ localdata.setVar('OVERRIDES', '%s:%s' % (type, old_overrides))
+ bb.data.update_data(localdata)
+ localdata.setVar('type', type)
+
+ image_cmd = localdata.getVar("IMAGE_CMD", True)
+ if image_cmd:
+ cmds.append("\t" + image_cmd)
+ else:
+ bb.fatal("No IMAGE_CMD defined for IMAGE_FSTYPES entry '%s' - possibly invalid type name or missing support class" % type)
+ cmds.append(localdata.expand("\tcd ${DEPLOY_DIR_IMAGE}"))
+
+ if type in cimages:
+ for ctype in cimages[type]:
+ cmds.append("\t" + localdata.getVar("COMPRESS_CMD_" + ctype, True))
+ subimages.append(type + "." + ctype)
+
+ if type not in alltypes:
+ cmds.append(localdata.expand("\trm ${IMAGE_NAME}.rootfs.${type}"))
+ else:
+ subimages.append(type)
+
+ script_name = self._write_script(type, cmds)
+
+ image_cmds.append((type, subimages, script_name))
+
+ image_cmd_groups.append(image_cmds)
+
+ return image_cmd_groups
+
+ def _write_wic_env(self):
+ """
+ Write environment variables used by wic
+ to tmp/sysroots/<machine>/imgdata/<image>.env
+ """
+ stdir = self.d.getVar('STAGING_DIR_TARGET', True)
+ outdir = os.path.join(stdir, 'imgdata')
+ if not os.path.exists(outdir):
+ os.makedirs(outdir)
+ basename = self.d.getVar('IMAGE_BASENAME', True)
+ with open(os.path.join(outdir, basename) + '.env', 'w') as envf:
+ for var in self.d.getVar('WICVARS', True).split():
+ value = self.d.getVar(var, True)
+ if value:
+ envf.write('%s="%s"\n' % (var, value.strip()))
+
+ def create(self):
+ bb.note("###### Generate images #######")
+ pre_process_cmds = self.d.getVar("IMAGE_PREPROCESS_COMMAND", True)
+ post_process_cmds = self.d.getVar("IMAGE_POSTPROCESS_COMMAND", True)
+
+ execute_pre_post_process(self.d, pre_process_cmds)
+
+ self._remove_old_symlinks()
+
+ image_cmd_groups = self._get_imagecmds()
+
+ self._write_wic_env()
+
+ for image_cmds in image_cmd_groups:
+ # create the images in parallel
+ nproc = multiprocessing.cpu_count()
+ pool = bb.utils.multiprocessingpool(nproc)
+ results = list(pool.imap(generate_image, image_cmds))
+ pool.close()
+ pool.join()
+
+ for result in results:
+ if result is not None:
+ bb.fatal(result)
+
+ for image_type, subimages, script in image_cmds:
+ bb.note("Creating symlinks for %s image ..." % image_type)
+ self._create_symlinks(subimages)
+
+ execute_pre_post_process(self.d, post_process_cmds)
+
+
+def create_image(d):
+ Image(d).create()
+
+if __name__ == "__main__":
+ """
+ Image creation can be called independent from bitbake environment.
+ """
+ """
+ TBD
+ """
diff --git a/meta/lib/oe/license.py b/meta/lib/oe/license.py
new file mode 100644
index 0000000..f0f661c
--- /dev/null
+++ b/meta/lib/oe/license.py
@@ -0,0 +1,217 @@
+# vi:sts=4:sw=4:et
+"""Code for parsing OpenEmbedded license strings"""
+
+import ast
+import re
+from fnmatch import fnmatchcase as fnmatch
+
+def license_ok(license, dont_want_licenses):
+ """ Return False if License exist in dont_want_licenses else True """
+ for dwl in dont_want_licenses:
+ # If you want to exclude license named generically 'X', we
+ # surely want to exclude 'X+' as well. In consequence, we
+ # will exclude a trailing '+' character from LICENSE in
+ # case INCOMPATIBLE_LICENSE is not a 'X+' license.
+ lic = license
+ if not re.search('\+$', dwl):
+ lic = re.sub('\+', '', license)
+ if fnmatch(lic, dwl):
+ return False
+ return True
+
+class LicenseError(Exception):
+ pass
+
+class LicenseSyntaxError(LicenseError):
+ def __init__(self, licensestr, exc):
+ self.licensestr = licensestr
+ self.exc = exc
+ LicenseError.__init__(self)
+
+ def __str__(self):
+ return "error in '%s': %s" % (self.licensestr, self.exc)
+
+class InvalidLicense(LicenseError):
+ def __init__(self, license):
+ self.license = license
+ LicenseError.__init__(self)
+
+ def __str__(self):
+ return "invalid characters in license '%s'" % self.license
+
+license_operator_chars = '&|() '
+license_operator = re.compile('([' + license_operator_chars + '])')
+license_pattern = re.compile('[a-zA-Z0-9.+_\-]+$')
+
+class LicenseVisitor(ast.NodeVisitor):
+ """Get elements based on OpenEmbedded license strings"""
+ def get_elements(self, licensestr):
+ new_elements = []
+ elements = filter(lambda x: x.strip(), license_operator.split(licensestr))
+ for pos, element in enumerate(elements):
+ if license_pattern.match(element):
+ if pos > 0 and license_pattern.match(elements[pos-1]):
+ new_elements.append('&')
+ element = '"' + element + '"'
+ elif not license_operator.match(element):
+ raise InvalidLicense(element)
+ new_elements.append(element)
+
+ return new_elements
+
+ """Syntax tree visitor which can accept elements previously generated with
+ OpenEmbedded license string"""
+ def visit_elements(self, elements):
+ self.visit(ast.parse(' '.join(elements)))
+
+ """Syntax tree visitor which can accept OpenEmbedded license strings"""
+ def visit_string(self, licensestr):
+ self.visit_elements(self.get_elements(licensestr))
+
+class FlattenVisitor(LicenseVisitor):
+ """Flatten a license tree (parsed from a string) by selecting one of each
+ set of OR options, in the way the user specifies"""
+ def __init__(self, choose_licenses):
+ self.choose_licenses = choose_licenses
+ self.licenses = []
+ LicenseVisitor.__init__(self)
+
+ def visit_Str(self, node):
+ self.licenses.append(node.s)
+
+ def visit_BinOp(self, node):
+ if isinstance(node.op, ast.BitOr):
+ left = FlattenVisitor(self.choose_licenses)
+ left.visit(node.left)
+
+ right = FlattenVisitor(self.choose_licenses)
+ right.visit(node.right)
+
+ selected = self.choose_licenses(left.licenses, right.licenses)
+ self.licenses.extend(selected)
+ else:
+ self.generic_visit(node)
+
+def flattened_licenses(licensestr, choose_licenses):
+ """Given a license string and choose_licenses function, return a flat list of licenses"""
+ flatten = FlattenVisitor(choose_licenses)
+ try:
+ flatten.visit_string(licensestr)
+ except SyntaxError as exc:
+ raise LicenseSyntaxError(licensestr, exc)
+ return flatten.licenses
+
+def is_included(licensestr, whitelist=None, blacklist=None):
+ """Given a license string and whitelist and blacklist, determine if the
+ license string matches the whitelist and does not match the blacklist.
+
+ Returns a tuple holding the boolean state and a list of the applicable
+ licenses which were excluded (or None, if the state is True)
+ """
+
+ def include_license(license):
+ return any(fnmatch(license, pattern) for pattern in whitelist)
+
+ def exclude_license(license):
+ return any(fnmatch(license, pattern) for pattern in blacklist)
+
+ def choose_licenses(alpha, beta):
+ """Select the option in an OR which is the 'best' (has the most
+ included licenses)."""
+ alpha_weight = len(filter(include_license, alpha))
+ beta_weight = len(filter(include_license, beta))
+ if alpha_weight > beta_weight:
+ return alpha
+ else:
+ return beta
+
+ if not whitelist:
+ whitelist = ['*']
+
+ if not blacklist:
+ blacklist = []
+
+ licenses = flattened_licenses(licensestr, choose_licenses)
+ excluded = filter(lambda lic: exclude_license(lic), licenses)
+ included = filter(lambda lic: include_license(lic), licenses)
+ if excluded:
+ return False, excluded
+ else:
+ return True, included
+
+class ManifestVisitor(LicenseVisitor):
+ """Walk license tree (parsed from a string) removing the incompatible
+ licenses specified"""
+ def __init__(self, dont_want_licenses, canonical_license, d):
+ self._dont_want_licenses = dont_want_licenses
+ self._canonical_license = canonical_license
+ self._d = d
+ self._operators = []
+
+ self.licenses = []
+ self.licensestr = ''
+
+ LicenseVisitor.__init__(self)
+
+ def visit(self, node):
+ if isinstance(node, ast.Str):
+ lic = node.s
+
+ if license_ok(self._canonical_license(self._d, lic),
+ self._dont_want_licenses) == True:
+ if self._operators:
+ ops = []
+ for op in self._operators:
+ if op == '[':
+ ops.append(op)
+ elif op == ']':
+ ops.append(op)
+ else:
+ if not ops:
+ ops.append(op)
+ elif ops[-1] in ['[', ']']:
+ ops.append(op)
+ else:
+ ops[-1] = op
+
+ for op in ops:
+ if op == '[' or op == ']':
+ self.licensestr += op
+ elif self.licenses:
+ self.licensestr += ' ' + op + ' '
+
+ self._operators = []
+
+ self.licensestr += lic
+ self.licenses.append(lic)
+ elif isinstance(node, ast.BitAnd):
+ self._operators.append("&")
+ elif isinstance(node, ast.BitOr):
+ self._operators.append("|")
+ elif isinstance(node, ast.List):
+ self._operators.append("[")
+ elif isinstance(node, ast.Load):
+ self.licensestr += "]"
+
+ self.generic_visit(node)
+
+def manifest_licenses(licensestr, dont_want_licenses, canonical_license, d):
+ """Given a license string and dont_want_licenses list,
+ return license string filtered and a list of licenses"""
+ manifest = ManifestVisitor(dont_want_licenses, canonical_license, d)
+
+ try:
+ elements = manifest.get_elements(licensestr)
+
+ # Replace '()' to '[]' for handle in ast as List and Load types.
+ elements = ['[' if e == '(' else e for e in elements]
+ elements = [']' if e == ')' else e for e in elements]
+
+ manifest.visit_elements(elements)
+ except SyntaxError as exc:
+ raise LicenseSyntaxError(licensestr, exc)
+
+ # Replace '[]' to '()' for output correct license.
+ manifest.licensestr = manifest.licensestr.replace('[', '(').replace(']', ')')
+
+ return (manifest.licensestr, manifest.licenses)
diff --git a/meta/lib/oe/lsb.py b/meta/lib/oe/lsb.py
new file mode 100644
index 0000000..ddfe71b
--- /dev/null
+++ b/meta/lib/oe/lsb.py
@@ -0,0 +1,83 @@
+def release_dict():
+ """Return the output of lsb_release -ir as a dictionary"""
+ from subprocess import PIPE
+
+ try:
+ output, err = bb.process.run(['lsb_release', '-ir'], stderr=PIPE)
+ except bb.process.CmdError as exc:
+ return None
+
+ data = {}
+ for line in output.splitlines():
+ if line.startswith("-e"): line = line[3:]
+ try:
+ key, value = line.split(":\t", 1)
+ except ValueError:
+ continue
+ else:
+ data[key] = value
+ return data
+
+def release_dict_file():
+ """ Try to gather LSB release information manually when lsb_release tool is unavailable """
+ data = None
+ try:
+ if os.path.exists('/etc/lsb-release'):
+ data = {}
+ with open('/etc/lsb-release') as f:
+ for line in f:
+ key, value = line.split("=", 1)
+ data[key] = value.strip()
+ elif os.path.exists('/etc/redhat-release'):
+ data = {}
+ with open('/etc/redhat-release') as f:
+ distro = f.readline().strip()
+ import re
+ match = re.match(r'(.*) release (.*) \((.*)\)', distro)
+ if match:
+ data['DISTRIB_ID'] = match.group(1)
+ data['DISTRIB_RELEASE'] = match.group(2)
+ elif os.path.exists('/etc/os-release'):
+ data = {}
+ with open('/etc/os-release') as f:
+ for line in f:
+ if line.startswith('NAME='):
+ data['DISTRIB_ID'] = line[5:].rstrip().strip('"')
+ if line.startswith('VERSION_ID='):
+ data['DISTRIB_RELEASE'] = line[11:].rstrip().strip('"')
+ elif os.path.exists('/etc/SuSE-release'):
+ data = {}
+ data['DISTRIB_ID'] = 'SUSE LINUX'
+ with open('/etc/SuSE-release') as f:
+ for line in f:
+ if line.startswith('VERSION = '):
+ data['DISTRIB_RELEASE'] = line[10:].rstrip()
+ break
+
+ except IOError:
+ return None
+ return data
+
+def distro_identifier(adjust_hook=None):
+ """Return a distro identifier string based upon lsb_release -ri,
+ with optional adjustment via a hook"""
+
+ lsb_data = release_dict()
+ if lsb_data:
+ distro_id, release = lsb_data['Distributor ID'], lsb_data['Release']
+ else:
+ lsb_data_file = release_dict_file()
+ if lsb_data_file:
+ distro_id, release = lsb_data_file['DISTRIB_ID'], lsb_data_file.get('DISTRIB_RELEASE', None)
+ else:
+ distro_id, release = None, None
+
+ if adjust_hook:
+ distro_id, release = adjust_hook(distro_id, release)
+ if not distro_id:
+ return "Unknown"
+ if release:
+ id_str = '{0}-{1}'.format(distro_id, release)
+ else:
+ id_str = distro_id
+ return id_str.replace(' ','-').replace('/','-')
diff --git a/meta/lib/oe/maketype.py b/meta/lib/oe/maketype.py
new file mode 100644
index 0000000..139f333
--- /dev/null
+++ b/meta/lib/oe/maketype.py
@@ -0,0 +1,99 @@
+"""OpenEmbedded variable typing support
+
+Types are defined in the metadata by name, using the 'type' flag on a
+variable. Other flags may be utilized in the construction of the types. See
+the arguments of the type's factory for details.
+"""
+
+import inspect
+import types
+
+available_types = {}
+
+class MissingFlag(TypeError):
+ """A particular flag is required to construct the type, but has not been
+ provided."""
+ def __init__(self, flag, type):
+ self.flag = flag
+ self.type = type
+ TypeError.__init__(self)
+
+ def __str__(self):
+ return "Type '%s' requires flag '%s'" % (self.type, self.flag)
+
+def factory(var_type):
+ """Return the factory for a specified type."""
+ if var_type is None:
+ raise TypeError("No type specified. Valid types: %s" %
+ ', '.join(available_types))
+ try:
+ return available_types[var_type]
+ except KeyError:
+ raise TypeError("Invalid type '%s':\n Valid types: %s" %
+ (var_type, ', '.join(available_types)))
+
+def create(value, var_type, **flags):
+ """Create an object of the specified type, given the specified flags and
+ string value."""
+ obj = factory(var_type)
+ objflags = {}
+ for flag in obj.flags:
+ if flag not in flags:
+ if flag not in obj.optflags:
+ raise MissingFlag(flag, var_type)
+ else:
+ objflags[flag] = flags[flag]
+
+ return obj(value, **objflags)
+
+def get_callable_args(obj):
+ """Grab all but the first argument of the specified callable, returning
+ the list, as well as a list of which of the arguments have default
+ values."""
+ if type(obj) is type:
+ obj = obj.__init__
+
+ args, varargs, keywords, defaults = inspect.getargspec(obj)
+ flaglist = []
+ if args:
+ if len(args) > 1 and args[0] == 'self':
+ args = args[1:]
+ flaglist.extend(args)
+
+ optional = set()
+ if defaults:
+ optional |= set(flaglist[-len(defaults):])
+ return flaglist, optional
+
+def factory_setup(name, obj):
+ """Prepare a factory for use."""
+ args, optional = get_callable_args(obj)
+ extra_args = args[1:]
+ if extra_args:
+ obj.flags, optional = extra_args, optional
+ obj.optflags = set(optional)
+ else:
+ obj.flags = obj.optflags = ()
+
+ if not hasattr(obj, 'name'):
+ obj.name = name
+
+def register(name, factory):
+ """Register a type, given its name and a factory callable.
+
+ Determines the required and optional flags from the factory's
+ arguments."""
+ factory_setup(name, factory)
+ available_types[factory.name] = factory
+
+
+# Register all our included types
+for name in dir(types):
+ if name.startswith('_'):
+ continue
+
+ obj = getattr(types, name)
+ if not callable(obj):
+ continue
+
+ register(name, obj)
diff --git a/meta/lib/oe/manifest.py b/meta/lib/oe/manifest.py
new file mode 100644
index 0000000..42832f1
--- /dev/null
+++ b/meta/lib/oe/manifest.py
@@ -0,0 +1,345 @@
+from abc import ABCMeta, abstractmethod
+import os
+import re
+import bb
+
+
+class Manifest(object):
+ """
+ This is an abstract class. Do not instantiate this directly.
+ """
+ __metaclass__ = ABCMeta
+
+ PKG_TYPE_MUST_INSTALL = "mip"
+ PKG_TYPE_MULTILIB = "mlp"
+ PKG_TYPE_LANGUAGE = "lgp"
+ PKG_TYPE_ATTEMPT_ONLY = "aop"
+
+ MANIFEST_TYPE_IMAGE = "image"
+ MANIFEST_TYPE_SDK_HOST = "sdk_host"
+ MANIFEST_TYPE_SDK_TARGET = "sdk_target"
+
+ var_maps = {
+ MANIFEST_TYPE_IMAGE: {
+ "PACKAGE_INSTALL": PKG_TYPE_MUST_INSTALL,
+ "PACKAGE_INSTALL_ATTEMPTONLY": PKG_TYPE_ATTEMPT_ONLY,
+ "LINGUAS_INSTALL": PKG_TYPE_LANGUAGE
+ },
+ MANIFEST_TYPE_SDK_HOST: {
+ "TOOLCHAIN_HOST_TASK": PKG_TYPE_MUST_INSTALL,
+ "TOOLCHAIN_HOST_TASK_ATTEMPTONLY": PKG_TYPE_ATTEMPT_ONLY
+ },
+ MANIFEST_TYPE_SDK_TARGET: {
+ "TOOLCHAIN_TARGET_TASK": PKG_TYPE_MUST_INSTALL,
+ "TOOLCHAIN_TARGET_TASK_ATTEMPTONLY": PKG_TYPE_ATTEMPT_ONLY
+ }
+ }
+
+ INSTALL_ORDER = [
+ PKG_TYPE_LANGUAGE,
+ PKG_TYPE_MUST_INSTALL,
+ PKG_TYPE_ATTEMPT_ONLY,
+ PKG_TYPE_MULTILIB
+ ]
+
+ initial_manifest_file_header = \
+ "# This file was generated automatically and contains the packages\n" \
+ "# passed on to the package manager in order to create the rootfs.\n\n" \
+ "# Format:\n" \
+ "# <package_type>,<package_name>\n" \
+ "# where:\n" \
+ "# <package_type> can be:\n" \
+ "# 'mip' = must install package\n" \
+ "# 'aop' = attempt only package\n" \
+ "# 'mlp' = multilib package\n" \
+ "# 'lgp' = language package\n\n"
+
+ def __init__(self, d, manifest_dir=None, manifest_type=MANIFEST_TYPE_IMAGE):
+ self.d = d
+ self.manifest_type = manifest_type
+
+ if manifest_dir is None:
+ if manifest_type != self.MANIFEST_TYPE_IMAGE:
+ self.manifest_dir = self.d.getVar('SDK_DIR', True)
+ else:
+ self.manifest_dir = self.d.getVar('WORKDIR', True)
+ else:
+ self.manifest_dir = manifest_dir
+
+ bb.utils.mkdirhier(self.manifest_dir)
+
+ self.initial_manifest = os.path.join(self.manifest_dir, "%s_initial_manifest" % manifest_type)
+ self.final_manifest = os.path.join(self.manifest_dir, "%s_final_manifest" % manifest_type)
+ self.full_manifest = os.path.join(self.manifest_dir, "%s_full_manifest" % manifest_type)
+
+ # packages in the following vars will be split in 'must install' and
+ # 'multilib'
+ self.vars_to_split = ["PACKAGE_INSTALL",
+ "TOOLCHAIN_HOST_TASK",
+ "TOOLCHAIN_TARGET_TASK"]
+
+ """
+ This creates a standard initial manifest for core-image-(minimal|sato|sato-sdk).
+ This will be used for testing until the class is implemented properly!
+ """
+ def _create_dummy_initial(self):
+ image_rootfs = self.d.getVar('IMAGE_ROOTFS', True)
+ pkg_list = dict()
+ if image_rootfs.find("core-image-sato-sdk") > 0:
+ pkg_list[self.PKG_TYPE_MUST_INSTALL] = \
+ "packagegroup-core-x11-sato-games packagegroup-base-extended " \
+ "packagegroup-core-x11-sato packagegroup-core-x11-base " \
+ "packagegroup-core-sdk packagegroup-core-tools-debug " \
+ "packagegroup-core-boot packagegroup-core-tools-testapps " \
+ "packagegroup-core-eclipse-debug packagegroup-core-qt-demoapps " \
+ "apt packagegroup-core-tools-profile psplash " \
+ "packagegroup-core-standalone-sdk-target " \
+ "packagegroup-core-ssh-openssh dpkg kernel-dev"
+ pkg_list[self.PKG_TYPE_LANGUAGE] = \
+ "locale-base-en-us locale-base-en-gb"
+ elif image_rootfs.find("core-image-sato") > 0:
+ pkg_list[self.PKG_TYPE_MUST_INSTALL] = \
+ "packagegroup-core-ssh-dropbear packagegroup-core-x11-sato-games " \
+ "packagegroup-core-x11-base psplash apt dpkg packagegroup-base-extended " \
+ "packagegroup-core-x11-sato packagegroup-core-boot"
+ pkg_list['lgp'] = \
+ "locale-base-en-us locale-base-en-gb"
+ elif image_rootfs.find("core-image-minimal") > 0:
+ pkg_list[self.PKG_TYPE_MUST_INSTALL] = "run-postinsts packagegroup-core-boot"
+
+ with open(self.initial_manifest, "w+") as manifest:
+ manifest.write(self.initial_manifest_file_header)
+
+ for pkg_type in pkg_list:
+ for pkg in pkg_list[pkg_type].split():
+ manifest.write("%s,%s\n" % (pkg_type, pkg))
+
+ """
+ This will create the initial manifest which will be used by Rootfs class to
+ generate the rootfs
+ """
+ @abstractmethod
+ def create_initial(self):
+ pass
+
+ """
+ This creates the manifest after everything has been installed.
+ """
+ @abstractmethod
+ def create_final(self):
+ pass
+
+ """
+ This creates the manifest after the package in initial manifest has been
+ dummy installed. It lists all *to be installed* packages. There is no real
+ installation, just a test.
+ """
+ @abstractmethod
+ def create_full(self, pm):
+ pass
+
+ """
+ The following function parses an initial manifest and returns a dictionary
+ object with the must install, attempt only, multilib and language packages.
+ """
+ def parse_initial_manifest(self):
+ pkgs = dict()
+
+ with open(self.initial_manifest) as manifest:
+ for line in manifest.read().split('\n'):
+ comment = re.match("^#.*", line)
+ pattern = "^(%s|%s|%s|%s),(.*)$" % \
+ (self.PKG_TYPE_MUST_INSTALL,
+ self.PKG_TYPE_ATTEMPT_ONLY,
+ self.PKG_TYPE_MULTILIB,
+ self.PKG_TYPE_LANGUAGE)
+ pkg = re.match(pattern, line)
+
+ if comment is not None:
+ continue
+
+ if pkg is not None:
+ pkg_type = pkg.group(1)
+ pkg_name = pkg.group(2)
+
+ if not pkg_type in pkgs:
+ pkgs[pkg_type] = [pkg_name]
+ else:
+ pkgs[pkg_type].append(pkg_name)
+
+ return pkgs
+
+ '''
+ This following function parses a full manifest and return a list
+ object with packages.
+ '''
+ def parse_full_manifest(self):
+ installed_pkgs = list()
+ if not os.path.exists(self.full_manifest):
+ bb.note('full manifest not exist')
+ return installed_pkgs
+
+ with open(self.full_manifest, 'r') as manifest:
+ for pkg in manifest.read().split('\n'):
+ installed_pkgs.append(pkg.strip())
+
+ return installed_pkgs
+
+
+class RpmManifest(Manifest):
+ """
+ Returns a dictionary object with mip and mlp packages.
+ """
+ def _split_multilib(self, pkg_list):
+ pkgs = dict()
+
+ for pkg in pkg_list.split():
+ pkg_type = self.PKG_TYPE_MUST_INSTALL
+
+ ml_variants = self.d.getVar('MULTILIB_VARIANTS', True).split()
+
+ for ml_variant in ml_variants:
+ if pkg.startswith(ml_variant + '-'):
+ pkg_type = self.PKG_TYPE_MULTILIB
+
+ if not pkg_type in pkgs:
+ pkgs[pkg_type] = pkg
+ else:
+ pkgs[pkg_type] += " " + pkg
+
+ return pkgs
+
+ def create_initial(self):
+ pkgs = dict()
+
+ with open(self.initial_manifest, "w+") as manifest:
+ manifest.write(self.initial_manifest_file_header)
+
+ for var in self.var_maps[self.manifest_type]:
+ if var in self.vars_to_split:
+ split_pkgs = self._split_multilib(self.d.getVar(var, True))
+ if split_pkgs is not None:
+ pkgs = dict(pkgs.items() + split_pkgs.items())
+ else:
+ pkg_list = self.d.getVar(var, True)
+ if pkg_list is not None:
+ pkgs[self.var_maps[self.manifest_type][var]] = self.d.getVar(var, True)
+
+ for pkg_type in pkgs:
+ for pkg in pkgs[pkg_type].split():
+ manifest.write("%s,%s\n" % (pkg_type, pkg))
+
+ def create_final(self):
+ pass
+
+ def create_full(self, pm):
+ pass
+
+
+class OpkgManifest(Manifest):
+ """
+ Returns a dictionary object with mip and mlp packages.
+ """
+ def _split_multilib(self, pkg_list):
+ pkgs = dict()
+
+ for pkg in pkg_list.split():
+ pkg_type = self.PKG_TYPE_MUST_INSTALL
+
+ ml_variants = self.d.getVar('MULTILIB_VARIANTS', True).split()
+
+ for ml_variant in ml_variants:
+ if pkg.startswith(ml_variant + '-'):
+ pkg_type = self.PKG_TYPE_MULTILIB
+
+ if not pkg_type in pkgs:
+ pkgs[pkg_type] = pkg
+ else:
+ pkgs[pkg_type] += " " + pkg
+
+ return pkgs
+
+ def create_initial(self):
+ pkgs = dict()
+
+ with open(self.initial_manifest, "w+") as manifest:
+ manifest.write(self.initial_manifest_file_header)
+
+ for var in self.var_maps[self.manifest_type]:
+ if var in self.vars_to_split:
+ split_pkgs = self._split_multilib(self.d.getVar(var, True))
+ if split_pkgs is not None:
+ pkgs = dict(pkgs.items() + split_pkgs.items())
+ else:
+ pkg_list = self.d.getVar(var, True)
+ if pkg_list is not None:
+ pkgs[self.var_maps[self.manifest_type][var]] = self.d.getVar(var, True)
+
+ for pkg_type in pkgs:
+ for pkg in pkgs[pkg_type].split():
+ manifest.write("%s,%s\n" % (pkg_type, pkg))
+
+ def create_final(self):
+ pass
+
+ def create_full(self, pm):
+ if not os.path.exists(self.initial_manifest):
+ self.create_initial()
+
+ initial_manifest = self.parse_initial_manifest()
+ pkgs_to_install = list()
+ for pkg_type in initial_manifest:
+ pkgs_to_install += initial_manifest[pkg_type]
+ if len(pkgs_to_install) == 0:
+ return
+
+ output = pm.dummy_install(pkgs_to_install)
+
+ with open(self.full_manifest, 'w+') as manifest:
+ pkg_re = re.compile('^Installing ([^ ]+) [^ ].*')
+ for line in set(output.split('\n')):
+ m = pkg_re.match(line)
+ if m:
+ manifest.write(m.group(1) + '\n')
+
+ return
+
+
+class DpkgManifest(Manifest):
+ def create_initial(self):
+ with open(self.initial_manifest, "w+") as manifest:
+ manifest.write(self.initial_manifest_file_header)
+
+ for var in self.var_maps[self.manifest_type]:
+ pkg_list = self.d.getVar(var, True)
+
+ if pkg_list is None:
+ continue
+
+ for pkg in pkg_list.split():
+ manifest.write("%s,%s\n" %
+ (self.var_maps[self.manifest_type][var], pkg))
+
+ def create_final(self):
+ pass
+
+ def create_full(self, pm):
+ pass
+
+
+def create_manifest(d, final_manifest=False, manifest_dir=None,
+ manifest_type=Manifest.MANIFEST_TYPE_IMAGE):
+ manifest_map = {'rpm': RpmManifest,
+ 'ipk': OpkgManifest,
+ 'deb': DpkgManifest}
+
+ manifest = manifest_map[d.getVar('IMAGE_PKGTYPE', True)](d, manifest_dir, manifest_type)
+
+ if final_manifest:
+ manifest.create_final()
+ else:
+ manifest.create_initial()
+
+
+if __name__ == "__main__":
+ pass
diff --git a/meta/lib/oe/package.py b/meta/lib/oe/package.py
new file mode 100644
index 0000000..f176446
--- /dev/null
+++ b/meta/lib/oe/package.py
@@ -0,0 +1,125 @@
+def runstrip(arg):
+ # Function to strip a single file, called from split_and_strip_files below
+ # A working 'file' (one which works on the target architecture)
+ #
+ # The elftype is a bit pattern (explained in split_and_strip_files) to tell
+ # us what type of file we're processing...
+ # 4 - executable
+ # 8 - shared library
+ # 16 - kernel module
+
+ import commands, stat, subprocess
+
+ (file, elftype, strip) = arg
+
+ newmode = None
+ if not os.access(file, os.W_OK) or os.access(file, os.R_OK):
+ origmode = os.stat(file)[stat.ST_MODE]
+ newmode = origmode | stat.S_IWRITE | stat.S_IREAD
+ os.chmod(file, newmode)
+
+ extraflags = ""
+
+ # kernel module
+ if elftype & 16:
+ extraflags = "--strip-debug --remove-section=.comment --remove-section=.note --preserve-dates"
+ # .so and shared library
+ elif ".so" in file and elftype & 8:
+ extraflags = "--remove-section=.comment --remove-section=.note --strip-unneeded"
+ # shared or executable:
+ elif elftype & 8 or elftype & 4:
+ extraflags = "--remove-section=.comment --remove-section=.note"
+
+ stripcmd = "'%s' %s '%s'" % (strip, extraflags, file)
+ bb.debug(1, "runstrip: %s" % stripcmd)
+
+ try:
+ output = subprocess.check_output(stripcmd, stderr=subprocess.STDOUT, shell=True)
+ except subprocess.CalledProcessError as e:
+ bb.error("runstrip: '%s' strip command failed with %s (%s)" % (stripcmd, e.returncode, e.output))
+
+ if newmode:
+ os.chmod(file, origmode)
+
+ return
+
+
+def file_translate(file):
+ ft = file.replace("@", "@at@")
+ ft = ft.replace(" ", "@space@")
+ ft = ft.replace("\t", "@tab@")
+ ft = ft.replace("[", "@openbrace@")
+ ft = ft.replace("]", "@closebrace@")
+ ft = ft.replace("_", "@underscore@")
+ return ft
+
+def filedeprunner(arg):
+ import re, subprocess, shlex
+
+ (pkg, pkgfiles, rpmdeps, pkgdest) = arg
+ provides = {}
+ requires = {}
+
+ r = re.compile(r'[<>=]+ +[^ ]*')
+
+ def process_deps(pipe, pkg, pkgdest, provides, requires):
+ for line in pipe:
+ f = line.split(" ", 1)[0].strip()
+ line = line.split(" ", 1)[1].strip()
+
+ if line.startswith("Requires:"):
+ i = requires
+ elif line.startswith("Provides:"):
+ i = provides
+ else:
+ continue
+
+ file = f.replace(pkgdest + "/" + pkg, "")
+ file = file_translate(file)
+ value = line.split(":", 1)[1].strip()
+ value = r.sub(r'(\g<0>)', value)
+
+ if value.startswith("rpmlib("):
+ continue
+ if value == "python":
+ continue
+ if file not in i:
+ i[file] = []
+ i[file].append(value)
+
+ return provides, requires
+
+ try:
+ dep_popen = subprocess.Popen(shlex.split(rpmdeps) + pkgfiles, stdout=subprocess.PIPE)
+ provides, requires = process_deps(dep_popen.stdout, pkg, pkgdest, provides, requires)
+ except OSError as e:
+ bb.error("rpmdeps: '%s' command failed, '%s'" % (shlex.split(rpmdeps) + pkgfiles, e))
+ raise e
+
+ return (pkg, provides, requires)
+
+
+def read_shlib_providers(d):
+ import re
+
+ shlib_provider = {}
+ shlibs_dirs = d.getVar('SHLIBSDIRS', True).split()
+ list_re = re.compile('^(.*)\.list$')
+ # Go from least to most specific since the last one found wins
+ for dir in reversed(shlibs_dirs):
+ bb.debug(2, "Reading shlib providers in %s" % (dir))
+ if not os.path.exists(dir):
+ continue
+ for file in os.listdir(dir):
+ m = list_re.match(file)
+ if m:
+ dep_pkg = m.group(1)
+ fd = open(os.path.join(dir, file))
+ lines = fd.readlines()
+ fd.close()
+ for l in lines:
+ s = l.strip().split(":")
+ if s[0] not in shlib_provider:
+ shlib_provider[s[0]] = {}
+ shlib_provider[s[0]][s[1]] = (dep_pkg, s[2])
+ return shlib_provider
diff --git a/meta/lib/oe/package_manager.py b/meta/lib/oe/package_manager.py
new file mode 100644
index 0000000..292ed44
--- /dev/null
+++ b/meta/lib/oe/package_manager.py
@@ -0,0 +1,1900 @@
+from abc import ABCMeta, abstractmethod
+import os
+import glob
+import subprocess
+import shutil
+import multiprocessing
+import re
+import bb
+import tempfile
+import oe.utils
+
+
+# this can be used by all PM backends to create the index files in parallel
+def create_index(arg):
+ index_cmd = arg
+
+ try:
+ bb.note("Executing '%s' ..." % index_cmd)
+ result = subprocess.check_output(index_cmd, stderr=subprocess.STDOUT, shell=True)
+ except subprocess.CalledProcessError as e:
+ return("Index creation command '%s' failed with return code %d:\n%s" %
+ (e.cmd, e.returncode, e.output))
+
+ if result:
+ bb.note(result)
+
+ return None
+
+
+class Indexer(object):
+ __metaclass__ = ABCMeta
+
+ def __init__(self, d, deploy_dir):
+ self.d = d
+ self.deploy_dir = deploy_dir
+
+ @abstractmethod
+ def write_index(self):
+ pass
+
+
+class RpmIndexer(Indexer):
+ def get_ml_prefix_and_os_list(self, arch_var=None, os_var=None):
+ package_archs = {
+ 'default': [],
+ }
+
+ target_os = {
+ 'default': "",
+ }
+
+ if arch_var is not None and os_var is not None:
+ package_archs['default'] = self.d.getVar(arch_var, True).split()
+ package_archs['default'].reverse()
+ target_os['default'] = self.d.getVar(os_var, True).strip()
+ else:
+ package_archs['default'] = self.d.getVar("PACKAGE_ARCHS", True).split()
+ # arch order is reversed. This ensures the -best- match is
+ # listed first!
+ package_archs['default'].reverse()
+ target_os['default'] = self.d.getVar("TARGET_OS", True).strip()
+ multilibs = self.d.getVar('MULTILIBS', True) or ""
+ for ext in multilibs.split():
+ eext = ext.split(':')
+ if len(eext) > 1 and eext[0] == 'multilib':
+ localdata = bb.data.createCopy(self.d)
+ default_tune_key = "DEFAULTTUNE_virtclass-multilib-" + eext[1]
+ default_tune = localdata.getVar(default_tune_key, False)
+ if default_tune is None:
+ default_tune_key = "DEFAULTTUNE_ML_" + eext[1]
+ default_tune = localdata.getVar(default_tune_key, False)
+ if default_tune:
+ localdata.setVar("DEFAULTTUNE", default_tune)
+ bb.data.update_data(localdata)
+ package_archs[eext[1]] = localdata.getVar('PACKAGE_ARCHS',
+ True).split()
+ package_archs[eext[1]].reverse()
+ target_os[eext[1]] = localdata.getVar("TARGET_OS",
+ True).strip()
+
+ ml_prefix_list = dict()
+ for mlib in package_archs:
+ if mlib == 'default':
+ ml_prefix_list[mlib] = package_archs[mlib]
+ else:
+ ml_prefix_list[mlib] = list()
+ for arch in package_archs[mlib]:
+ if arch in ['all', 'noarch', 'any']:
+ ml_prefix_list[mlib].append(arch)
+ else:
+ ml_prefix_list[mlib].append(mlib + "_" + arch)
+
+ return (ml_prefix_list, target_os)
+
+ def write_index(self):
+ sdk_pkg_archs = (self.d.getVar('SDK_PACKAGE_ARCHS', True) or "").replace('-', '_').split()
+ all_mlb_pkg_archs = (self.d.getVar('ALL_MULTILIB_PACKAGE_ARCHS', True) or "").replace('-', '_').split()
+
+ mlb_prefix_list = self.get_ml_prefix_and_os_list()[0]
+
+ archs = set()
+ for item in mlb_prefix_list:
+ archs = archs.union(set(i.replace('-', '_') for i in mlb_prefix_list[item]))
+
+ if len(archs) == 0:
+ archs = archs.union(set(all_mlb_pkg_archs))
+
+ archs = archs.union(set(sdk_pkg_archs))
+
+ rpm_createrepo = bb.utils.which(os.getenv('PATH'), "createrepo")
+ if self.d.getVar('PACKAGE_FEED_SIGN', True) == '1':
+ pkgfeed_gpg_name = self.d.getVar('PACKAGE_FEED_GPG_NAME', True)
+ pkgfeed_gpg_pass = self.d.getVar('PACKAGE_FEED_GPG_PASSPHRASE_FILE', True)
+ else:
+ pkgfeed_gpg_name = None
+ pkgfeed_gpg_pass = None
+ gpg_bin = self.d.getVar('GPG_BIN', True) or \
+ bb.utils.which(os.getenv('PATH'), "gpg")
+
+ index_cmds = []
+ repo_sign_cmds = []
+ rpm_dirs_found = False
+ for arch in archs:
+ dbpath = os.path.join(self.d.getVar('WORKDIR', True), 'rpmdb', arch)
+ if os.path.exists(dbpath):
+ bb.utils.remove(dbpath, True)
+ arch_dir = os.path.join(self.deploy_dir, arch)
+ if not os.path.isdir(arch_dir):
+ continue
+
+ index_cmds.append("%s --dbpath %s --update -q %s" % \
+ (rpm_createrepo, dbpath, arch_dir))
+ if pkgfeed_gpg_name:
+ repomd_file = os.path.join(arch_dir, 'repodata', 'repomd.xml')
+ gpg_cmd = "%s --detach-sign --armor --batch --no-tty --yes " \
+ "--passphrase-file '%s' -u '%s' %s" % (gpg_bin,
+ pkgfeed_gpg_pass, pkgfeed_gpg_name, repomd_file)
+ repo_sign_cmds.append(gpg_cmd)
+
+ rpm_dirs_found = True
+
+ if not rpm_dirs_found:
+ bb.note("There are no packages in %s" % self.deploy_dir)
+ return
+
+ # Create repodata
+ result = oe.utils.multiprocess_exec(index_cmds, create_index)
+ if result:
+ bb.fatal('%s' % ('\n'.join(result)))
+ # Sign repomd
+ result = oe.utils.multiprocess_exec(repo_sign_cmds, create_index)
+ if result:
+ bb.fatal('%s' % ('\n'.join(result)))
+ # Copy pubkey(s) to repo
+ distro_version = self.d.getVar('DISTRO_VERSION', True) or "oe.0"
+ if self.d.getVar('RPM_SIGN_PACKAGES', True) == '1':
+ shutil.copy2(self.d.getVar('RPM_GPG_PUBKEY', True),
+ os.path.join(self.deploy_dir,
+ 'RPM-GPG-KEY-%s' % distro_version))
+ if self.d.getVar('PACKAGE_FEED_SIGN', True) == '1':
+ shutil.copy2(self.d.getVar('PACKAGE_FEED_GPG_PUBKEY', True),
+ os.path.join(self.deploy_dir,
+ 'REPODATA-GPG-KEY-%s' % distro_version))
+
+
+class OpkgIndexer(Indexer):
+ def write_index(self):
+ arch_vars = ["ALL_MULTILIB_PACKAGE_ARCHS",
+ "SDK_PACKAGE_ARCHS",
+ "MULTILIB_ARCHS"]
+
+ opkg_index_cmd = bb.utils.which(os.getenv('PATH'), "opkg-make-index")
+
+ if not os.path.exists(os.path.join(self.deploy_dir, "Packages")):
+ open(os.path.join(self.deploy_dir, "Packages"), "w").close()
+
+ index_cmds = []
+ for arch_var in arch_vars:
+ archs = self.d.getVar(arch_var, True)
+ if archs is None:
+ continue
+
+ for arch in archs.split():
+ pkgs_dir = os.path.join(self.deploy_dir, arch)
+ pkgs_file = os.path.join(pkgs_dir, "Packages")
+
+ if not os.path.isdir(pkgs_dir):
+ continue
+
+ if not os.path.exists(pkgs_file):
+ open(pkgs_file, "w").close()
+
+ index_cmds.append('%s -r %s -p %s -m %s' %
+ (opkg_index_cmd, pkgs_file, pkgs_file, pkgs_dir))
+
+ if len(index_cmds) == 0:
+ bb.note("There are no packages in %s!" % self.deploy_dir)
+ return
+
+ result = oe.utils.multiprocess_exec(index_cmds, create_index)
+ if result:
+ bb.fatal('%s' % ('\n'.join(result)))
+
+
+
+class DpkgIndexer(Indexer):
+ def _create_configs(self):
+ bb.utils.mkdirhier(self.apt_conf_dir)
+ bb.utils.mkdirhier(os.path.join(self.apt_conf_dir, "lists", "partial"))
+ bb.utils.mkdirhier(os.path.join(self.apt_conf_dir, "apt.conf.d"))
+ bb.utils.mkdirhier(os.path.join(self.apt_conf_dir, "preferences.d"))
+
+ with open(os.path.join(self.apt_conf_dir, "preferences"),
+ "w") as prefs_file:
+ pass
+ with open(os.path.join(self.apt_conf_dir, "sources.list"),
+ "w+") as sources_file:
+ pass
+
+ with open(self.apt_conf_file, "w") as apt_conf:
+ with open(os.path.join(self.d.expand("${STAGING_ETCDIR_NATIVE}"),
+ "apt", "apt.conf.sample")) as apt_conf_sample:
+ for line in apt_conf_sample.read().split("\n"):
+ line = re.sub("#ROOTFS#", "/dev/null", line)
+ line = re.sub("#APTCONF#", self.apt_conf_dir, line)
+ apt_conf.write(line + "\n")
+
+ def write_index(self):
+ self.apt_conf_dir = os.path.join(self.d.expand("${APTCONF_TARGET}"),
+ "apt-ftparchive")
+ self.apt_conf_file = os.path.join(self.apt_conf_dir, "apt.conf")
+ self._create_configs()
+
+ os.environ['APT_CONFIG'] = self.apt_conf_file
+
+ pkg_archs = self.d.getVar('PACKAGE_ARCHS', True)
+ if pkg_archs is not None:
+ arch_list = pkg_archs.split()
+ sdk_pkg_archs = self.d.getVar('SDK_PACKAGE_ARCHS', True)
+ if sdk_pkg_archs is not None:
+ for a in sdk_pkg_archs.split():
+ if a not in pkg_archs:
+ arch_list.append(a)
+
+ all_mlb_pkg_arch_list = (self.d.getVar('ALL_MULTILIB_PACKAGE_ARCHS', True) or "").replace('-', '_').split()
+ arch_list.extend(arch for arch in all_mlb_pkg_arch_list if arch not in arch_list)
+
+ apt_ftparchive = bb.utils.which(os.getenv('PATH'), "apt-ftparchive")
+ gzip = bb.utils.which(os.getenv('PATH'), "gzip")
+
+ index_cmds = []
+ deb_dirs_found = False
+ for arch in arch_list:
+ arch_dir = os.path.join(self.deploy_dir, arch)
+ if not os.path.isdir(arch_dir):
+ continue
+
+ cmd = "cd %s; PSEUDO_UNLOAD=1 %s packages . > Packages;" % (arch_dir, apt_ftparchive)
+
+ cmd += "%s -fc Packages > Packages.gz;" % gzip
+
+ with open(os.path.join(arch_dir, "Release"), "w+") as release:
+ release.write("Label: %s\n" % arch)
+
+ cmd += "PSEUDO_UNLOAD=1 %s release . >> Release" % apt_ftparchive
+
+ index_cmds.append(cmd)
+
+ deb_dirs_found = True
+
+ if not deb_dirs_found:
+ bb.note("There are no packages in %s" % self.deploy_dir)
+ return
+
+ result = oe.utils.multiprocess_exec(index_cmds, create_index)
+ if result:
+ bb.fatal('%s' % ('\n'.join(result)))
+
+
+
+class PkgsList(object):
+ __metaclass__ = ABCMeta
+
+ def __init__(self, d, rootfs_dir):
+ self.d = d
+ self.rootfs_dir = rootfs_dir
+
+ @abstractmethod
+ def list(self, format=None):
+ pass
+
+
+class RpmPkgsList(PkgsList):
+ def __init__(self, d, rootfs_dir, arch_var=None, os_var=None):
+ super(RpmPkgsList, self).__init__(d, rootfs_dir)
+
+ self.rpm_cmd = bb.utils.which(os.getenv('PATH'), "rpm")
+ self.image_rpmlib = os.path.join(self.rootfs_dir, 'var/lib/rpm')
+
+ self.ml_prefix_list, self.ml_os_list = \
+ RpmIndexer(d, rootfs_dir).get_ml_prefix_and_os_list(arch_var, os_var)
+
+ # Determine rpm version
+ cmd = "%s --version" % self.rpm_cmd
+ try:
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Getting rpm version failed. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+ self.rpm_version = int(output.split()[-1].split('.')[0])
+
+ '''
+ Translate the RPM/Smart format names to the OE multilib format names
+ '''
+ def _pkg_translate_smart_to_oe(self, pkg, arch):
+ new_pkg = pkg
+ new_arch = arch
+ fixed_arch = arch.replace('_', '-')
+ found = 0
+ for mlib in self.ml_prefix_list:
+ for cmp_arch in self.ml_prefix_list[mlib]:
+ fixed_cmp_arch = cmp_arch.replace('_', '-')
+ if fixed_arch == fixed_cmp_arch:
+ if mlib == 'default':
+ new_pkg = pkg
+ new_arch = cmp_arch
+ else:
+ new_pkg = mlib + '-' + pkg
+ # We need to strip off the ${mlib}_ prefix on the arch
+ new_arch = cmp_arch.replace(mlib + '_', '')
+
+ # Workaround for bug 3565. Simply look to see if we
+ # know of a package with that name, if not try again!
+ filename = os.path.join(self.d.getVar('PKGDATA_DIR', True),
+ 'runtime-reverse',
+ new_pkg)
+ if os.path.exists(filename):
+ found = 1
+ break
+
+ if found == 1 and fixed_arch == fixed_cmp_arch:
+ break
+ #bb.note('%s, %s -> %s, %s' % (pkg, arch, new_pkg, new_arch))
+ return new_pkg, new_arch
+
+ def _list_pkg_deps(self):
+ cmd = [bb.utils.which(os.getenv('PATH'), "rpmresolve"),
+ "-t", self.image_rpmlib]
+
+ try:
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).strip()
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Cannot get the package dependencies. Command '%s' "
+ "returned %d:\n%s" % (' '.join(cmd), e.returncode, e.output))
+
+ return output
+
+ def list(self, format=None):
+ if format == "deps":
+ if self.rpm_version == 4:
+ bb.fatal("'deps' format dependency listings are not supported with rpm 4 since rpmresolve does not work")
+ return self._list_pkg_deps()
+
+ cmd = self.rpm_cmd + ' --root ' + self.rootfs_dir
+ cmd += ' -D "_dbpath /var/lib/rpm" -qa'
+ if self.rpm_version == 4:
+ cmd += " --qf '[%{NAME} %{ARCH} %{VERSION}\n]'"
+ else:
+ cmd += " --qf '[%{NAME} %{ARCH} %{VERSION} %{PACKAGEORIGIN}\n]'"
+
+ try:
+ # bb.note(cmd)
+ tmp_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True).strip()
+
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Cannot get the installed packages list. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ output = list()
+ for line in tmp_output.split('\n'):
+ if len(line.strip()) == 0:
+ continue
+ pkg = line.split()[0]
+ arch = line.split()[1]
+ ver = line.split()[2]
+ # Skip GPG keys
+ if pkg == 'gpg-pubkey':
+ continue
+ if self.rpm_version == 4:
+ pkgorigin = "unknown"
+ else:
+ pkgorigin = line.split()[3]
+ new_pkg, new_arch = self._pkg_translate_smart_to_oe(pkg, arch)
+
+ if format == "arch":
+ output.append('%s %s' % (new_pkg, new_arch))
+ elif format == "file":
+ output.append('%s %s %s' % (new_pkg, pkgorigin, new_arch))
+ elif format == "ver":
+ output.append('%s %s %s' % (new_pkg, new_arch, ver))
+ else:
+ output.append('%s' % (new_pkg))
+
+ output.sort()
+
+ return '\n'.join(output)
+
+
+class OpkgPkgsList(PkgsList):
+ def __init__(self, d, rootfs_dir, config_file):
+ super(OpkgPkgsList, self).__init__(d, rootfs_dir)
+
+ self.opkg_cmd = bb.utils.which(os.getenv('PATH'), "opkg")
+ self.opkg_args = "-f %s -o %s " % (config_file, rootfs_dir)
+ self.opkg_args += self.d.getVar("OPKG_ARGS", True)
+
+ def list(self, format=None):
+ opkg_query_cmd = bb.utils.which(os.getenv('PATH'), "opkg-query-helper.py")
+
+ if format == "arch":
+ cmd = "%s %s status | %s -a" % \
+ (self.opkg_cmd, self.opkg_args, opkg_query_cmd)
+ elif format == "file":
+ cmd = "%s %s status | %s -f" % \
+ (self.opkg_cmd, self.opkg_args, opkg_query_cmd)
+ elif format == "ver":
+ cmd = "%s %s status | %s -v" % \
+ (self.opkg_cmd, self.opkg_args, opkg_query_cmd)
+ elif format == "deps":
+ cmd = "%s %s status | %s" % \
+ (self.opkg_cmd, self.opkg_args, opkg_query_cmd)
+ else:
+ cmd = "%s %s list_installed | cut -d' ' -f1" % \
+ (self.opkg_cmd, self.opkg_args)
+
+ try:
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True).strip()
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Cannot get the installed packages list. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ if output and format == "file":
+ tmp_output = ""
+ for line in output.split('\n'):
+ pkg, pkg_file, pkg_arch = line.split()
+ full_path = os.path.join(self.rootfs_dir, pkg_arch, pkg_file)
+ if os.path.exists(full_path):
+ tmp_output += "%s %s %s\n" % (pkg, full_path, pkg_arch)
+ else:
+ tmp_output += "%s %s %s\n" % (pkg, pkg_file, pkg_arch)
+
+ output = tmp_output
+
+ return output
+
+
+class DpkgPkgsList(PkgsList):
+ def list(self, format=None):
+ cmd = [bb.utils.which(os.getenv('PATH'), "dpkg-query"),
+ "--admindir=%s/var/lib/dpkg" % self.rootfs_dir,
+ "-W"]
+
+ if format == "arch":
+ cmd.append("-f=${Package} ${PackageArch}\n")
+ elif format == "file":
+ cmd.append("-f=${Package} ${Package}_${Version}_${Architecture}.deb ${PackageArch}\n")
+ elif format == "ver":
+ cmd.append("-f=${Package} ${PackageArch} ${Version}\n")
+ elif format == "deps":
+ cmd.append("-f=Package: ${Package}\nDepends: ${Depends}\nRecommends: ${Recommends}\n\n")
+ else:
+ cmd.append("-f=${Package}\n")
+
+ try:
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).strip()
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Cannot get the installed packages list. Command '%s' "
+ "returned %d:\n%s" % (' '.join(cmd), e.returncode, e.output))
+
+ if format == "file":
+ tmp_output = ""
+ for line in tuple(output.split('\n')):
+ if not line.strip():
+ continue
+ pkg, pkg_file, pkg_arch = line.split()
+ full_path = os.path.join(self.rootfs_dir, pkg_arch, pkg_file)
+ if os.path.exists(full_path):
+ tmp_output += "%s %s %s\n" % (pkg, full_path, pkg_arch)
+ else:
+ tmp_output += "%s %s %s\n" % (pkg, pkg_file, pkg_arch)
+
+ output = tmp_output
+ elif format == "deps":
+ opkg_query_cmd = bb.utils.which(os.getenv('PATH'), "opkg-query-helper.py")
+ file_out = tempfile.NamedTemporaryFile()
+ file_out.write(output)
+ file_out.flush()
+
+ try:
+ output = subprocess.check_output("cat %s | %s" %
+ (file_out.name, opkg_query_cmd),
+ stderr=subprocess.STDOUT,
+ shell=True)
+ except subprocess.CalledProcessError as e:
+ file_out.close()
+ bb.fatal("Cannot compute packages dependencies. Command '%s' "
+ "returned %d:\n%s" % (e.cmd, e.returncode, e.output))
+
+ file_out.close()
+
+ return output
+
+
+class PackageManager(object):
+ """
+ This is an abstract class. Do not instantiate this directly.
+ """
+ __metaclass__ = ABCMeta
+
+ def __init__(self, d):
+ self.d = d
+ self.deploy_dir = None
+ self.deploy_lock = None
+ self.feed_uris = self.d.getVar('PACKAGE_FEED_URIS', True) or ""
+ self.feed_prefix = self.d.getVar('PACKAGE_FEED_PREFIX', True) or ""
+
+ """
+ Update the package manager package database.
+ """
+ @abstractmethod
+ def update(self):
+ pass
+
+ """
+ Install a list of packages. 'pkgs' is a list object. If 'attempt_only' is
+ True, installation failures are ignored.
+ """
+ @abstractmethod
+ def install(self, pkgs, attempt_only=False):
+ pass
+
+ """
+ Remove a list of packages. 'pkgs' is a list object. If 'with_dependencies'
+ is False, the any dependencies are left in place.
+ """
+ @abstractmethod
+ def remove(self, pkgs, with_dependencies=True):
+ pass
+
+ """
+ This function creates the index files
+ """
+ @abstractmethod
+ def write_index(self):
+ pass
+
+ @abstractmethod
+ def remove_packaging_data(self):
+ pass
+
+ @abstractmethod
+ def list_installed(self, format=None):
+ pass
+
+ @abstractmethod
+ def insert_feeds_uris(self):
+ pass
+
+ """
+ Install complementary packages based upon the list of currently installed
+ packages e.g. locales, *-dev, *-dbg, etc. This will only attempt to install
+ these packages, if they don't exist then no error will occur. Note: every
+ backend needs to call this function explicitly after the normal package
+ installation
+ """
+ def install_complementary(self, globs=None):
+ # we need to write the list of installed packages to a file because the
+ # oe-pkgdata-util reads it from a file
+ installed_pkgs_file = os.path.join(self.d.getVar('WORKDIR', True),
+ "installed_pkgs.txt")
+ with open(installed_pkgs_file, "w+") as installed_pkgs:
+ installed_pkgs.write(self.list_installed("arch"))
+
+ if globs is None:
+ globs = self.d.getVar('IMAGE_INSTALL_COMPLEMENTARY', True)
+ split_linguas = set()
+
+ for translation in self.d.getVar('IMAGE_LINGUAS', True).split():
+ split_linguas.add(translation)
+ split_linguas.add(translation.split('-')[0])
+
+ split_linguas = sorted(split_linguas)
+
+ for lang in split_linguas:
+ globs += " *-locale-%s" % lang
+
+ if globs is None:
+ return
+
+ cmd = [bb.utils.which(os.getenv('PATH'), "oe-pkgdata-util"),
+ "-p", self.d.getVar('PKGDATA_DIR', True), "glob", installed_pkgs_file,
+ globs]
+ exclude = self.d.getVar('PACKAGE_EXCLUDE_COMPLEMENTARY', True)
+ if exclude:
+ cmd.extend(['-x', exclude])
+ try:
+ bb.note("Installing complementary packages ...")
+ complementary_pkgs = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Could not compute complementary packages list. Command "
+ "'%s' returned %d:\n%s" %
+ (' '.join(cmd), e.returncode, e.output))
+
+ self.install(complementary_pkgs.split(), attempt_only=True)
+
+ def deploy_dir_lock(self):
+ if self.deploy_dir is None:
+ raise RuntimeError("deploy_dir is not set!")
+
+ lock_file_name = os.path.join(self.deploy_dir, "deploy.lock")
+
+ self.deploy_lock = bb.utils.lockfile(lock_file_name)
+
+ def deploy_dir_unlock(self):
+ if self.deploy_lock is None:
+ return
+
+ bb.utils.unlockfile(self.deploy_lock)
+
+ self.deploy_lock = None
+
+
+class RpmPM(PackageManager):
+ def __init__(self,
+ d,
+ target_rootfs,
+ target_vendor,
+ task_name='target',
+ providename=None,
+ arch_var=None,
+ os_var=None):
+ super(RpmPM, self).__init__(d)
+ self.target_rootfs = target_rootfs
+ self.target_vendor = target_vendor
+ self.task_name = task_name
+ self.providename = providename
+ self.fullpkglist = list()
+ self.deploy_dir = self.d.getVar('DEPLOY_DIR_RPM', True)
+ self.etcrpm_dir = os.path.join(self.target_rootfs, "etc/rpm")
+ self.install_dir_name = "oe_install"
+ self.install_dir_path = os.path.join(self.target_rootfs, self.install_dir_name)
+ self.rpm_cmd = bb.utils.which(os.getenv('PATH'), "rpm")
+ self.smart_cmd = bb.utils.which(os.getenv('PATH'), "smart")
+ self.smart_opt = "--log-level=warning --data-dir=" + os.path.join(target_rootfs,
+ 'var/lib/smart')
+ self.scriptlet_wrapper = self.d.expand('${WORKDIR}/scriptlet_wrapper')
+ self.solution_manifest = self.d.expand('${T}/saved/%s_solution' %
+ self.task_name)
+ self.saved_rpmlib = self.d.expand('${T}/saved/%s' % self.task_name)
+ self.image_rpmlib = os.path.join(self.target_rootfs, 'var/lib/rpm')
+
+ if not os.path.exists(self.d.expand('${T}/saved')):
+ bb.utils.mkdirhier(self.d.expand('${T}/saved'))
+
+ self.indexer = RpmIndexer(self.d, self.deploy_dir)
+ self.pkgs_list = RpmPkgsList(self.d, self.target_rootfs, arch_var, os_var)
+ self.rpm_version = self.pkgs_list.rpm_version
+
+ self.ml_prefix_list, self.ml_os_list = self.indexer.get_ml_prefix_and_os_list(arch_var, os_var)
+
+ def insert_feeds_uris(self):
+ if self.feed_uris == "":
+ return
+
+ # List must be prefered to least preferred order
+ default_platform_extra = set()
+ platform_extra = set()
+ bbextendvariant = self.d.getVar('BBEXTENDVARIANT', True) or ""
+ for mlib in self.ml_os_list:
+ for arch in self.ml_prefix_list[mlib]:
+ plt = arch.replace('-', '_') + '-.*-' + self.ml_os_list[mlib]
+ if mlib == bbextendvariant:
+ default_platform_extra.add(plt)
+ else:
+ platform_extra.add(plt)
+
+ platform_extra = platform_extra.union(default_platform_extra)
+
+ arch_list = []
+ for canonical_arch in platform_extra:
+ arch = canonical_arch.split('-')[0]
+ if not os.path.exists(os.path.join(self.deploy_dir, arch)):
+ continue
+ arch_list.append(arch)
+
+ uri_iterator = 0
+ channel_priority = 10 + 5 * len(self.feed_uris.split()) * len(arch_list)
+
+ for uri in self.feed_uris.split():
+ full_uri = uri
+ if self.feed_prefix:
+ full_uri = os.path.join(uri, self.feed_prefix)
+ for arch in arch_list:
+ bb.note('Note: adding Smart channel url%d%s (%s)' %
+ (uri_iterator, arch, channel_priority))
+ self._invoke_smart('channel --add url%d-%s type=rpm-md baseurl=%s/%s -y'
+ % (uri_iterator, arch, full_uri, arch))
+ self._invoke_smart('channel --set url%d-%s priority=%d' %
+ (uri_iterator, arch, channel_priority))
+ channel_priority -= 5
+ uri_iterator += 1
+
+ '''
+ Create configs for rpm and smart, and multilib is supported
+ '''
+ def create_configs(self):
+ target_arch = self.d.getVar('TARGET_ARCH', True)
+ platform = '%s%s-%s' % (target_arch.replace('-', '_'),
+ self.target_vendor,
+ self.ml_os_list['default'])
+
+ # List must be prefered to least preferred order
+ default_platform_extra = list()
+ platform_extra = list()
+ bbextendvariant = self.d.getVar('BBEXTENDVARIANT', True) or ""
+ for mlib in self.ml_os_list:
+ for arch in self.ml_prefix_list[mlib]:
+ plt = arch.replace('-', '_') + '-.*-' + self.ml_os_list[mlib]
+ if mlib == bbextendvariant:
+ if plt not in default_platform_extra:
+ default_platform_extra.append(plt)
+ else:
+ if plt not in platform_extra:
+ platform_extra.append(plt)
+ platform_extra = default_platform_extra + platform_extra
+
+ self._create_configs(platform, platform_extra)
+
+ def _invoke_smart(self, args):
+ cmd = "%s %s %s" % (self.smart_cmd, self.smart_opt, args)
+ # bb.note(cmd)
+ try:
+ complementary_pkgs = subprocess.check_output(cmd,
+ stderr=subprocess.STDOUT,
+ shell=True)
+ # bb.note(complementary_pkgs)
+ return complementary_pkgs
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Could not invoke smart. Command "
+ "'%s' returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ def _search_pkg_name_in_feeds(self, pkg, feed_archs):
+ for arch in feed_archs:
+ arch = arch.replace('-', '_')
+ regex_match = re.compile(r"^%s-[^-]*-[^-]*@%s$" % \
+ (re.escape(pkg), re.escape(arch)))
+ for p in self.fullpkglist:
+ if regex_match.match(p) is not None:
+ # First found is best match
+ # bb.note('%s -> %s' % (pkg, pkg + '@' + arch))
+ return pkg + '@' + arch
+
+ # Search provides if not found by pkgname.
+ bb.note('Not found %s by name, searching provides ...' % pkg)
+ cmd = "%s %s query --provides %s --show-format='$name-$version'" % \
+ (self.smart_cmd, self.smart_opt, pkg)
+ cmd += " | sed -ne 's/ *Provides://p'"
+ bb.note('cmd: %s' % cmd)
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+ # Found a provider
+ if output:
+ bb.note('Found providers for %s: %s' % (pkg, output))
+ for p in output.split():
+ for arch in feed_archs:
+ arch = arch.replace('-', '_')
+ if p.rstrip().endswith('@' + arch):
+ return p
+
+ return ""
+
+ '''
+ Translate the OE multilib format names to the RPM/Smart format names
+ It searched the RPM/Smart format names in probable multilib feeds first,
+ and then searched the default base feed.
+ '''
+ def _pkg_translate_oe_to_smart(self, pkgs, attempt_only=False):
+ new_pkgs = list()
+
+ for pkg in pkgs:
+ new_pkg = pkg
+ # Search new_pkg in probable multilibs first
+ for mlib in self.ml_prefix_list:
+ # Jump the default archs
+ if mlib == 'default':
+ continue
+
+ subst = pkg.replace(mlib + '-', '')
+ # if the pkg in this multilib feed
+ if subst != pkg:
+ feed_archs = self.ml_prefix_list[mlib]
+ new_pkg = self._search_pkg_name_in_feeds(subst, feed_archs)
+ if not new_pkg:
+ # Failed to translate, package not found!
+ err_msg = '%s not found in the %s feeds (%s).\n' % \
+ (pkg, mlib, " ".join(feed_archs))
+ if not attempt_only:
+ err_msg += " ".join(self.fullpkglist)
+ bb.fatal(err_msg)
+ bb.warn(err_msg)
+ else:
+ new_pkgs.append(new_pkg)
+
+ break
+
+ # Apparently not a multilib package...
+ if pkg == new_pkg:
+ # Search new_pkg in default archs
+ default_archs = self.ml_prefix_list['default']
+ new_pkg = self._search_pkg_name_in_feeds(pkg, default_archs)
+ if not new_pkg:
+ err_msg = '%s not found in the base feeds (%s).\n' % \
+ (pkg, ' '.join(default_archs))
+ if not attempt_only:
+ err_msg += " ".join(self.fullpkglist)
+ bb.fatal(err_msg)
+ bb.warn(err_msg)
+ else:
+ new_pkgs.append(new_pkg)
+
+ return new_pkgs
+
+ def _create_configs(self, platform, platform_extra):
+ # Setup base system configuration
+ bb.note("configuring RPM platform settings")
+
+ # Configure internal RPM environment when using Smart
+ os.environ['RPM_ETCRPM'] = self.etcrpm_dir
+ bb.utils.mkdirhier(self.etcrpm_dir)
+
+ # Setup temporary directory -- install...
+ if os.path.exists(self.install_dir_path):
+ bb.utils.remove(self.install_dir_path, True)
+ bb.utils.mkdirhier(os.path.join(self.install_dir_path, 'tmp'))
+
+ channel_priority = 5
+ platform_dir = os.path.join(self.etcrpm_dir, "platform")
+ sdkos = self.d.getVar("SDK_OS", True)
+ with open(platform_dir, "w+") as platform_fd:
+ platform_fd.write(platform + '\n')
+ for pt in platform_extra:
+ channel_priority += 5
+ if sdkos:
+ tmp = re.sub("-%s$" % sdkos, "-%s\n" % sdkos, pt)
+ tmp = re.sub("-linux.*$", "-linux.*\n", tmp)
+ platform_fd.write(tmp)
+
+ # Tell RPM that the "/" directory exist and is available
+ bb.note("configuring RPM system provides")
+ sysinfo_dir = os.path.join(self.etcrpm_dir, "sysinfo")
+ bb.utils.mkdirhier(sysinfo_dir)
+ with open(os.path.join(sysinfo_dir, "Dirnames"), "w+") as dirnames:
+ dirnames.write("/\n")
+
+ if self.providename:
+ providename_dir = os.path.join(sysinfo_dir, "Providename")
+ if not os.path.exists(providename_dir):
+ providename_content = '\n'.join(self.providename)
+ providename_content += '\n'
+ open(providename_dir, "w+").write(providename_content)
+
+ # Configure RPM... we enforce these settings!
+ bb.note("configuring RPM DB settings")
+ # After change the __db.* cache size, log file will not be
+ # generated automatically, that will raise some warnings,
+ # so touch a bare log for rpm write into it.
+ if self.rpm_version == 5:
+ rpmlib_log = os.path.join(self.image_rpmlib, 'log', 'log.0000000001')
+ if not os.path.exists(rpmlib_log):
+ bb.utils.mkdirhier(os.path.join(self.image_rpmlib, 'log'))
+ open(rpmlib_log, 'w+').close()
+
+ DB_CONFIG_CONTENT = "# ================ Environment\n" \
+ "set_data_dir .\n" \
+ "set_create_dir .\n" \
+ "set_lg_dir ./log\n" \
+ "set_tmp_dir ./tmp\n" \
+ "set_flags db_log_autoremove on\n" \
+ "\n" \
+ "# -- thread_count must be >= 8\n" \
+ "set_thread_count 64\n" \
+ "\n" \
+ "# ================ Logging\n" \
+ "\n" \
+ "# ================ Memory Pool\n" \
+ "set_cachesize 0 1048576 0\n" \
+ "set_mp_mmapsize 268435456\n" \
+ "\n" \
+ "# ================ Locking\n" \
+ "set_lk_max_locks 16384\n" \
+ "set_lk_max_lockers 16384\n" \
+ "set_lk_max_objects 16384\n" \
+ "mutex_set_max 163840\n" \
+ "\n" \
+ "# ================ Replication\n"
+
+ db_config_dir = os.path.join(self.image_rpmlib, 'DB_CONFIG')
+ if not os.path.exists(db_config_dir):
+ open(db_config_dir, 'w+').write(DB_CONFIG_CONTENT)
+
+ # Create database so that smart doesn't complain (lazy init)
+ opt = "-qa"
+ if self.rpm_version == 4:
+ opt = "--initdb"
+ cmd = "%s --root %s --dbpath /var/lib/rpm %s > /dev/null" % (
+ self.rpm_cmd, self.target_rootfs, opt)
+ try:
+ subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Create rpm database failed. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+ # Import GPG key to RPM database of the target system
+ if self.d.getVar('RPM_SIGN_PACKAGES', True) == '1':
+ pubkey_path = self.d.getVar('RPM_GPG_PUBKEY', True)
+ cmd = "%s --root %s --dbpath /var/lib/rpm --import %s > /dev/null" % (
+ self.rpm_cmd, self.target_rootfs, pubkey_path)
+ subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+
+ # Configure smart
+ bb.note("configuring Smart settings")
+ bb.utils.remove(os.path.join(self.target_rootfs, 'var/lib/smart'),
+ True)
+ self._invoke_smart('config --set rpm-root=%s' % self.target_rootfs)
+ self._invoke_smart('config --set rpm-dbpath=/var/lib/rpm')
+ self._invoke_smart('config --set rpm-extra-macros._var=%s' %
+ self.d.getVar('localstatedir', True))
+ cmd = "config --set rpm-extra-macros._tmppath=/%s/tmp" % (self.install_dir_name)
+
+ prefer_color = self.d.getVar('RPM_PREFER_ELF_ARCH', True)
+ if prefer_color:
+ if prefer_color not in ['0', '1', '2', '4']:
+ bb.fatal("Invalid RPM_PREFER_ELF_ARCH: %s, it should be one of:\n"
+ "\t1: ELF32 wins\n"
+ "\t2: ELF64 wins\n"
+ "\t4: ELF64 N32 wins (mips64 or mips64el only)" %
+ prefer_color)
+ if prefer_color == "4" and self.d.getVar("TUNE_ARCH", True) not in \
+ ['mips64', 'mips64el']:
+ bb.fatal("RPM_PREFER_ELF_ARCH = \"4\" is for mips64 or mips64el "
+ "only.")
+ self._invoke_smart('config --set rpm-extra-macros._prefer_color=%s'
+ % prefer_color)
+
+ self._invoke_smart(cmd)
+ self._invoke_smart('config --set rpm-ignoresize=1')
+
+ # Write common configuration for host and target usage
+ self._invoke_smart('config --set rpm-nolinktos=1')
+ self._invoke_smart('config --set rpm-noparentdirs=1')
+ check_signature = self.d.getVar('RPM_CHECK_SIGNATURES', True)
+ if check_signature and check_signature.strip() == "0":
+ self._invoke_smart('config --set rpm-check-signatures=false')
+ for i in self.d.getVar('BAD_RECOMMENDATIONS', True).split():
+ self._invoke_smart('flag --set ignore-recommends %s' % i)
+
+ # Do the following configurations here, to avoid them being
+ # saved for field upgrade
+ if self.d.getVar('NO_RECOMMENDATIONS', True).strip() == "1":
+ self._invoke_smart('config --set ignore-all-recommends=1')
+ pkg_exclude = self.d.getVar('PACKAGE_EXCLUDE', True) or ""
+ for i in pkg_exclude.split():
+ self._invoke_smart('flag --set exclude-packages %s' % i)
+
+ # Optional debugging
+ # self._invoke_smart('config --set rpm-log-level=debug')
+ # cmd = 'config --set rpm-log-file=/tmp/smart-debug-logfile'
+ # self._invoke_smart(cmd)
+ ch_already_added = []
+ for canonical_arch in platform_extra:
+ arch = canonical_arch.split('-')[0]
+ arch_channel = os.path.join(self.deploy_dir, arch)
+ if os.path.exists(arch_channel) and not arch in ch_already_added:
+ bb.note('Note: adding Smart channel %s (%s)' %
+ (arch, channel_priority))
+ self._invoke_smart('channel --add %s type=rpm-md baseurl=%s -y'
+ % (arch, arch_channel))
+ self._invoke_smart('channel --set %s priority=%d' %
+ (arch, channel_priority))
+ channel_priority -= 5
+
+ ch_already_added.append(arch)
+
+ bb.note('adding Smart RPM DB channel')
+ self._invoke_smart('channel --add rpmsys type=rpm-sys -y')
+
+ # Construct install scriptlet wrapper.
+ # Scripts need to be ordered when executed, this ensures numeric order.
+ # If we ever run into needing more the 899 scripts, we'll have to.
+ # change num to start with 1000.
+ #
+ if self.rpm_version == 4:
+ scriptletcmd = "$2 $3 $4\n"
+ scriptpath = "$3"
+ else:
+ scriptletcmd = "$2 $1/$3 $4\n"
+ scriptpath = "$1/$3"
+
+ SCRIPTLET_FORMAT = "#!/bin/bash\n" \
+ "\n" \
+ "export PATH=%s\n" \
+ "export D=%s\n" \
+ 'export OFFLINE_ROOT="$D"\n' \
+ 'export IPKG_OFFLINE_ROOT="$D"\n' \
+ 'export OPKG_OFFLINE_ROOT="$D"\n' \
+ "export INTERCEPT_DIR=%s\n" \
+ "export NATIVE_ROOT=%s\n" \
+ "\n" \
+ + scriptletcmd + \
+ "if [ $? -ne 0 ]; then\n" \
+ " if [ $4 -eq 1 ]; then\n" \
+ " mkdir -p $1/etc/rpm-postinsts\n" \
+ " num=100\n" \
+ " while [ -e $1/etc/rpm-postinsts/${num}-* ]; do num=$((num + 1)); done\n" \
+ " name=`head -1 " + scriptpath + " | cut -d\' \' -f 2`\n" \
+ ' echo "#!$2" > $1/etc/rpm-postinsts/${num}-${name}\n' \
+ ' echo "# Arg: $4" >> $1/etc/rpm-postinsts/${num}-${name}\n' \
+ " cat " + scriptpath + " >> $1/etc/rpm-postinsts/${num}-${name}\n" \
+ " chmod +x $1/etc/rpm-postinsts/${num}-${name}\n" \
+ " else\n" \
+ ' echo "Error: pre/post remove scriptlet failed"\n' \
+ " fi\n" \
+ "fi\n"
+
+ intercept_dir = self.d.expand('${WORKDIR}/intercept_scripts')
+ native_root = self.d.getVar('STAGING_DIR_NATIVE', True)
+ scriptlet_content = SCRIPTLET_FORMAT % (os.environ['PATH'],
+ self.target_rootfs,
+ intercept_dir,
+ native_root)
+ open(self.scriptlet_wrapper, 'w+').write(scriptlet_content)
+
+ bb.note("Note: configuring RPM cross-install scriptlet_wrapper")
+ os.chmod(self.scriptlet_wrapper, 0755)
+ cmd = 'config --set rpm-extra-macros._cross_scriptlet_wrapper=%s' % \
+ self.scriptlet_wrapper
+ self._invoke_smart(cmd)
+
+ # Debug to show smart config info
+ # bb.note(self._invoke_smart('config --show'))
+
+ def update(self):
+ self._invoke_smart('update rpmsys')
+
+ '''
+ Install pkgs with smart, the pkg name is oe format
+ '''
+ def install(self, pkgs, attempt_only=False):
+
+ if not pkgs:
+ bb.note("There are no packages to install")
+ return
+ bb.note("Installing the following packages: %s" % ' '.join(pkgs))
+ pkgs = self._pkg_translate_oe_to_smart(pkgs, attempt_only)
+
+ if not attempt_only:
+ bb.note('to be installed: %s' % ' '.join(pkgs))
+ cmd = "%s %s install -y %s" % \
+ (self.smart_cmd, self.smart_opt, ' '.join(pkgs))
+ bb.note(cmd)
+ else:
+ bb.note('installing attempt only packages...')
+ bb.note('Attempting %s' % ' '.join(pkgs))
+ cmd = "%s %s install --attempt -y %s" % \
+ (self.smart_cmd, self.smart_opt, ' '.join(pkgs))
+ try:
+ output = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+ bb.note(output)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Unable to install packages. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ '''
+ Remove pkgs with smart, the pkg name is smart/rpm format
+ '''
+ def remove(self, pkgs, with_dependencies=True):
+ bb.note('to be removed: ' + ' '.join(pkgs))
+
+ if not with_dependencies:
+ cmd = "%s -e --nodeps " % self.rpm_cmd
+ cmd += "--root=%s " % self.target_rootfs
+ cmd += "--dbpath=/var/lib/rpm "
+ cmd += "--define='_cross_scriptlet_wrapper %s' " % \
+ self.scriptlet_wrapper
+ cmd += "--define='_tmppath /%s/tmp' %s" % (self.install_dir_name, ' '.join(pkgs))
+ else:
+ # for pkg in pkgs:
+ # bb.note('Debug: What required: %s' % pkg)
+ # bb.note(self._invoke_smart('query %s --show-requiredby' % pkg))
+
+ cmd = "%s %s remove -y %s" % (self.smart_cmd,
+ self.smart_opt,
+ ' '.join(pkgs))
+
+ try:
+ bb.note(cmd)
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+ bb.note(output)
+ except subprocess.CalledProcessError as e:
+ bb.note("Unable to remove packages. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ def upgrade(self):
+ bb.note('smart upgrade')
+ self._invoke_smart('upgrade')
+
+ def write_index(self):
+ result = self.indexer.write_index()
+
+ if result is not None:
+ bb.fatal(result)
+
+ def remove_packaging_data(self):
+ bb.utils.remove(self.image_rpmlib, True)
+ bb.utils.remove(os.path.join(self.target_rootfs, 'var/lib/smart'),
+ True)
+ bb.utils.remove(os.path.join(self.target_rootfs, 'var/lib/opkg'), True)
+
+ # remove temp directory
+ bb.utils.remove(self.install_dir_path, True)
+
+ def backup_packaging_data(self):
+ # Save the rpmlib for increment rpm image generation
+ if os.path.exists(self.saved_rpmlib):
+ bb.utils.remove(self.saved_rpmlib, True)
+ shutil.copytree(self.image_rpmlib,
+ self.saved_rpmlib,
+ symlinks=True)
+
+ def recovery_packaging_data(self):
+ # Move the rpmlib back
+ if os.path.exists(self.saved_rpmlib):
+ if os.path.exists(self.image_rpmlib):
+ bb.utils.remove(self.image_rpmlib, True)
+
+ bb.note('Recovery packaging data')
+ shutil.copytree(self.saved_rpmlib,
+ self.image_rpmlib,
+ symlinks=True)
+
+ def list_installed(self, format=None):
+ return self.pkgs_list.list(format)
+
+ '''
+ If incremental install, we need to determine what we've got,
+ what we need to add, and what to remove...
+ The dump_install_solution will dump and save the new install
+ solution.
+ '''
+ def dump_install_solution(self, pkgs):
+ bb.note('creating new install solution for incremental install')
+ if len(pkgs) == 0:
+ return
+
+ pkgs = self._pkg_translate_oe_to_smart(pkgs, False)
+ install_pkgs = list()
+
+ cmd = "%s %s install -y --dump %s 2>%s" % \
+ (self.smart_cmd,
+ self.smart_opt,
+ ' '.join(pkgs),
+ self.solution_manifest)
+ try:
+ # Disable rpmsys channel for the fake install
+ self._invoke_smart('channel --disable rpmsys')
+
+ subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+ with open(self.solution_manifest, 'r') as manifest:
+ for pkg in manifest.read().split('\n'):
+ if '@' in pkg:
+ install_pkgs.append(pkg)
+ except subprocess.CalledProcessError as e:
+ bb.note("Unable to dump install packages. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+ # Recovery rpmsys channel
+ self._invoke_smart('channel --enable rpmsys')
+ return install_pkgs
+
+ '''
+ If incremental install, we need to determine what we've got,
+ what we need to add, and what to remove...
+ The load_old_install_solution will load the previous install
+ solution
+ '''
+ def load_old_install_solution(self):
+ bb.note('load old install solution for incremental install')
+ installed_pkgs = list()
+ if not os.path.exists(self.solution_manifest):
+ bb.note('old install solution not exist')
+ return installed_pkgs
+
+ with open(self.solution_manifest, 'r') as manifest:
+ for pkg in manifest.read().split('\n'):
+ if '@' in pkg:
+ installed_pkgs.append(pkg.strip())
+
+ return installed_pkgs
+
+ '''
+ Dump all available packages in feeds, it should be invoked after the
+ newest rpm index was created
+ '''
+ def dump_all_available_pkgs(self):
+ available_manifest = self.d.expand('${T}/saved/available_pkgs.txt')
+ available_pkgs = list()
+ cmd = "%s %s query --output %s" % \
+ (self.smart_cmd, self.smart_opt, available_manifest)
+ try:
+ subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+ with open(available_manifest, 'r') as manifest:
+ for pkg in manifest.read().split('\n'):
+ if '@' in pkg:
+ available_pkgs.append(pkg.strip())
+ except subprocess.CalledProcessError as e:
+ bb.note("Unable to list all available packages. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ self.fullpkglist = available_pkgs
+
+ return
+
+ def save_rpmpostinst(self, pkg):
+ mlibs = (self.d.getVar('MULTILIB_GLOBAL_VARIANTS', False) or "").split()
+
+ new_pkg = pkg
+ # Remove any multilib prefix from the package name
+ for mlib in mlibs:
+ if mlib in pkg:
+ new_pkg = pkg.replace(mlib + '-', '')
+ break
+
+ bb.note(' * postponing %s' % new_pkg)
+ saved_dir = self.target_rootfs + self.d.expand('${sysconfdir}/rpm-postinsts/') + new_pkg
+
+ cmd = self.rpm_cmd + ' -q --scripts --root ' + self.target_rootfs
+ cmd += ' --dbpath=/var/lib/rpm ' + new_pkg
+ cmd += ' | sed -n -e "/^postinstall scriptlet (using .*):$/,/^.* scriptlet (using .*):$/ {/.*/p}"'
+ cmd += ' | sed -e "/postinstall scriptlet (using \(.*\)):$/d"'
+ cmd += ' -e "/^.* scriptlet (using .*):$/d" > %s' % saved_dir
+
+ try:
+ bb.note(cmd)
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True).strip()
+ bb.note(output)
+ os.chmod(saved_dir, 0755)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Invoke save_rpmpostinst failed. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ '''Write common configuration for target usage'''
+ def rpm_setup_smart_target_config(self):
+ bb.utils.remove(os.path.join(self.target_rootfs, 'var/lib/smart'),
+ True)
+
+ self._invoke_smart('config --set rpm-nolinktos=1')
+ self._invoke_smart('config --set rpm-noparentdirs=1')
+ for i in self.d.getVar('BAD_RECOMMENDATIONS', True).split():
+ self._invoke_smart('flag --set ignore-recommends %s' % i)
+ self._invoke_smart('channel --add rpmsys type=rpm-sys -y')
+
+ '''
+ The rpm db lock files were produced after invoking rpm to query on
+ build system, and they caused the rpm on target didn't work, so we
+ need to unlock the rpm db by removing the lock files.
+ '''
+ def unlock_rpm_db(self):
+ # Remove rpm db lock files
+ rpm_db_locks = glob.glob('%s/var/lib/rpm/__db.*' % self.target_rootfs)
+ for f in rpm_db_locks:
+ bb.utils.remove(f, True)
+
+
+class OpkgPM(PackageManager):
+ def __init__(self, d, target_rootfs, config_file, archs, task_name='target'):
+ super(OpkgPM, self).__init__(d)
+
+ self.target_rootfs = target_rootfs
+ self.config_file = config_file
+ self.pkg_archs = archs
+ self.task_name = task_name
+
+ self.deploy_dir = self.d.getVar("DEPLOY_DIR_IPK", True)
+ self.deploy_lock_file = os.path.join(self.deploy_dir, "deploy.lock")
+ self.opkg_cmd = bb.utils.which(os.getenv('PATH'), "opkg")
+ self.opkg_args = "--volatile-cache -f %s -o %s " % (self.config_file, target_rootfs)
+ self.opkg_args += self.d.getVar("OPKG_ARGS", True)
+
+ opkg_lib_dir = self.d.getVar('OPKGLIBDIR', True)
+ if opkg_lib_dir[0] == "/":
+ opkg_lib_dir = opkg_lib_dir[1:]
+
+ self.opkg_dir = os.path.join(target_rootfs, opkg_lib_dir, "opkg")
+
+ bb.utils.mkdirhier(self.opkg_dir)
+
+ self.saved_opkg_dir = self.d.expand('${T}/saved/%s' % self.task_name)
+ if not os.path.exists(self.d.expand('${T}/saved')):
+ bb.utils.mkdirhier(self.d.expand('${T}/saved'))
+
+ if (self.d.getVar('BUILD_IMAGES_FROM_FEEDS', True) or "") != "1":
+ self._create_config()
+ else:
+ self._create_custom_config()
+
+ self.indexer = OpkgIndexer(self.d, self.deploy_dir)
+
+ """
+ This function will change a package's status in /var/lib/opkg/status file.
+ If 'packages' is None then the new_status will be applied to all
+ packages
+ """
+ def mark_packages(self, status_tag, packages=None):
+ status_file = os.path.join(self.opkg_dir, "status")
+
+ with open(status_file, "r") as sf:
+ with open(status_file + ".tmp", "w+") as tmp_sf:
+ if packages is None:
+ tmp_sf.write(re.sub(r"Package: (.*?)\n((?:[^\n]+\n)*?)Status: (.*)(?:unpacked|installed)",
+ r"Package: \1\n\2Status: \3%s" % status_tag,
+ sf.read()))
+ else:
+ if type(packages).__name__ != "list":
+ raise TypeError("'packages' should be a list object")
+
+ status = sf.read()
+ for pkg in packages:
+ status = re.sub(r"Package: %s\n((?:[^\n]+\n)*?)Status: (.*)(?:unpacked|installed)" % pkg,
+ r"Package: %s\n\1Status: \2%s" % (pkg, status_tag),
+ status)
+
+ tmp_sf.write(status)
+
+ os.rename(status_file + ".tmp", status_file)
+
+ def _create_custom_config(self):
+ bb.note("Building from feeds activated!")
+
+ with open(self.config_file, "w+") as config_file:
+ priority = 1
+ for arch in self.pkg_archs.split():
+ config_file.write("arch %s %d\n" % (arch, priority))
+ priority += 5
+
+ for line in (self.d.getVar('IPK_FEED_URIS', True) or "").split():
+ feed_match = re.match("^[ \t]*(.*)##([^ \t]*)[ \t]*$", line)
+
+ if feed_match is not None:
+ feed_name = feed_match.group(1)
+ feed_uri = feed_match.group(2)
+
+ bb.note("Add %s feed with URL %s" % (feed_name, feed_uri))
+
+ config_file.write("src/gz %s %s\n" % (feed_name, feed_uri))
+
+ """
+ Allow to use package deploy directory contents as quick devel-testing
+ feed. This creates individual feed configs for each arch subdir of those
+ specified as compatible for the current machine.
+ NOTE: Development-helper feature, NOT a full-fledged feed.
+ """
+ if (self.d.getVar('FEED_DEPLOYDIR_BASE_URI', True) or "") != "":
+ for arch in self.pkg_archs.split():
+ cfg_file_name = os.path.join(self.target_rootfs,
+ self.d.getVar("sysconfdir", True),
+ "opkg",
+ "local-%s-feed.conf" % arch)
+
+ with open(cfg_file_name, "w+") as cfg_file:
+ cfg_file.write("src/gz local-%s %s/%s" %
+ (arch,
+ self.d.getVar('FEED_DEPLOYDIR_BASE_URI', True),
+ arch))
+
+ def _create_config(self):
+ with open(self.config_file, "w+") as config_file:
+ priority = 1
+ for arch in self.pkg_archs.split():
+ config_file.write("arch %s %d\n" % (arch, priority))
+ priority += 5
+
+ config_file.write("src oe file:%s\n" % self.deploy_dir)
+
+ for arch in self.pkg_archs.split():
+ pkgs_dir = os.path.join(self.deploy_dir, arch)
+ if os.path.isdir(pkgs_dir):
+ config_file.write("src oe-%s file:%s\n" %
+ (arch, pkgs_dir))
+
+ def insert_feeds_uris(self):
+ if self.feed_uris == "":
+ return
+
+ rootfs_config = os.path.join('%s/etc/opkg/base-feeds.conf'
+ % self.target_rootfs)
+
+ with open(rootfs_config, "w+") as config_file:
+ uri_iterator = 0
+ for uri in self.feed_uris.split():
+ full_uri = uri
+ if self.feed_prefix:
+ full_uri = os.path.join(uri, self.feed_prefix)
+
+ for arch in self.pkg_archs.split():
+ if not os.path.exists(os.path.join(self.deploy_dir, arch)):
+ continue
+ bb.note('Note: adding opkg feed url-%s-%d (%s)' %
+ (arch, uri_iterator, full_uri))
+
+ config_file.write("src/gz uri-%s-%d %s/%s\n" %
+ (arch, uri_iterator, full_uri, arch))
+ uri_iterator += 1
+
+ def update(self):
+ self.deploy_dir_lock()
+
+ cmd = "%s %s update" % (self.opkg_cmd, self.opkg_args)
+
+ try:
+ subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ self.deploy_dir_unlock()
+ bb.fatal("Unable to update the package index files. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ self.deploy_dir_unlock()
+
+ def install(self, pkgs, attempt_only=False):
+ if attempt_only and len(pkgs) == 0:
+ return
+
+ cmd = "%s %s install %s" % (self.opkg_cmd, self.opkg_args, ' '.join(pkgs))
+
+ os.environ['D'] = self.target_rootfs
+ os.environ['OFFLINE_ROOT'] = self.target_rootfs
+ os.environ['IPKG_OFFLINE_ROOT'] = self.target_rootfs
+ os.environ['OPKG_OFFLINE_ROOT'] = self.target_rootfs
+ os.environ['INTERCEPT_DIR'] = os.path.join(self.d.getVar('WORKDIR', True),
+ "intercept_scripts")
+ os.environ['NATIVE_ROOT'] = self.d.getVar('STAGING_DIR_NATIVE', True)
+
+ try:
+ bb.note("Installing the following packages: %s" % ' '.join(pkgs))
+ bb.note(cmd)
+ output = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+ bb.note(output)
+ except subprocess.CalledProcessError as e:
+ (bb.fatal, bb.note)[attempt_only]("Unable to install packages. "
+ "Command '%s' returned %d:\n%s" %
+ (cmd, e.returncode, e.output))
+
+ def remove(self, pkgs, with_dependencies=True):
+ if with_dependencies:
+ cmd = "%s %s --force-depends --force-remove --force-removal-of-dependent-packages remove %s" % \
+ (self.opkg_cmd, self.opkg_args, ' '.join(pkgs))
+ else:
+ cmd = "%s %s --force-depends remove %s" % \
+ (self.opkg_cmd, self.opkg_args, ' '.join(pkgs))
+
+ try:
+ bb.note(cmd)
+ output = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+ bb.note(output)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Unable to remove packages. Command '%s' "
+ "returned %d:\n%s" % (e.cmd, e.returncode, e.output))
+
+ def write_index(self):
+ self.deploy_dir_lock()
+
+ result = self.indexer.write_index()
+
+ self.deploy_dir_unlock()
+
+ if result is not None:
+ bb.fatal(result)
+
+ def remove_packaging_data(self):
+ bb.utils.remove(self.opkg_dir, True)
+ # create the directory back, it's needed by PM lock
+ bb.utils.mkdirhier(self.opkg_dir)
+
+ def list_installed(self, format=None):
+ return OpkgPkgsList(self.d, self.target_rootfs, self.config_file).list(format)
+
+ def handle_bad_recommendations(self):
+ bad_recommendations = self.d.getVar("BAD_RECOMMENDATIONS", True) or ""
+ if bad_recommendations.strip() == "":
+ return
+
+ status_file = os.path.join(self.opkg_dir, "status")
+
+ # If status file existed, it means the bad recommendations has already
+ # been handled
+ if os.path.exists(status_file):
+ return
+
+ cmd = "%s %s info " % (self.opkg_cmd, self.opkg_args)
+
+ with open(status_file, "w+") as status:
+ for pkg in bad_recommendations.split():
+ pkg_info = cmd + pkg
+
+ try:
+ output = subprocess.check_output(pkg_info.split(), stderr=subprocess.STDOUT).strip()
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Cannot get package info. Command '%s' "
+ "returned %d:\n%s" % (pkg_info, e.returncode, e.output))
+
+ if output == "":
+ bb.note("Ignored bad recommendation: '%s' is "
+ "not a package" % pkg)
+ continue
+
+ for line in output.split('\n'):
+ if line.startswith("Status:"):
+ status.write("Status: deinstall hold not-installed\n")
+ else:
+ status.write(line + "\n")
+
+ # Append a blank line after each package entry to ensure that it
+ # is separated from the following entry
+ status.write("\n")
+
+ '''
+ The following function dummy installs pkgs and returns the log of output.
+ '''
+ def dummy_install(self, pkgs):
+ if len(pkgs) == 0:
+ return
+
+ # Create an temp dir as opkg root for dummy installation
+ temp_rootfs = self.d.expand('${T}/opkg')
+ temp_opkg_dir = os.path.join(temp_rootfs, 'var/lib/opkg')
+ bb.utils.mkdirhier(temp_opkg_dir)
+
+ opkg_args = "-f %s -o %s " % (self.config_file, temp_rootfs)
+ opkg_args += self.d.getVar("OPKG_ARGS", True)
+
+ cmd = "%s %s update" % (self.opkg_cmd, opkg_args)
+ try:
+ subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Unable to update. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ # Dummy installation
+ cmd = "%s %s --noaction install %s " % (self.opkg_cmd,
+ opkg_args,
+ ' '.join(pkgs))
+ try:
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Unable to dummy install packages. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ bb.utils.remove(temp_rootfs, True)
+
+ return output
+
+ def backup_packaging_data(self):
+ # Save the opkglib for increment ipk image generation
+ if os.path.exists(self.saved_opkg_dir):
+ bb.utils.remove(self.saved_opkg_dir, True)
+ shutil.copytree(self.opkg_dir,
+ self.saved_opkg_dir,
+ symlinks=True)
+
+ def recover_packaging_data(self):
+ # Move the opkglib back
+ if os.path.exists(self.saved_opkg_dir):
+ if os.path.exists(self.opkg_dir):
+ bb.utils.remove(self.opkg_dir, True)
+
+ bb.note('Recover packaging data')
+ shutil.copytree(self.saved_opkg_dir,
+ self.opkg_dir,
+ symlinks=True)
+
+
+class DpkgPM(PackageManager):
+ def __init__(self, d, target_rootfs, archs, base_archs, apt_conf_dir=None):
+ super(DpkgPM, self).__init__(d)
+ self.target_rootfs = target_rootfs
+ self.deploy_dir = self.d.getVar('DEPLOY_DIR_DEB', True)
+ if apt_conf_dir is None:
+ self.apt_conf_dir = self.d.expand("${APTCONF_TARGET}/apt")
+ else:
+ self.apt_conf_dir = apt_conf_dir
+ self.apt_conf_file = os.path.join(self.apt_conf_dir, "apt.conf")
+ self.apt_get_cmd = bb.utils.which(os.getenv('PATH'), "apt-get")
+
+ self.apt_args = d.getVar("APT_ARGS", True)
+
+ self.all_arch_list = archs.split()
+ all_mlb_pkg_arch_list = (self.d.getVar('ALL_MULTILIB_PACKAGE_ARCHS', True) or "").replace('-', '_').split()
+ self.all_arch_list.extend(arch for arch in all_mlb_pkg_arch_list if arch not in self.all_arch_list)
+
+ self._create_configs(archs, base_archs)
+
+ self.indexer = DpkgIndexer(self.d, self.deploy_dir)
+
+ """
+ This function will change a package's status in /var/lib/dpkg/status file.
+ If 'packages' is None then the new_status will be applied to all
+ packages
+ """
+ def mark_packages(self, status_tag, packages=None):
+ status_file = self.target_rootfs + "/var/lib/dpkg/status"
+
+ with open(status_file, "r") as sf:
+ with open(status_file + ".tmp", "w+") as tmp_sf:
+ if packages is None:
+ tmp_sf.write(re.sub(r"Package: (.*?)\n((?:[^\n]+\n)*?)Status: (.*)(?:unpacked|installed)",
+ r"Package: \1\n\2Status: \3%s" % status_tag,
+ sf.read()))
+ else:
+ if type(packages).__name__ != "list":
+ raise TypeError("'packages' should be a list object")
+
+ status = sf.read()
+ for pkg in packages:
+ status = re.sub(r"Package: %s\n((?:[^\n]+\n)*?)Status: (.*)(?:unpacked|installed)" % pkg,
+ r"Package: %s\n\1Status: \2%s" % (pkg, status_tag),
+ status)
+
+ tmp_sf.write(status)
+
+ os.rename(status_file + ".tmp", status_file)
+
+ """
+ Run the pre/post installs for package "package_name". If package_name is
+ None, then run all pre/post install scriptlets.
+ """
+ def run_pre_post_installs(self, package_name=None):
+ info_dir = self.target_rootfs + "/var/lib/dpkg/info"
+ suffixes = [(".preinst", "Preinstall"), (".postinst", "Postinstall")]
+ status_file = self.target_rootfs + "/var/lib/dpkg/status"
+ installed_pkgs = []
+
+ with open(status_file, "r") as status:
+ for line in status.read().split('\n'):
+ m = re.match("^Package: (.*)", line)
+ if m is not None:
+ installed_pkgs.append(m.group(1))
+
+ if package_name is not None and not package_name in installed_pkgs:
+ return
+
+ os.environ['D'] = self.target_rootfs
+ os.environ['OFFLINE_ROOT'] = self.target_rootfs
+ os.environ['IPKG_OFFLINE_ROOT'] = self.target_rootfs
+ os.environ['OPKG_OFFLINE_ROOT'] = self.target_rootfs
+ os.environ['INTERCEPT_DIR'] = os.path.join(self.d.getVar('WORKDIR', True),
+ "intercept_scripts")
+ os.environ['NATIVE_ROOT'] = self.d.getVar('STAGING_DIR_NATIVE', True)
+
+ failed_pkgs = []
+ for pkg_name in installed_pkgs:
+ for suffix in suffixes:
+ p_full = os.path.join(info_dir, pkg_name + suffix[0])
+ if os.path.exists(p_full):
+ try:
+ bb.note("Executing %s for package: %s ..." %
+ (suffix[1].lower(), pkg_name))
+ subprocess.check_output(p_full, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ bb.note("%s for package %s failed with %d:\n%s" %
+ (suffix[1], pkg_name, e.returncode, e.output))
+ failed_pkgs.append(pkg_name)
+ break
+
+ if len(failed_pkgs):
+ self.mark_packages("unpacked", failed_pkgs)
+
+ def update(self):
+ os.environ['APT_CONFIG'] = self.apt_conf_file
+
+ self.deploy_dir_lock()
+
+ cmd = "%s update" % self.apt_get_cmd
+
+ try:
+ subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Unable to update the package index files. Command '%s' "
+ "returned %d:\n%s" % (e.cmd, e.returncode, e.output))
+
+ self.deploy_dir_unlock()
+
+ def install(self, pkgs, attempt_only=False):
+ if attempt_only and len(pkgs) == 0:
+ return
+
+ os.environ['APT_CONFIG'] = self.apt_conf_file
+
+ cmd = "%s %s install --force-yes --allow-unauthenticated %s" % \
+ (self.apt_get_cmd, self.apt_args, ' '.join(pkgs))
+
+ try:
+ bb.note("Installing the following packages: %s" % ' '.join(pkgs))
+ subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ (bb.fatal, bb.note)[attempt_only]("Unable to install packages. "
+ "Command '%s' returned %d:\n%s" %
+ (cmd, e.returncode, e.output))
+
+ # rename *.dpkg-new files/dirs
+ for root, dirs, files in os.walk(self.target_rootfs):
+ for dir in dirs:
+ new_dir = re.sub("\.dpkg-new", "", dir)
+ if dir != new_dir:
+ os.rename(os.path.join(root, dir),
+ os.path.join(root, new_dir))
+
+ for file in files:
+ new_file = re.sub("\.dpkg-new", "", file)
+ if file != new_file:
+ os.rename(os.path.join(root, file),
+ os.path.join(root, new_file))
+
+
+ def remove(self, pkgs, with_dependencies=True):
+ if with_dependencies:
+ os.environ['APT_CONFIG'] = self.apt_conf_file
+ cmd = "%s purge %s" % (self.apt_get_cmd, ' '.join(pkgs))
+ else:
+ cmd = "%s --admindir=%s/var/lib/dpkg --instdir=%s" \
+ " -P --force-depends %s" % \
+ (bb.utils.which(os.getenv('PATH'), "dpkg"),
+ self.target_rootfs, self.target_rootfs, ' '.join(pkgs))
+
+ try:
+ subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Unable to remove packages. Command '%s' "
+ "returned %d:\n%s" % (e.cmd, e.returncode, e.output))
+
+ def write_index(self):
+ self.deploy_dir_lock()
+
+ result = self.indexer.write_index()
+
+ self.deploy_dir_unlock()
+
+ if result is not None:
+ bb.fatal(result)
+
+ def insert_feeds_uris(self):
+ if self.feed_uris == "":
+ return
+
+ sources_conf = os.path.join("%s/etc/apt/sources.list"
+ % self.target_rootfs)
+ arch_list = []
+
+ for arch in self.all_arch_list:
+ if not os.path.exists(os.path.join(self.deploy_dir, arch)):
+ continue
+ arch_list.append(arch)
+
+ with open(sources_conf, "w+") as sources_file:
+ for uri in self.feed_uris.split():
+ full_uri = uri
+ if self.feed_prefix:
+ full_uri = os.path.join(uri, self.feed_prefix)
+ for arch in arch_list:
+ bb.note('Note: adding dpkg channel at (%s)' % uri)
+ sources_file.write("deb %s/%s ./\n" %
+ (full_uri, arch))
+
+ def _create_configs(self, archs, base_archs):
+ base_archs = re.sub("_", "-", base_archs)
+
+ if os.path.exists(self.apt_conf_dir):
+ bb.utils.remove(self.apt_conf_dir, True)
+
+ bb.utils.mkdirhier(self.apt_conf_dir)
+ bb.utils.mkdirhier(self.apt_conf_dir + "/lists/partial/")
+ bb.utils.mkdirhier(self.apt_conf_dir + "/apt.conf.d/")
+
+ arch_list = []
+ for arch in self.all_arch_list:
+ if not os.path.exists(os.path.join(self.deploy_dir, arch)):
+ continue
+ arch_list.append(arch)
+
+ with open(os.path.join(self.apt_conf_dir, "preferences"), "w+") as prefs_file:
+ priority = 801
+ for arch in arch_list:
+ prefs_file.write(
+ "Package: *\n"
+ "Pin: release l=%s\n"
+ "Pin-Priority: %d\n\n" % (arch, priority))
+
+ priority += 5
+
+ pkg_exclude = self.d.getVar('PACKAGE_EXCLUDE', True) or ""
+ for pkg in pkg_exclude.split():
+ prefs_file.write(
+ "Package: %s\n"
+ "Pin: release *\n"
+ "Pin-Priority: -1\n\n" % pkg)
+
+ arch_list.reverse()
+
+ with open(os.path.join(self.apt_conf_dir, "sources.list"), "w+") as sources_file:
+ for arch in arch_list:
+ sources_file.write("deb file:%s/ ./\n" %
+ os.path.join(self.deploy_dir, arch))
+
+ base_arch_list = base_archs.split()
+ multilib_variants = self.d.getVar("MULTILIB_VARIANTS", True);
+ for variant in multilib_variants.split():
+ if variant == "lib32":
+ base_arch_list.append("i386")
+ elif variant == "lib64":
+ base_arch_list.append("amd64")
+
+ with open(self.apt_conf_file, "w+") as apt_conf:
+ with open(self.d.expand("${STAGING_ETCDIR_NATIVE}/apt/apt.conf.sample")) as apt_conf_sample:
+ for line in apt_conf_sample.read().split("\n"):
+ match_arch = re.match(" Architecture \".*\";$", line)
+ architectures = ""
+ if match_arch:
+ for base_arch in base_arch_list:
+ architectures += "\"%s\";" % base_arch
+ apt_conf.write(" Architectures {%s};\n" % architectures);
+ apt_conf.write(" Architecture \"%s\";\n" % base_archs)
+ else:
+ line = re.sub("#ROOTFS#", self.target_rootfs, line)
+ line = re.sub("#APTCONF#", self.apt_conf_dir, line)
+ apt_conf.write(line + "\n")
+
+ target_dpkg_dir = "%s/var/lib/dpkg" % self.target_rootfs
+ bb.utils.mkdirhier(os.path.join(target_dpkg_dir, "info"))
+
+ bb.utils.mkdirhier(os.path.join(target_dpkg_dir, "updates"))
+
+ if not os.path.exists(os.path.join(target_dpkg_dir, "status")):
+ open(os.path.join(target_dpkg_dir, "status"), "w+").close()
+ if not os.path.exists(os.path.join(target_dpkg_dir, "available")):
+ open(os.path.join(target_dpkg_dir, "available"), "w+").close()
+
+ def remove_packaging_data(self):
+ bb.utils.remove(os.path.join(self.target_rootfs,
+ self.d.getVar('opkglibdir', True)), True)
+ bb.utils.remove(self.target_rootfs + "/var/lib/dpkg/", True)
+
+ def fix_broken_dependencies(self):
+ os.environ['APT_CONFIG'] = self.apt_conf_file
+
+ cmd = "%s %s -f install" % (self.apt_get_cmd, self.apt_args)
+
+ try:
+ subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Cannot fix broken dependencies. Command '%s' "
+ "returned %d:\n%s" % (cmd, e.returncode, e.output))
+
+ def list_installed(self, format=None):
+ return DpkgPkgsList(self.d, self.target_rootfs).list()
+
+
+def generate_index_files(d):
+ classes = d.getVar('PACKAGE_CLASSES', True).replace("package_", "").split()
+
+ indexer_map = {
+ "rpm": (RpmIndexer, d.getVar('DEPLOY_DIR_RPM', True)),
+ "ipk": (OpkgIndexer, d.getVar('DEPLOY_DIR_IPK', True)),
+ "deb": (DpkgIndexer, d.getVar('DEPLOY_DIR_DEB', True))
+ }
+
+ result = None
+
+ for pkg_class in classes:
+ if not pkg_class in indexer_map:
+ continue
+
+ if os.path.exists(indexer_map[pkg_class][1]):
+ result = indexer_map[pkg_class][0](d, indexer_map[pkg_class][1]).write_index()
+
+ if result is not None:
+ bb.fatal(result)
+
+if __name__ == "__main__":
+ """
+ We should be able to run this as a standalone script, from outside bitbake
+ environment.
+ """
+ """
+ TBD
+ """
diff --git a/meta/lib/oe/packagedata.py b/meta/lib/oe/packagedata.py
new file mode 100644
index 0000000..cd5f044
--- /dev/null
+++ b/meta/lib/oe/packagedata.py
@@ -0,0 +1,94 @@
+import codecs
+
+def packaged(pkg, d):
+ return os.access(get_subpkgedata_fn(pkg, d) + '.packaged', os.R_OK)
+
+def read_pkgdatafile(fn):
+ pkgdata = {}
+
+ def decode(str):
+ c = codecs.getdecoder("string_escape")
+ return c(str)[0]
+
+ if os.access(fn, os.R_OK):
+ import re
+ f = open(fn, 'r')
+ lines = f.readlines()
+ f.close()
+ r = re.compile("([^:]+):\s*(.*)")
+ for l in lines:
+ m = r.match(l)
+ if m:
+ pkgdata[m.group(1)] = decode(m.group(2))
+
+ return pkgdata
+
+def get_subpkgedata_fn(pkg, d):
+ return d.expand('${PKGDATA_DIR}/runtime/%s' % pkg)
+
+def has_subpkgdata(pkg, d):
+ return os.access(get_subpkgedata_fn(pkg, d), os.R_OK)
+
+def read_subpkgdata(pkg, d):
+ return read_pkgdatafile(get_subpkgedata_fn(pkg, d))
+
+def has_pkgdata(pn, d):
+ fn = d.expand('${PKGDATA_DIR}/%s' % pn)
+ return os.access(fn, os.R_OK)
+
+def read_pkgdata(pn, d):
+ fn = d.expand('${PKGDATA_DIR}/%s' % pn)
+ return read_pkgdatafile(fn)
+
+#
+# Collapse FOO_pkg variables into FOO
+#
+def read_subpkgdata_dict(pkg, d):
+ ret = {}
+ subd = read_pkgdatafile(get_subpkgedata_fn(pkg, d))
+ for var in subd:
+ newvar = var.replace("_" + pkg, "")
+ if newvar == var and var + "_" + pkg in subd:
+ continue
+ ret[newvar] = subd[var]
+ return ret
+
+def _pkgmap(d):
+ """Return a dictionary mapping package to recipe name."""
+
+ pkgdatadir = d.getVar("PKGDATA_DIR", True)
+
+ pkgmap = {}
+ try:
+ files = os.listdir(pkgdatadir)
+ except OSError:
+ bb.warn("No files in %s?" % pkgdatadir)
+ files = []
+
+ for pn in filter(lambda f: not os.path.isdir(os.path.join(pkgdatadir, f)), files):
+ try:
+ pkgdata = read_pkgdatafile(os.path.join(pkgdatadir, pn))
+ except OSError:
+ continue
+
+ packages = pkgdata.get("PACKAGES") or ""
+ for pkg in packages.split():
+ pkgmap[pkg] = pn
+
+ return pkgmap
+
+def pkgmap(d):
+ """Return a dictionary mapping package to recipe name.
+ Cache the mapping in the metadata"""
+
+ pkgmap_data = d.getVar("__pkgmap_data", False)
+ if pkgmap_data is None:
+ pkgmap_data = _pkgmap(d)
+ d.setVar("__pkgmap_data", pkgmap_data)
+
+ return pkgmap_data
+
+def recipename(pkg, d):
+ """Return the recipe name for the given binary package name."""
+
+ return pkgmap(d).get(pkg)
diff --git a/meta/lib/oe/packagegroup.py b/meta/lib/oe/packagegroup.py
new file mode 100644
index 0000000..12eb421
--- /dev/null
+++ b/meta/lib/oe/packagegroup.py
@@ -0,0 +1,36 @@
+import itertools
+
+def is_optional(feature, d):
+ packages = d.getVar("FEATURE_PACKAGES_%s" % feature, True)
+ if packages:
+ return bool(d.getVarFlag("FEATURE_PACKAGES_%s" % feature, "optional"))
+ else:
+ return bool(d.getVarFlag("PACKAGE_GROUP_%s" % feature, "optional"))
+
+def packages(features, d):
+ for feature in features:
+ packages = d.getVar("FEATURE_PACKAGES_%s" % feature, True)
+ if not packages:
+ packages = d.getVar("PACKAGE_GROUP_%s" % feature, True)
+ for pkg in (packages or "").split():
+ yield pkg
+
+def required_packages(features, d):
+ req = filter(lambda feature: not is_optional(feature, d), features)
+ return packages(req, d)
+
+def optional_packages(features, d):
+ opt = filter(lambda feature: is_optional(feature, d), features)
+ return packages(opt, d)
+
+def active_packages(features, d):
+ return itertools.chain(required_packages(features, d),
+ optional_packages(features, d))
+
+def active_recipes(features, d):
+ import oe.packagedata
+
+ for pkg in active_packages(features, d):
+ recipe = oe.packagedata.recipename(pkg, d)
+ if recipe:
+ yield recipe
diff --git a/meta/lib/oe/patch.py b/meta/lib/oe/patch.py
new file mode 100644
index 0000000..108bf1d
--- /dev/null
+++ b/meta/lib/oe/patch.py
@@ -0,0 +1,659 @@
+import oe.path
+
+class NotFoundError(bb.BBHandledException):
+ def __init__(self, path):
+ self.path = path
+
+ def __str__(self):
+ return "Error: %s not found." % self.path
+
+class CmdError(bb.BBHandledException):
+ def __init__(self, exitstatus, output):
+ self.status = exitstatus
+ self.output = output
+
+ def __str__(self):
+ return "Command Error: exit status: %d Output:\n%s" % (self.status, self.output)
+
+
+def runcmd(args, dir = None):
+ import pipes
+
+ if dir:
+ olddir = os.path.abspath(os.curdir)
+ if not os.path.exists(dir):
+ raise NotFoundError(dir)
+ os.chdir(dir)
+ # print("cwd: %s -> %s" % (olddir, dir))
+
+ try:
+ args = [ pipes.quote(str(arg)) for arg in args ]
+ cmd = " ".join(args)
+ # print("cmd: %s" % cmd)
+ (exitstatus, output) = oe.utils.getstatusoutput(cmd)
+ if exitstatus != 0:
+ raise CmdError(exitstatus >> 8, output)
+ return output
+
+ finally:
+ if dir:
+ os.chdir(olddir)
+
+class PatchError(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
+ def __str__(self):
+ return "Patch Error: %s" % self.msg
+
+class PatchSet(object):
+ defaults = {
+ "strippath": 1
+ }
+
+ def __init__(self, dir, d):
+ self.dir = dir
+ self.d = d
+ self.patches = []
+ self._current = None
+
+ def current(self):
+ return self._current
+
+ def Clean(self):
+ """
+ Clean out the patch set. Generally includes unapplying all
+ patches and wiping out all associated metadata.
+ """
+ raise NotImplementedError()
+
+ def Import(self, patch, force):
+ if not patch.get("file"):
+ if not patch.get("remote"):
+ raise PatchError("Patch file must be specified in patch import.")
+ else:
+ patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
+
+ for param in PatchSet.defaults:
+ if not patch.get(param):
+ patch[param] = PatchSet.defaults[param]
+
+ if patch.get("remote"):
+ patch["file"] = bb.data.expand(bb.fetch2.localpath(patch["remote"], self.d), self.d)
+
+ patch["filemd5"] = bb.utils.md5_file(patch["file"])
+
+ def Push(self, force):
+ raise NotImplementedError()
+
+ def Pop(self, force):
+ raise NotImplementedError()
+
+ def Refresh(self, remote = None, all = None):
+ raise NotImplementedError()
+
+ @staticmethod
+ def getPatchedFiles(patchfile, striplevel, srcdir=None):
+ """
+ Read a patch file and determine which files it will modify.
+ Params:
+ patchfile: the patch file to read
+ striplevel: the strip level at which the patch is going to be applied
+ srcdir: optional path to join onto the patched file paths
+ Returns:
+ A list of tuples of file path and change mode ('A' for add,
+ 'D' for delete or 'M' for modify)
+ """
+
+ def patchedpath(patchline):
+ filepth = patchline.split()[1]
+ if filepth.endswith('/dev/null'):
+ return '/dev/null'
+ filesplit = filepth.split(os.sep)
+ if striplevel > len(filesplit):
+ bb.error('Patch %s has invalid strip level %d' % (patchfile, striplevel))
+ return None
+ return os.sep.join(filesplit[striplevel:])
+
+ copiedmode = False
+ filelist = []
+ with open(patchfile) as f:
+ for line in f:
+ if line.startswith('--- '):
+ patchpth = patchedpath(line)
+ if not patchpth:
+ break
+ if copiedmode:
+ addedfile = patchpth
+ else:
+ removedfile = patchpth
+ elif line.startswith('+++ '):
+ addedfile = patchedpath(line)
+ if not addedfile:
+ break
+ elif line.startswith('*** '):
+ copiedmode = True
+ removedfile = patchedpath(line)
+ if not removedfile:
+ break
+ else:
+ removedfile = None
+ addedfile = None
+
+ if addedfile and removedfile:
+ if removedfile == '/dev/null':
+ mode = 'A'
+ elif addedfile == '/dev/null':
+ mode = 'D'
+ else:
+ mode = 'M'
+ if srcdir:
+ fullpath = os.path.abspath(os.path.join(srcdir, addedfile))
+ else:
+ fullpath = addedfile
+ filelist.append((fullpath, mode))
+
+ return filelist
+
+
+class PatchTree(PatchSet):
+ def __init__(self, dir, d):
+ PatchSet.__init__(self, dir, d)
+ self.patchdir = os.path.join(self.dir, 'patches')
+ self.seriespath = os.path.join(self.dir, 'patches', 'series')
+ bb.utils.mkdirhier(self.patchdir)
+
+ def _appendPatchFile(self, patch, strippath):
+ with open(self.seriespath, 'a') as f:
+ f.write(os.path.basename(patch) + "," + strippath + "\n")
+ shellcmd = ["cat", patch, ">" , self.patchdir + "/" + os.path.basename(patch)]
+ runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
+
+ def _removePatch(self, p):
+ patch = {}
+ patch['file'] = p.split(",")[0]
+ patch['strippath'] = p.split(",")[1]
+ self._applypatch(patch, False, True)
+
+ def _removePatchFile(self, all = False):
+ if not os.path.exists(self.seriespath):
+ return
+ with open(self.seriespath, 'r+') as f:
+ patches = f.readlines()
+ if all:
+ for p in reversed(patches):
+ self._removePatch(os.path.join(self.patchdir, p.strip()))
+ patches = []
+ else:
+ self._removePatch(os.path.join(self.patchdir, patches[-1].strip()))
+ patches.pop()
+ with open(self.seriespath, 'w') as f:
+ for p in patches:
+ f.write(p)
+
+ def Import(self, patch, force = None):
+ """"""
+ PatchSet.Import(self, patch, force)
+
+ if self._current is not None:
+ i = self._current + 1
+ else:
+ i = 0
+ self.patches.insert(i, patch)
+
+ def _applypatch(self, patch, force = False, reverse = False, run = True):
+ shellcmd = ["cat", patch['file'], "|", "patch", "-p", patch['strippath']]
+ if reverse:
+ shellcmd.append('-R')
+
+ if not run:
+ return "sh" + "-c" + " ".join(shellcmd)
+
+ if not force:
+ shellcmd.append('--dry-run')
+
+ output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
+
+ if force:
+ return
+
+ shellcmd.pop(len(shellcmd) - 1)
+ output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
+
+ if not reverse:
+ self._appendPatchFile(patch['file'], patch['strippath'])
+
+ return output
+
+ def Push(self, force = False, all = False, run = True):
+ bb.note("self._current is %s" % self._current)
+ bb.note("patches is %s" % self.patches)
+ if all:
+ for i in self.patches:
+ bb.note("applying patch %s" % i)
+ self._applypatch(i, force)
+ self._current = i
+ else:
+ if self._current is not None:
+ next = self._current + 1
+ else:
+ next = 0
+
+ bb.note("applying patch %s" % self.patches[next])
+ ret = self._applypatch(self.patches[next], force)
+
+ self._current = next
+ return ret
+
+ def Pop(self, force = None, all = None):
+ if all:
+ self._removePatchFile(True)
+ self._current = None
+ else:
+ self._removePatchFile(False)
+
+ if self._current == 0:
+ self._current = None
+
+ if self._current is not None:
+ self._current = self._current - 1
+
+ def Clean(self):
+ """"""
+ self.Pop(all=True)
+
+class GitApplyTree(PatchTree):
+ patch_line_prefix = '%% original patch'
+
+ def __init__(self, dir, d):
+ PatchTree.__init__(self, dir, d)
+
+ @staticmethod
+ def extractPatchHeader(patchfile):
+ """
+ Extract just the header lines from the top of a patch file
+ """
+ lines = []
+ with open(patchfile, 'r') as f:
+ for line in f.readlines():
+ if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'):
+ break
+ lines.append(line)
+ return lines
+
+ @staticmethod
+ def prepareCommit(patchfile):
+ """
+ Prepare a git commit command line based on the header from a patch file
+ (typically this is useful for patches that cannot be applied with "git am" due to formatting)
+ """
+ import tempfile
+ import re
+ author_re = re.compile('[\S ]+ <\S+@\S+\.\S+>')
+ # Process patch header and extract useful information
+ lines = GitApplyTree.extractPatchHeader(patchfile)
+ outlines = []
+ author = None
+ date = None
+ for line in lines:
+ if line.startswith('Subject: '):
+ subject = line.split(':', 1)[1]
+ # Remove any [PATCH][oe-core] etc.
+ subject = re.sub(r'\[.+?\]\s*', '', subject)
+ outlines.insert(0, '%s\n\n' % subject.strip())
+ continue
+ if line.startswith('From: ') or line.startswith('Author: '):
+ authorval = line.split(':', 1)[1].strip().replace('"', '')
+ # git is fussy about author formatting i.e. it must be Name <email@domain>
+ if author_re.match(authorval):
+ author = authorval
+ continue
+ if line.startswith('Date: '):
+ if date is None:
+ dateval = line.split(':', 1)[1].strip()
+ # Very crude check for date format, since git will blow up if it's not in the right
+ # format. Without e.g. a python-dateutils dependency we can't do a whole lot more
+ if len(dateval) > 12:
+ date = dateval
+ continue
+ if line.startswith('Signed-off-by: '):
+ authorval = line.split(':', 1)[1].strip().replace('"', '')
+ # git is fussy about author formatting i.e. it must be Name <email@domain>
+ if author_re.match(authorval):
+ author = authorval
+ outlines.append(line)
+ # Write out commit message to a file
+ with tempfile.NamedTemporaryFile('w', delete=False) as tf:
+ tmpfile = tf.name
+ for line in outlines:
+ tf.write(line)
+ # Prepare git command
+ cmd = ["git", "commit", "-F", tmpfile]
+ # git doesn't like plain email addresses as authors
+ if author and '<' in author:
+ cmd.append('--author="%s"' % author)
+ if date:
+ cmd.append('--date="%s"' % date)
+ return (tmpfile, cmd)
+
+ @staticmethod
+ def extractPatches(tree, startcommit, outdir):
+ import tempfile
+ import shutil
+ tempdir = tempfile.mkdtemp(prefix='oepatch')
+ try:
+ shellcmd = ["git", "format-patch", startcommit, "-o", tempdir]
+ out = runcmd(["sh", "-c", " ".join(shellcmd)], tree)
+ if out:
+ for srcfile in out.split():
+ patchlines = []
+ outfile = None
+ with open(srcfile, 'r') as f:
+ for line in f:
+ if line.startswith(GitApplyTree.patch_line_prefix):
+ outfile = line.split()[-1].strip()
+ continue
+ patchlines.append(line)
+ if not outfile:
+ outfile = os.path.basename(srcfile)
+ with open(os.path.join(outdir, outfile), 'w') as of:
+ for line in patchlines:
+ of.write(line)
+ finally:
+ shutil.rmtree(tempdir)
+
+ def _applypatch(self, patch, force = False, reverse = False, run = True):
+ import shutil
+
+ def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
+ if reverse:
+ shellcmd.append('-R')
+
+ shellcmd.append(patch['file'])
+
+ if not run:
+ return "sh" + "-c" + " ".join(shellcmd)
+
+ return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
+
+ # Add hooks which add a pointer to the original patch file name in the commit message
+ reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
+ if not reporoot:
+ raise Exception("Cannot get repository root for directory %s" % self.dir)
+ commithook = os.path.join(reporoot, '.git', 'hooks', 'commit-msg')
+ commithook_backup = commithook + '.devtool-orig'
+ applyhook = os.path.join(reporoot, '.git', 'hooks', 'applypatch-msg')
+ applyhook_backup = applyhook + '.devtool-orig'
+ if os.path.exists(commithook):
+ shutil.move(commithook, commithook_backup)
+ if os.path.exists(applyhook):
+ shutil.move(applyhook, applyhook_backup)
+ with open(commithook, 'w') as f:
+ # NOTE: the formatting here is significant; if you change it you'll also need to
+ # change other places which read it back
+ f.write('echo >> $1\n')
+ f.write('echo "%s: $PATCHFILE" >> $1\n' % GitApplyTree.patch_line_prefix)
+ os.chmod(commithook, 0755)
+ shutil.copy2(commithook, applyhook)
+ try:
+ patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
+ try:
+ shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot, "am", "-3", "--keep-cr", "-p%s" % patch['strippath']]
+ return _applypatchhelper(shellcmd, patch, force, reverse, run)
+ except CmdError:
+ # Need to abort the git am, or we'll still be within it at the end
+ try:
+ shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"]
+ runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
+ except CmdError:
+ pass
+ # Fall back to git apply
+ shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']]
+ try:
+ output = _applypatchhelper(shellcmd, patch, force, reverse, run)
+ except CmdError:
+ # Fall back to patch
+ output = PatchTree._applypatch(self, patch, force, reverse, run)
+ # Add all files
+ shellcmd = ["git", "add", "-f", "-A", "."]
+ output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
+ # Exclude the patches directory
+ shellcmd = ["git", "reset", "HEAD", self.patchdir]
+ output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
+ # Commit the result
+ (tmpfile, shellcmd) = self.prepareCommit(patch['file'])
+ try:
+ shellcmd.insert(0, patchfilevar)
+ output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
+ finally:
+ os.remove(tmpfile)
+ return output
+ finally:
+ os.remove(commithook)
+ os.remove(applyhook)
+ if os.path.exists(commithook_backup):
+ shutil.move(commithook_backup, commithook)
+ if os.path.exists(applyhook_backup):
+ shutil.move(applyhook_backup, applyhook)
+
+
+class QuiltTree(PatchSet):
+ def _runcmd(self, args, run = True):
+ quiltrc = self.d.getVar('QUILTRCFILE', True)
+ if not run:
+ return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
+ runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
+
+ def _quiltpatchpath(self, file):
+ return os.path.join(self.dir, "patches", os.path.basename(file))
+
+
+ def __init__(self, dir, d):
+ PatchSet.__init__(self, dir, d)
+ self.initialized = False
+ p = os.path.join(self.dir, 'patches')
+ if not os.path.exists(p):
+ os.makedirs(p)
+
+ def Clean(self):
+ try:
+ self._runcmd(["pop", "-a", "-f"])
+ oe.path.remove(os.path.join(self.dir, "patches","series"))
+ except Exception:
+ pass
+ self.initialized = True
+
+ def InitFromDir(self):
+ # read series -> self.patches
+ seriespath = os.path.join(self.dir, 'patches', 'series')
+ if not os.path.exists(self.dir):
+ raise NotFoundError(self.dir)
+ if os.path.exists(seriespath):
+ with open(seriespath, 'r') as f:
+ for line in f.readlines():
+ patch = {}
+ parts = line.strip().split()
+ patch["quiltfile"] = self._quiltpatchpath(parts[0])
+ patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
+ if len(parts) > 1:
+ patch["strippath"] = parts[1][2:]
+ self.patches.append(patch)
+
+ # determine which patches are applied -> self._current
+ try:
+ output = runcmd(["quilt", "applied"], self.dir)
+ except CmdError:
+ import sys
+ if sys.exc_value.output.strip() == "No patches applied":
+ return
+ else:
+ raise
+ output = [val for val in output.split('\n') if not val.startswith('#')]
+ for patch in self.patches:
+ if os.path.basename(patch["quiltfile"]) == output[-1]:
+ self._current = self.patches.index(patch)
+ self.initialized = True
+
+ def Import(self, patch, force = None):
+ if not self.initialized:
+ self.InitFromDir()
+ PatchSet.Import(self, patch, force)
+ oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
+ with open(os.path.join(self.dir, "patches", "series"), "a") as f:
+ f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n")
+ patch["quiltfile"] = self._quiltpatchpath(patch["file"])
+ patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
+
+ # TODO: determine if the file being imported:
+ # 1) is already imported, and is the same
+ # 2) is already imported, but differs
+
+ self.patches.insert(self._current or 0, patch)
+
+
+ def Push(self, force = False, all = False, run = True):
+ # quilt push [-f]
+
+ args = ["push"]
+ if force:
+ args.append("-f")
+ if all:
+ args.append("-a")
+ if not run:
+ return self._runcmd(args, run)
+
+ self._runcmd(args)
+
+ if self._current is not None:
+ self._current = self._current + 1
+ else:
+ self._current = 0
+
+ def Pop(self, force = None, all = None):
+ # quilt pop [-f]
+ args = ["pop"]
+ if force:
+ args.append("-f")
+ if all:
+ args.append("-a")
+
+ self._runcmd(args)
+
+ if self._current == 0:
+ self._current = None
+
+ if self._current is not None:
+ self._current = self._current - 1
+
+ def Refresh(self, **kwargs):
+ if kwargs.get("remote"):
+ patch = self.patches[kwargs["patch"]]
+ if not patch:
+ raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
+ (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
+ if type == "file":
+ import shutil
+ if not patch.get("file") and patch.get("remote"):
+ patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
+
+ shutil.copyfile(patch["quiltfile"], patch["file"])
+ else:
+ raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
+ else:
+ # quilt refresh
+ args = ["refresh"]
+ if kwargs.get("quiltfile"):
+ args.append(os.path.basename(kwargs["quiltfile"]))
+ elif kwargs.get("patch"):
+ args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
+ self._runcmd(args)
+
+class Resolver(object):
+ def __init__(self, patchset, terminal):
+ raise NotImplementedError()
+
+ def Resolve(self):
+ raise NotImplementedError()
+
+ def Revert(self):
+ raise NotImplementedError()
+
+ def Finalize(self):
+ raise NotImplementedError()
+
+class NOOPResolver(Resolver):
+ def __init__(self, patchset, terminal):
+ self.patchset = patchset
+ self.terminal = terminal
+
+ def Resolve(self):
+ olddir = os.path.abspath(os.curdir)
+ os.chdir(self.patchset.dir)
+ try:
+ self.patchset.Push()
+ except Exception:
+ import sys
+ os.chdir(olddir)
+ raise
+
+# Patch resolver which relies on the user doing all the work involved in the
+# resolution, with the exception of refreshing the remote copy of the patch
+# files (the urls).
+class UserResolver(Resolver):
+ def __init__(self, patchset, terminal):
+ self.patchset = patchset
+ self.terminal = terminal
+
+ # Force a push in the patchset, then drop to a shell for the user to
+ # resolve any rejected hunks
+ def Resolve(self):
+ olddir = os.path.abspath(os.curdir)
+ os.chdir(self.patchset.dir)
+ try:
+ self.patchset.Push(False)
+ except CmdError as v:
+ # Patch application failed
+ patchcmd = self.patchset.Push(True, False, False)
+
+ t = self.patchset.d.getVar('T', True)
+ if not t:
+ bb.msg.fatal("Build", "T not set")
+ bb.utils.mkdirhier(t)
+ import random
+ rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
+ with open(rcfile, "w") as f:
+ f.write("echo '*** Manual patch resolution mode ***'\n")
+ f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
+ f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
+ f.write("echo ''\n")
+ f.write(" ".join(patchcmd) + "\n")
+ os.chmod(rcfile, 0775)
+
+ self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
+
+ # Construct a new PatchSet after the user's changes, compare the
+ # sets, checking patches for modifications, and doing a remote
+ # refresh on each.
+ oldpatchset = self.patchset
+ self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
+
+ for patch in self.patchset.patches:
+ oldpatch = None
+ for opatch in oldpatchset.patches:
+ if opatch["quiltfile"] == patch["quiltfile"]:
+ oldpatch = opatch
+
+ if oldpatch:
+ patch["remote"] = oldpatch["remote"]
+ if patch["quiltfile"] == oldpatch["quiltfile"]:
+ if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
+ bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
+ # user change? remote refresh
+ self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
+ else:
+ # User did not fix the problem. Abort.
+ raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
+ except Exception:
+ os.chdir(olddir)
+ raise
+ os.chdir(olddir)
diff --git a/meta/lib/oe/path.py b/meta/lib/oe/path.py
new file mode 100644
index 0000000..413ebfb
--- /dev/null
+++ b/meta/lib/oe/path.py
@@ -0,0 +1,243 @@
+import errno
+import glob
+import shutil
+import subprocess
+import os.path
+
+def join(*paths):
+ """Like os.path.join but doesn't treat absolute RHS specially"""
+ return os.path.normpath("/".join(paths))
+
+def relative(src, dest):
+ """ Return a relative path from src to dest.
+
+ >>> relative("/usr/bin", "/tmp/foo/bar")
+ ../../tmp/foo/bar
+
+ >>> relative("/usr/bin", "/usr/lib")
+ ../lib
+
+ >>> relative("/tmp", "/tmp/foo/bar")
+ foo/bar
+ """
+
+ return os.path.relpath(dest, src)
+
+def make_relative_symlink(path):
+ """ Convert an absolute symlink to a relative one """
+ if not os.path.islink(path):
+ return
+ link = os.readlink(path)
+ if not os.path.isabs(link):
+ return
+
+ # find the common ancestor directory
+ ancestor = path
+ depth = 0
+ while ancestor and not link.startswith(ancestor):
+ ancestor = ancestor.rpartition('/')[0]
+ depth += 1
+
+ if not ancestor:
+ print("make_relative_symlink() Error: unable to find the common ancestor of %s and its target" % path)
+ return
+
+ base = link.partition(ancestor)[2].strip('/')
+ while depth > 1:
+ base = "../" + base
+ depth -= 1
+
+ os.remove(path)
+ os.symlink(base, path)
+
+def format_display(path, metadata):
+ """ Prepare a path for display to the user. """
+ rel = relative(metadata.getVar("TOPDIR", True), path)
+ if len(rel) > len(path):
+ return path
+ else:
+ return rel
+
+def copytree(src, dst):
+ # We could use something like shutil.copytree here but it turns out to
+ # to be slow. It takes twice as long copying to an empty directory.
+ # If dst already has contents performance can be 15 time slower
+ # This way we also preserve hardlinks between files in the tree.
+
+ bb.utils.mkdirhier(dst)
+ cmd = 'tar -cf - -C %s -p . | tar -xf - -C %s' % (src, dst)
+ check_output(cmd, shell=True, stderr=subprocess.STDOUT)
+
+def copyhardlinktree(src, dst):
+ """ Make the hard link when possible, otherwise copy. """
+ bb.utils.mkdirhier(dst)
+ if os.path.isdir(src) and not len(os.listdir(src)):
+ return
+
+ if (os.stat(src).st_dev == os.stat(dst).st_dev):
+ # Need to copy directories only with tar first since cp will error if two
+ # writers try and create a directory at the same time
+ cmd = 'cd %s; find . -type d -print | tar -cf - -C %s -p --files-from - --no-recursion | tar -xf - -C %s' % (src, src, dst)
+ check_output(cmd, shell=True, stderr=subprocess.STDOUT)
+ cmd = 'cd %s; find . -print0 | cpio --null -pdlu %s' % (src, dst)
+ check_output(cmd, shell=True, stderr=subprocess.STDOUT)
+ else:
+ copytree(src, dst)
+
+def remove(path, recurse=True):
+ """Equivalent to rm -f or rm -rf"""
+ for name in glob.glob(path):
+ try:
+ os.unlink(name)
+ except OSError as exc:
+ if recurse and exc.errno == errno.EISDIR:
+ shutil.rmtree(name)
+ elif exc.errno != errno.ENOENT:
+ raise
+
+def symlink(source, destination, force=False):
+ """Create a symbolic link"""
+ try:
+ if force:
+ remove(destination)
+ os.symlink(source, destination)
+ except OSError as e:
+ if e.errno != errno.EEXIST or os.readlink(destination) != source:
+ raise
+
+class CalledProcessError(Exception):
+ def __init__(self, retcode, cmd, output = None):
+ self.retcode = retcode
+ self.cmd = cmd
+ self.output = output
+ def __str__(self):
+ return "Command '%s' returned non-zero exit status %d with output %s" % (self.cmd, self.retcode, self.output)
+
+# Not needed when we move to python 2.7
+def check_output(*popenargs, **kwargs):
+ r"""Run command with arguments and return its output as a byte string.
+
+ If the exit code was non-zero it raises a CalledProcessError. The
+ CalledProcessError object will have the return code in the returncode
+ attribute and output in the output attribute.
+
+ The arguments are the same as for the Popen constructor. Example:
+
+ >>> check_output(["ls", "-l", "/dev/null"])
+ 'crw-rw-rw- 1 root root 1, 3 Oct 18 2007 /dev/null\n'
+
+ The stdout argument is not allowed as it is used internally.
+ To capture standard error in the result, use stderr=STDOUT.
+
+ >>> check_output(["/bin/sh", "-c",
+ ... "ls -l non_existent_file ; exit 0"],
+ ... stderr=STDOUT)
+ 'ls: non_existent_file: No such file or directory\n'
+ """
+ if 'stdout' in kwargs:
+ raise ValueError('stdout argument not allowed, it will be overridden.')
+ process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
+ output, unused_err = process.communicate()
+ retcode = process.poll()
+ if retcode:
+ cmd = kwargs.get("args")
+ if cmd is None:
+ cmd = popenargs[0]
+ raise CalledProcessError(retcode, cmd, output=output)
+ return output
+
+def find(dir, **walkoptions):
+ """ Given a directory, recurses into that directory,
+ returning all files as absolute paths. """
+
+ for root, dirs, files in os.walk(dir, **walkoptions):
+ for file in files:
+ yield os.path.join(root, file)
+
+
+## realpath() related functions
+def __is_path_below(file, root):
+ return (file + os.path.sep).startswith(root)
+
+def __realpath_rel(start, rel_path, root, loop_cnt, assume_dir):
+ """Calculates real path of symlink 'start' + 'rel_path' below
+ 'root'; no part of 'start' below 'root' must contain symlinks. """
+ have_dir = True
+
+ for d in rel_path.split(os.path.sep):
+ if not have_dir and not assume_dir:
+ raise OSError(errno.ENOENT, "no such directory %s" % start)
+
+ if d == os.path.pardir: # '..'
+ if len(start) >= len(root):
+ # do not follow '..' before root
+ start = os.path.dirname(start)
+ else:
+ # emit warning?
+ pass
+ else:
+ (start, have_dir) = __realpath(os.path.join(start, d),
+ root, loop_cnt, assume_dir)
+
+ assert(__is_path_below(start, root))
+
+ return start
+
+def __realpath(file, root, loop_cnt, assume_dir):
+ while os.path.islink(file) and len(file) >= len(root):
+ if loop_cnt == 0:
+ raise OSError(errno.ELOOP, file)
+
+ loop_cnt -= 1
+ target = os.path.normpath(os.readlink(file))
+
+ if not os.path.isabs(target):
+ tdir = os.path.dirname(file)
+ assert(__is_path_below(tdir, root))
+ else:
+ tdir = root
+
+ file = __realpath_rel(tdir, target, root, loop_cnt, assume_dir)
+
+ try:
+ is_dir = os.path.isdir(file)
+ except:
+ is_dir = false
+
+ return (file, is_dir)
+
+def realpath(file, root, use_physdir = True, loop_cnt = 100, assume_dir = False):
+ """ Returns the canonical path of 'file' with assuming a
+ toplevel 'root' directory. When 'use_physdir' is set, all
+ preceding path components of 'file' will be resolved first;
+ this flag should be set unless it is guaranteed that there is
+ no symlink in the path. When 'assume_dir' is not set, missing
+ path components will raise an ENOENT error"""
+
+ root = os.path.normpath(root)
+ file = os.path.normpath(file)
+
+ if not root.endswith(os.path.sep):
+ # letting root end with '/' makes some things easier
+ root = root + os.path.sep
+
+ if not __is_path_below(file, root):
+ raise OSError(errno.EINVAL, "file '%s' is not below root" % file)
+
+ try:
+ if use_physdir:
+ file = __realpath_rel(root, file[(len(root) - 1):], root, loop_cnt, assume_dir)
+ else:
+ file = __realpath(file, root, loop_cnt, assume_dir)[0]
+ except OSError as e:
+ if e.errno == errno.ELOOP:
+ # make ELOOP more readable; without catching it, there will
+ # be printed a backtrace with 100s of OSError exceptions
+ # else
+ raise OSError(errno.ELOOP,
+ "too much recursions while resolving '%s'; loop in '%s'" %
+ (file, e.strerror))
+
+ raise
+
+ return file
diff --git a/meta/lib/oe/prservice.py b/meta/lib/oe/prservice.py
new file mode 100644
index 0000000..b0cbcb1
--- /dev/null
+++ b/meta/lib/oe/prservice.py
@@ -0,0 +1,126 @@
+
+def prserv_make_conn(d, check = False):
+ import prserv.serv
+ host_params = filter(None, (d.getVar("PRSERV_HOST", True) or '').split(':'))
+ try:
+ conn = None
+ conn = prserv.serv.PRServerConnection(host_params[0], int(host_params[1]))
+ if check:
+ if not conn.ping():
+ raise Exception('service not available')
+ d.setVar("__PRSERV_CONN",conn)
+ except Exception, exc:
+ bb.fatal("Connecting to PR service %s:%s failed: %s" % (host_params[0], host_params[1], str(exc)))
+
+ return conn
+
+def prserv_dump_db(d):
+ if not d.getVar('PRSERV_HOST', True):
+ bb.error("Not using network based PR service")
+ return None
+
+ conn = d.getVar("__PRSERV_CONN", True)
+ if conn is None:
+ conn = prserv_make_conn(d)
+ if conn is None:
+ bb.error("Making connection failed to remote PR service")
+ return None
+
+ #dump db
+ opt_version = d.getVar('PRSERV_DUMPOPT_VERSION', True)
+ opt_pkgarch = d.getVar('PRSERV_DUMPOPT_PKGARCH', True)
+ opt_checksum = d.getVar('PRSERV_DUMPOPT_CHECKSUM', True)
+ opt_col = ("1" == d.getVar('PRSERV_DUMPOPT_COL', True))
+ return conn.export(opt_version, opt_pkgarch, opt_checksum, opt_col)
+
+def prserv_import_db(d, filter_version=None, filter_pkgarch=None, filter_checksum=None):
+ if not d.getVar('PRSERV_HOST', True):
+ bb.error("Not using network based PR service")
+ return None
+
+ conn = d.getVar("__PRSERV_CONN", True)
+ if conn is None:
+ conn = prserv_make_conn(d)
+ if conn is None:
+ bb.error("Making connection failed to remote PR service")
+ return None
+ #get the entry values
+ imported = []
+ prefix = "PRAUTO$"
+ for v in d.keys():
+ if v.startswith(prefix):
+ (remain, sep, checksum) = v.rpartition('$')
+ (remain, sep, pkgarch) = remain.rpartition('$')
+ (remain, sep, version) = remain.rpartition('$')
+ if (remain + '$' != prefix) or \
+ (filter_version and filter_version != version) or \
+ (filter_pkgarch and filter_pkgarch != pkgarch) or \
+ (filter_checksum and filter_checksum != checksum):
+ continue
+ try:
+ value = int(d.getVar(remain + '$' + version + '$' + pkgarch + '$' + checksum, True))
+ except BaseException as exc:
+ bb.debug("Not valid value of %s:%s" % (v,str(exc)))
+ continue
+ ret = conn.importone(version,pkgarch,checksum,value)
+ if ret != value:
+ bb.error("importing(%s,%s,%s,%d) failed. DB may have larger value %d" % (version,pkgarch,checksum,value,ret))
+ else:
+ imported.append((version,pkgarch,checksum,value))
+ return imported
+
+def prserv_export_tofile(d, metainfo, datainfo, lockdown, nomax=False):
+ import bb.utils
+ #initilize the output file
+ bb.utils.mkdirhier(d.getVar('PRSERV_DUMPDIR', True))
+ df = d.getVar('PRSERV_DUMPFILE', True)
+ #write data
+ lf = bb.utils.lockfile("%s.lock" % df)
+ f = open(df, "a")
+ if metainfo:
+ #dump column info
+ f.write("#PR_core_ver = \"%s\"\n\n" % metainfo['core_ver']);
+ f.write("#Table: %s\n" % metainfo['tbl_name'])
+ f.write("#Columns:\n")
+ f.write("#name \t type \t notn \t dflt \t pk\n")
+ f.write("#----------\t --------\t --------\t --------\t ----\n")
+ for i in range(len(metainfo['col_info'])):
+ f.write("#%10s\t %8s\t %8s\t %8s\t %4s\n" %
+ (metainfo['col_info'][i]['name'],
+ metainfo['col_info'][i]['type'],
+ metainfo['col_info'][i]['notnull'],
+ metainfo['col_info'][i]['dflt_value'],
+ metainfo['col_info'][i]['pk']))
+ f.write("\n")
+
+ if lockdown:
+ f.write("PRSERV_LOCKDOWN = \"1\"\n\n")
+
+ if datainfo:
+ idx = {}
+ for i in range(len(datainfo)):
+ pkgarch = datainfo[i]['pkgarch']
+ value = datainfo[i]['value']
+ if pkgarch not in idx:
+ idx[pkgarch] = i
+ elif value > datainfo[idx[pkgarch]]['value']:
+ idx[pkgarch] = i
+ f.write("PRAUTO$%s$%s$%s = \"%s\"\n" %
+ (str(datainfo[i]['version']), pkgarch, str(datainfo[i]['checksum']), str(value)))
+ if not nomax:
+ for i in idx:
+ f.write("PRAUTO_%s_%s = \"%s\"\n" % (str(datainfo[idx[i]]['version']),str(datainfo[idx[i]]['pkgarch']),str(datainfo[idx[i]]['value'])))
+ f.close()
+ bb.utils.unlockfile(lf)
+
+def prserv_check_avail(d):
+ host_params = filter(None, (d.getVar("PRSERV_HOST", True) or '').split(':'))
+ try:
+ if len(host_params) != 2:
+ raise TypeError
+ else:
+ int(host_params[1])
+ except TypeError:
+ bb.fatal('Undefined/incorrect PRSERV_HOST value. Format: "host:port"')
+ else:
+ prserv_make_conn(d, True)
diff --git a/meta/lib/oe/qa.py b/meta/lib/oe/qa.py
new file mode 100644
index 0000000..d5cdaa0
--- /dev/null
+++ b/meta/lib/oe/qa.py
@@ -0,0 +1,111 @@
+class ELFFile:
+ EI_NIDENT = 16
+
+ EI_CLASS = 4
+ EI_DATA = 5
+ EI_VERSION = 6
+ EI_OSABI = 7
+ EI_ABIVERSION = 8
+
+ # possible values for EI_CLASS
+ ELFCLASSNONE = 0
+ ELFCLASS32 = 1
+ ELFCLASS64 = 2
+
+ # possible value for EI_VERSION
+ EV_CURRENT = 1
+
+ # possible values for EI_DATA
+ ELFDATANONE = 0
+ ELFDATA2LSB = 1
+ ELFDATA2MSB = 2
+
+ def my_assert(self, expectation, result):
+ if not expectation == result:
+ #print "'%x','%x' %s" % (ord(expectation), ord(result), self.name)
+ raise Exception("This does not work as expected")
+
+ def __init__(self, name, bits = 0):
+ self.name = name
+ self.bits = bits
+ self.objdump_output = {}
+
+ def open(self):
+ self.file = file(self.name, "r")
+ self.data = self.file.read(ELFFile.EI_NIDENT+4)
+
+ self.my_assert(len(self.data), ELFFile.EI_NIDENT+4)
+ self.my_assert(self.data[0], chr(0x7f) )
+ self.my_assert(self.data[1], 'E')
+ self.my_assert(self.data[2], 'L')
+ self.my_assert(self.data[3], 'F')
+ if self.bits == 0:
+ if self.data[ELFFile.EI_CLASS] == chr(ELFFile.ELFCLASS32):
+ self.bits = 32
+ elif self.data[ELFFile.EI_CLASS] == chr(ELFFile.ELFCLASS64):
+ self.bits = 64
+ else:
+ # Not 32-bit or 64.. lets assert
+ raise Exception("ELF but not 32 or 64 bit.")
+ elif self.bits == 32:
+ self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS32))
+ elif self.bits == 64:
+ self.my_assert(self.data[ELFFile.EI_CLASS], chr(ELFFile.ELFCLASS64))
+ else:
+ raise Exception("Must specify unknown, 32 or 64 bit size.")
+ self.my_assert(self.data[ELFFile.EI_VERSION], chr(ELFFile.EV_CURRENT) )
+
+ self.sex = self.data[ELFFile.EI_DATA]
+ if self.sex == chr(ELFFile.ELFDATANONE):
+ raise Exception("self.sex == ELFDATANONE")
+ elif self.sex == chr(ELFFile.ELFDATA2LSB):
+ self.sex = "<"
+ elif self.sex == chr(ELFFile.ELFDATA2MSB):
+ self.sex = ">"
+ else:
+ raise Exception("Unknown self.sex")
+
+ def osAbi(self):
+ return ord(self.data[ELFFile.EI_OSABI])
+
+ def abiVersion(self):
+ return ord(self.data[ELFFile.EI_ABIVERSION])
+
+ def abiSize(self):
+ return self.bits
+
+ def isLittleEndian(self):
+ return self.sex == "<"
+
+ def isBigEngian(self):
+ return self.sex == ">"
+
+ def machine(self):
+ """
+ We know the sex stored in self.sex and we
+ know the position
+ """
+ import struct
+ (a,) = struct.unpack(self.sex+"H", self.data[18:20])
+ return a
+
+ def run_objdump(self, cmd, d):
+ import bb.process
+ import sys
+
+ if cmd in self.objdump_output:
+ return self.objdump_output[cmd]
+
+ objdump = d.getVar('OBJDUMP', True)
+
+ env = os.environ.copy()
+ env["LC_ALL"] = "C"
+ env["PATH"] = d.getVar('PATH', True)
+
+ try:
+ bb.note("%s %s %s" % (objdump, cmd, self.name))
+ self.objdump_output[cmd] = bb.process.run([objdump, cmd, self.name], env=env, shell=False)[0]
+ return self.objdump_output[cmd]
+ except Exception as e:
+ bb.note("%s %s %s failed: %s" % (objdump, cmd, self.name, e))
+ return ""
diff --git a/meta/lib/oe/recipeutils.py b/meta/lib/oe/recipeutils.py
new file mode 100644
index 0000000..d4fa726
--- /dev/null
+++ b/meta/lib/oe/recipeutils.py
@@ -0,0 +1,740 @@
+# Utility functions for reading and modifying recipes
+#
+# Some code borrowed from the OE layer index
+#
+# Copyright (C) 2013-2015 Intel Corporation
+#
+
+import sys
+import os
+import os.path
+import tempfile
+import textwrap
+import difflib
+import utils
+import shutil
+import re
+import fnmatch
+from collections import OrderedDict, defaultdict
+
+
+# Help us to find places to insert values
+recipe_progression = ['SUMMARY', 'DESCRIPTION', 'HOMEPAGE', 'BUGTRACKER', 'SECTION', 'LICENSE', 'LIC_FILES_CHKSUM', 'PROVIDES', 'DEPENDS', 'PR', 'PV', 'SRCREV', 'SRC_URI', 'S', 'do_fetch', 'do_unpack', 'do_patch', 'EXTRA_OECONF', 'do_configure', 'EXTRA_OEMAKE', 'do_compile', 'do_install', 'do_populate_sysroot', 'INITSCRIPT', 'USERADD', 'GROUPADD', 'PACKAGES', 'FILES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RPROVIDES', 'RREPLACES', 'RCONFLICTS', 'ALLOW_EMPTY', 'do_package', 'do_deploy']
+# Variables that sometimes are a bit long but shouldn't be wrapped
+nowrap_vars = ['SUMMARY', 'HOMEPAGE', 'BUGTRACKER']
+list_vars = ['SRC_URI', 'LIC_FILES_CHKSUM']
+meta_vars = ['SUMMARY', 'DESCRIPTION', 'HOMEPAGE', 'BUGTRACKER', 'SECTION']
+
+
+def pn_to_recipe(cooker, pn):
+ """Convert a recipe name (PN) to the path to the recipe file"""
+ import bb.providers
+
+ if pn in cooker.recipecache.pkg_pn:
+ filenames = cooker.recipecache.pkg_pn[pn]
+ best = bb.providers.findBestProvider(pn, cooker.data, cooker.recipecache, cooker.recipecache.pkg_pn)
+ return best[3]
+ else:
+ return None
+
+
+def get_unavailable_reasons(cooker, pn):
+ """If a recipe could not be found, find out why if possible"""
+ import bb.taskdata
+ taskdata = bb.taskdata.TaskData(None, skiplist=cooker.skiplist)
+ return taskdata.get_reasons(pn)
+
+
+def parse_recipe(fn, appendfiles, d):
+ """
+ Parse an individual recipe file, optionally with a list of
+ bbappend files.
+ """
+ import bb.cache
+ envdata = bb.cache.Cache.loadDataFull(fn, appendfiles, d)
+ return envdata
+
+
+def parse_recipe_simple(cooker, pn, d, appends=True):
+ """
+ Parse a recipe and optionally all bbappends that apply to it
+ in the current configuration.
+ """
+ import bb.providers
+
+ recipefile = pn_to_recipe(cooker, pn)
+ if not recipefile:
+ skipreasons = get_unavailable_reasons(cooker, pn)
+ # We may as well re-use bb.providers.NoProvider here
+ if skipreasons:
+ raise bb.providers.NoProvider(skipreasons)
+ else:
+ raise bb.providers.NoProvider('Unable to find any recipe file matching %s' % pn)
+ if appends:
+ appendfiles = cooker.collection.get_file_appends(recipefile)
+ return parse_recipe(recipefile, appendfiles, d)
+
+
+def get_var_files(fn, varlist, d):
+ """Find the file in which each of a list of variables is set.
+ Note: requires variable history to be enabled when parsing.
+ """
+ varfiles = {}
+ for v in varlist:
+ history = d.varhistory.variable(v)
+ files = []
+ for event in history:
+ if 'file' in event and not 'flag' in event:
+ files.append(event['file'])
+ if files:
+ actualfile = files[-1]
+ else:
+ actualfile = None
+ varfiles[v] = actualfile
+
+ return varfiles
+
+
+def patch_recipe_file(fn, values, patch=False, relpath=''):
+ """Update or insert variable values into a recipe file (assuming you
+ have already identified the exact file you want to update.)
+ Note that some manual inspection/intervention may be required
+ since this cannot handle all situations.
+ """
+ remainingnames = {}
+ for k in values.keys():
+ remainingnames[k] = recipe_progression.index(k) if k in recipe_progression else -1
+ remainingnames = OrderedDict(sorted(remainingnames.iteritems(), key=lambda x: x[1]))
+
+ with tempfile.NamedTemporaryFile('w', delete=False) as tf:
+ def outputvalue(name):
+ rawtext = '%s = "%s"\n' % (name, values[name])
+ if name in nowrap_vars:
+ tf.write(rawtext)
+ elif name in list_vars:
+ splitvalue = values[name].split()
+ if len(splitvalue) > 1:
+ linesplit = ' \\\n' + (' ' * (len(name) + 4))
+ tf.write('%s = "%s%s"\n' % (name, linesplit.join(splitvalue), linesplit))
+ else:
+ tf.write(rawtext)
+ else:
+ wrapped = textwrap.wrap(rawtext)
+ for wrapline in wrapped[:-1]:
+ tf.write('%s \\\n' % wrapline)
+ tf.write('%s\n' % wrapped[-1])
+
+ tfn = tf.name
+ with open(fn, 'r') as f:
+ # First runthrough - find existing names (so we know not to insert based on recipe_progression)
+ # Second runthrough - make the changes
+ existingnames = []
+ for runthrough in [1, 2]:
+ currname = None
+ for line in f:
+ if not currname:
+ insert = False
+ for k in remainingnames.keys():
+ for p in recipe_progression:
+ if re.match('^%s(_prepend|_append)*[ ?:=(]' % p, line):
+ if remainingnames[k] > -1 and recipe_progression.index(p) > remainingnames[k] and runthrough > 1 and not k in existingnames:
+ outputvalue(k)
+ del remainingnames[k]
+ break
+ for k in remainingnames.keys():
+ if re.match('^%s[ ?:=]' % k, line):
+ currname = k
+ if runthrough == 1:
+ existingnames.append(k)
+ else:
+ del remainingnames[k]
+ break
+ if currname and runthrough > 1:
+ outputvalue(currname)
+
+ if currname:
+ sline = line.rstrip()
+ if not sline.endswith('\\'):
+ currname = None
+ continue
+ if runthrough > 1:
+ tf.write(line)
+ f.seek(0)
+ if remainingnames:
+ tf.write('\n')
+ for k in remainingnames.keys():
+ outputvalue(k)
+
+ with open(tfn, 'U') as f:
+ tolines = f.readlines()
+ if patch:
+ with open(fn, 'U') as f:
+ fromlines = f.readlines()
+ relfn = os.path.relpath(fn, relpath)
+ diff = difflib.unified_diff(fromlines, tolines, 'a/%s' % relfn, 'b/%s' % relfn)
+ os.remove(tfn)
+ return diff
+ else:
+ with open(fn, 'w') as f:
+ f.writelines(tolines)
+ os.remove(tfn)
+ return None
+
+def localise_file_vars(fn, varfiles, varlist):
+ """Given a list of variables and variable history (fetched with get_var_files())
+ find where each variable should be set/changed. This handles for example where a
+ recipe includes an inc file where variables might be changed - in most cases
+ we want to update the inc file when changing the variable value rather than adding
+ it to the recipe itself.
+ """
+ fndir = os.path.dirname(fn) + os.sep
+
+ first_meta_file = None
+ for v in meta_vars:
+ f = varfiles.get(v, None)
+ if f:
+ actualdir = os.path.dirname(f) + os.sep
+ if actualdir.startswith(fndir):
+ first_meta_file = f
+ break
+
+ filevars = defaultdict(list)
+ for v in varlist:
+ f = varfiles[v]
+ # Only return files that are in the same directory as the recipe or in some directory below there
+ # (this excludes bbclass files and common inc files that wouldn't be appropriate to set the variable
+ # in if we were going to set a value specific to this recipe)
+ if f:
+ actualfile = f
+ else:
+ # Variable isn't in a file, if it's one of the "meta" vars, use the first file with a meta var in it
+ if first_meta_file:
+ actualfile = first_meta_file
+ else:
+ actualfile = fn
+
+ actualdir = os.path.dirname(actualfile) + os.sep
+ if not actualdir.startswith(fndir):
+ actualfile = fn
+ filevars[actualfile].append(v)
+
+ return filevars
+
+def patch_recipe(d, fn, varvalues, patch=False, relpath=''):
+ """Modify a list of variable values in the specified recipe. Handles inc files if
+ used by the recipe.
+ """
+ varlist = varvalues.keys()
+ varfiles = get_var_files(fn, varlist, d)
+ locs = localise_file_vars(fn, varfiles, varlist)
+ patches = []
+ for f,v in locs.iteritems():
+ vals = {k: varvalues[k] for k in v}
+ patchdata = patch_recipe_file(f, vals, patch, relpath)
+ if patch:
+ patches.append(patchdata)
+
+ if patch:
+ return patches
+ else:
+ return None
+
+
+
+def copy_recipe_files(d, tgt_dir, whole_dir=False, download=True):
+ """Copy (local) recipe files, including both files included via include/require,
+ and files referred to in the SRC_URI variable."""
+ import bb.fetch2
+ import oe.path
+
+ # FIXME need a warning if the unexpanded SRC_URI value contains variable references
+
+ uris = (d.getVar('SRC_URI', True) or "").split()
+ fetch = bb.fetch2.Fetch(uris, d)
+ if download:
+ fetch.download()
+
+ # Copy local files to target directory and gather any remote files
+ bb_dir = os.path.dirname(d.getVar('FILE', True)) + os.sep
+ remotes = []
+ includes = [path for path in d.getVar('BBINCLUDED', True).split() if
+ path.startswith(bb_dir) and os.path.exists(path)]
+ for path in fetch.localpaths() + includes:
+ # Only import files that are under the meta directory
+ if path.startswith(bb_dir):
+ if not whole_dir:
+ relpath = os.path.relpath(path, bb_dir)
+ subdir = os.path.join(tgt_dir, os.path.dirname(relpath))
+ if not os.path.exists(subdir):
+ os.makedirs(subdir)
+ shutil.copy2(path, os.path.join(tgt_dir, relpath))
+ else:
+ remotes.append(path)
+ # Simply copy whole meta dir, if requested
+ if whole_dir:
+ shutil.copytree(bb_dir, tgt_dir)
+
+ return remotes
+
+
+def get_recipe_patches(d):
+ """Get a list of the patches included in SRC_URI within a recipe."""
+ patchfiles = []
+ # Execute src_patches() defined in patch.bbclass - this works since that class
+ # is inherited globally
+ patches = bb.utils.exec_flat_python_func('src_patches', d)
+ for patch in patches:
+ _, _, local, _, _, parm = bb.fetch.decodeurl(patch)
+ patchfiles.append(local)
+ return patchfiles
+
+
+def get_recipe_patched_files(d):
+ """
+ Get the list of patches for a recipe along with the files each patch modifies.
+ Params:
+ d: the datastore for the recipe
+ Returns:
+ a dict mapping patch file path to a list of tuples of changed files and
+ change mode ('A' for add, 'D' for delete or 'M' for modify)
+ """
+ import oe.patch
+ # Execute src_patches() defined in patch.bbclass - this works since that class
+ # is inherited globally
+ patches = bb.utils.exec_flat_python_func('src_patches', d)
+ patchedfiles = {}
+ for patch in patches:
+ _, _, patchfile, _, _, parm = bb.fetch.decodeurl(patch)
+ striplevel = int(parm['striplevel'])
+ patchedfiles[patchfile] = oe.patch.PatchSet.getPatchedFiles(patchfile, striplevel, os.path.join(d.getVar('S', True), parm.get('patchdir', '')))
+ return patchedfiles
+
+
+def validate_pn(pn):
+ """Perform validation on a recipe name (PN) for a new recipe."""
+ reserved_names = ['forcevariable', 'append', 'prepend', 'remove']
+ if not re.match('[0-9a-z-.]+', pn):
+ return 'Recipe name "%s" is invalid: only characters 0-9, a-z, - and . are allowed' % pn
+ elif pn in reserved_names:
+ return 'Recipe name "%s" is invalid: is a reserved keyword' % pn
+ elif pn.startswith('pn-'):
+ return 'Recipe name "%s" is invalid: names starting with "pn-" are reserved' % pn
+ return ''
+
+
+def get_bbappend_path(d, destlayerdir, wildcardver=False):
+ """Determine how a bbappend for a recipe should be named and located within another layer"""
+
+ import bb.cookerdata
+
+ destlayerdir = os.path.abspath(destlayerdir)
+ recipefile = d.getVar('FILE', True)
+ recipefn = os.path.splitext(os.path.basename(recipefile))[0]
+ if wildcardver and '_' in recipefn:
+ recipefn = recipefn.split('_', 1)[0] + '_%'
+ appendfn = recipefn + '.bbappend'
+
+ # Parse the specified layer's layer.conf file directly, in case the layer isn't in bblayers.conf
+ confdata = d.createCopy()
+ confdata.setVar('BBFILES', '')
+ confdata.setVar('LAYERDIR', destlayerdir)
+ destlayerconf = os.path.join(destlayerdir, "conf", "layer.conf")
+ confdata = bb.cookerdata.parse_config_file(destlayerconf, confdata)
+
+ origlayerdir = find_layerdir(recipefile)
+ if not origlayerdir:
+ return (None, False)
+ # Now join this to the path where the bbappend is going and check if it is covered by BBFILES
+ appendpath = os.path.join(destlayerdir, os.path.relpath(os.path.dirname(recipefile), origlayerdir), appendfn)
+ closepath = ''
+ pathok = True
+ for bbfilespec in confdata.getVar('BBFILES', True).split():
+ if fnmatch.fnmatchcase(appendpath, bbfilespec):
+ # Our append path works, we're done
+ break
+ elif bbfilespec.startswith(destlayerdir) and fnmatch.fnmatchcase('test.bbappend', os.path.basename(bbfilespec)):
+ # Try to find the longest matching path
+ if len(bbfilespec) > len(closepath):
+ closepath = bbfilespec
+ else:
+ # Unfortunately the bbappend layer and the original recipe's layer don't have the same structure
+ if closepath:
+ # bbappend layer's layer.conf at least has a spec that picks up .bbappend files
+ # Now we just need to substitute out any wildcards
+ appendsubdir = os.path.relpath(os.path.dirname(closepath), destlayerdir)
+ if 'recipes-*' in appendsubdir:
+ # Try to copy this part from the original recipe path
+ res = re.search('/recipes-[^/]+/', recipefile)
+ if res:
+ appendsubdir = appendsubdir.replace('/recipes-*/', res.group(0))
+ # This is crude, but we have to do something
+ appendsubdir = appendsubdir.replace('*', recipefn.split('_')[0])
+ appendsubdir = appendsubdir.replace('?', 'a')
+ appendpath = os.path.join(destlayerdir, appendsubdir, appendfn)
+ else:
+ pathok = False
+ return (appendpath, pathok)
+
+
+def bbappend_recipe(rd, destlayerdir, srcfiles, install=None, wildcardver=False, machine=None, extralines=None, removevalues=None):
+ """
+ Writes a bbappend file for a recipe
+ Parameters:
+ rd: data dictionary for the recipe
+ destlayerdir: base directory of the layer to place the bbappend in
+ (subdirectory path from there will be determined automatically)
+ srcfiles: dict of source files to add to SRC_URI, where the value
+ is the full path to the file to be added, and the value is the
+ original filename as it would appear in SRC_URI or None if it
+ isn't already present. You may pass None for this parameter if
+ you simply want to specify your own content via the extralines
+ parameter.
+ install: dict mapping entries in srcfiles to a tuple of two elements:
+ install path (*without* ${D} prefix) and permission value (as a
+ string, e.g. '0644').
+ wildcardver: True to use a % wildcard in the bbappend filename, or
+ False to make the bbappend specific to the recipe version.
+ machine:
+ If specified, make the changes in the bbappend specific to this
+ machine. This will also cause PACKAGE_ARCH = "${MACHINE_ARCH}"
+ to be added to the bbappend.
+ extralines:
+ Extra lines to add to the bbappend. This may be a dict of name
+ value pairs, or simply a list of the lines.
+ removevalues:
+ Variable values to remove - a dict of names/values.
+ """
+
+ if not removevalues:
+ removevalues = {}
+
+ # Determine how the bbappend should be named
+ appendpath, pathok = get_bbappend_path(rd, destlayerdir, wildcardver)
+ if not appendpath:
+ bb.error('Unable to determine layer directory containing %s' % recipefile)
+ return (None, None)
+ if not pathok:
+ bb.warn('Unable to determine correct subdirectory path for bbappend file - check that what %s adds to BBFILES also matches .bbappend files. Using %s for now, but until you fix this the bbappend will not be applied.' % (os.path.join(destlayerdir, 'conf', 'layer.conf'), os.path.dirname(appendpath)))
+
+ appenddir = os.path.dirname(appendpath)
+ bb.utils.mkdirhier(appenddir)
+
+ # FIXME check if the bbappend doesn't get overridden by a higher priority layer?
+
+ layerdirs = [os.path.abspath(layerdir) for layerdir in rd.getVar('BBLAYERS', True).split()]
+ if not os.path.abspath(destlayerdir) in layerdirs:
+ bb.warn('Specified layer is not currently enabled in bblayers.conf, you will need to add it before this bbappend will be active')
+
+ bbappendlines = []
+ if extralines:
+ if isinstance(extralines, dict):
+ for name, value in extralines.iteritems():
+ bbappendlines.append((name, '=', value))
+ else:
+ # Do our best to split it
+ for line in extralines:
+ if line[-1] == '\n':
+ line = line[:-1]
+ splitline = line.split(None, 2)
+ if len(splitline) == 3:
+ bbappendlines.append(tuple(splitline))
+ else:
+ raise Exception('Invalid extralines value passed')
+
+ def popline(varname):
+ for i in xrange(0, len(bbappendlines)):
+ if bbappendlines[i][0] == varname:
+ line = bbappendlines.pop(i)
+ return line
+ return None
+
+ def appendline(varname, op, value):
+ for i in xrange(0, len(bbappendlines)):
+ item = bbappendlines[i]
+ if item[0] == varname:
+ bbappendlines[i] = (item[0], item[1], item[2] + ' ' + value)
+ break
+ else:
+ bbappendlines.append((varname, op, value))
+
+ destsubdir = rd.getVar('PN', True)
+ if srcfiles:
+ bbappendlines.append(('FILESEXTRAPATHS_prepend', ':=', '${THISDIR}/${PN}:'))
+
+ appendoverride = ''
+ if machine:
+ bbappendlines.append(('PACKAGE_ARCH', '=', '${MACHINE_ARCH}'))
+ appendoverride = '_%s' % machine
+ copyfiles = {}
+ if srcfiles:
+ instfunclines = []
+ for newfile, origsrcfile in srcfiles.iteritems():
+ srcfile = origsrcfile
+ srcurientry = None
+ if not srcfile:
+ srcfile = os.path.basename(newfile)
+ srcurientry = 'file://%s' % srcfile
+ # Double-check it's not there already
+ # FIXME do we care if the entry is added by another bbappend that might go away?
+ if not srcurientry in rd.getVar('SRC_URI', True).split():
+ if machine:
+ appendline('SRC_URI_append%s' % appendoverride, '=', ' ' + srcurientry)
+ else:
+ appendline('SRC_URI', '+=', srcurientry)
+ copyfiles[newfile] = srcfile
+ if install:
+ institem = install.pop(newfile, None)
+ if institem:
+ (destpath, perms) = institem
+ instdestpath = replace_dir_vars(destpath, rd)
+ instdirline = 'install -d ${D}%s' % os.path.dirname(instdestpath)
+ if not instdirline in instfunclines:
+ instfunclines.append(instdirline)
+ instfunclines.append('install -m %s ${WORKDIR}/%s ${D}%s' % (perms, os.path.basename(srcfile), instdestpath))
+ if instfunclines:
+ bbappendlines.append(('do_install_append%s()' % appendoverride, '', instfunclines))
+
+ bb.note('Writing append file %s' % appendpath)
+
+ if os.path.exists(appendpath):
+ # Work around lack of nonlocal in python 2
+ extvars = {'destsubdir': destsubdir}
+
+ def appendfile_varfunc(varname, origvalue, op, newlines):
+ if varname == 'FILESEXTRAPATHS_prepend':
+ if origvalue.startswith('${THISDIR}/'):
+ popline('FILESEXTRAPATHS_prepend')
+ extvars['destsubdir'] = rd.expand(origvalue.split('${THISDIR}/', 1)[1].rstrip(':'))
+ elif varname == 'PACKAGE_ARCH':
+ if machine:
+ popline('PACKAGE_ARCH')
+ return (machine, None, 4, False)
+ elif varname.startswith('do_install_append'):
+ func = popline(varname)
+ if func:
+ instfunclines = [line.strip() for line in origvalue.strip('\n').splitlines()]
+ for line in func[2]:
+ if not line in instfunclines:
+ instfunclines.append(line)
+ return (instfunclines, None, 4, False)
+ else:
+ splitval = origvalue.split()
+ changed = False
+ removevar = varname
+ if varname in ['SRC_URI', 'SRC_URI_append%s' % appendoverride]:
+ removevar = 'SRC_URI'
+ line = popline(varname)
+ if line:
+ if line[2] not in splitval:
+ splitval.append(line[2])
+ changed = True
+ else:
+ line = popline(varname)
+ if line:
+ splitval = [line[2]]
+ changed = True
+
+ if removevar in removevalues:
+ remove = removevalues[removevar]
+ if isinstance(remove, basestring):
+ if remove in splitval:
+ splitval.remove(remove)
+ changed = True
+ else:
+ for removeitem in remove:
+ if removeitem in splitval:
+ splitval.remove(removeitem)
+ changed = True
+
+ if changed:
+ newvalue = splitval
+ if len(newvalue) == 1:
+ # Ensure it's written out as one line
+ if '_append' in varname:
+ newvalue = ' ' + newvalue[0]
+ else:
+ newvalue = newvalue[0]
+ if not newvalue and (op in ['+=', '.='] or '_append' in varname):
+ # There's no point appending nothing
+ newvalue = None
+ if varname.endswith('()'):
+ indent = 4
+ else:
+ indent = -1
+ return (newvalue, None, indent, True)
+ return (origvalue, None, 4, False)
+
+ varnames = [item[0] for item in bbappendlines]
+ if removevalues:
+ varnames.extend(removevalues.keys())
+
+ with open(appendpath, 'r') as f:
+ (updated, newlines) = bb.utils.edit_metadata(f, varnames, appendfile_varfunc)
+
+ destsubdir = extvars['destsubdir']
+ else:
+ updated = False
+ newlines = []
+
+ if bbappendlines:
+ for line in bbappendlines:
+ if line[0].endswith('()'):
+ newlines.append('%s {\n %s\n}\n' % (line[0], '\n '.join(line[2])))
+ else:
+ newlines.append('%s %s "%s"\n\n' % line)
+ updated = True
+
+ if updated:
+ with open(appendpath, 'w') as f:
+ f.writelines(newlines)
+
+ if copyfiles:
+ if machine:
+ destsubdir = os.path.join(destsubdir, machine)
+ for newfile, srcfile in copyfiles.iteritems():
+ filedest = os.path.join(appenddir, destsubdir, os.path.basename(srcfile))
+ if os.path.abspath(newfile) != os.path.abspath(filedest):
+ bb.note('Copying %s to %s' % (newfile, filedest))
+ bb.utils.mkdirhier(os.path.dirname(filedest))
+ shutil.copyfile(newfile, filedest)
+
+ return (appendpath, os.path.join(appenddir, destsubdir))
+
+
+def find_layerdir(fn):
+ """ Figure out relative path to base of layer for a file (e.g. a recipe)"""
+ pth = os.path.dirname(fn)
+ layerdir = ''
+ while pth:
+ if os.path.exists(os.path.join(pth, 'conf', 'layer.conf')):
+ layerdir = pth
+ break
+ pth = os.path.dirname(pth)
+ return layerdir
+
+
+def replace_dir_vars(path, d):
+ """Replace common directory paths with appropriate variable references (e.g. /etc becomes ${sysconfdir})"""
+ dirvars = {}
+ # Sort by length so we get the variables we're interested in first
+ for var in sorted(d.keys(), key=len):
+ if var.endswith('dir') and var.lower() == var:
+ value = d.getVar(var, True)
+ if value.startswith('/') and not '\n' in value and value not in dirvars:
+ dirvars[value] = var
+ for dirpath in sorted(dirvars.keys(), reverse=True):
+ path = path.replace(dirpath, '${%s}' % dirvars[dirpath])
+ return path
+
+def get_recipe_pv_without_srcpv(pv, uri_type):
+ """
+ Get PV without SRCPV common in SCM's for now only
+ support git.
+
+ Returns tuple with pv, prefix and suffix.
+ """
+ pfx = ''
+ sfx = ''
+
+ if uri_type == 'git':
+ git_regex = re.compile("(?P<pfx>v?)(?P<ver>[^\+]*)((?P<sfx>\+(git)?r?(AUTOINC\+))(?P<rev>.*))?")
+ m = git_regex.match(pv)
+
+ if m:
+ pv = m.group('ver')
+ pfx = m.group('pfx')
+ sfx = m.group('sfx')
+ else:
+ regex = re.compile("(?P<pfx>(v|r)?)(?P<ver>.*)")
+ m = regex.match(pv)
+ if m:
+ pv = m.group('ver')
+ pfx = m.group('pfx')
+
+ return (pv, pfx, sfx)
+
+def get_recipe_upstream_version(rd):
+ """
+ Get upstream version of recipe using bb.fetch2 methods with support for
+ http, https, ftp and git.
+
+ bb.fetch2 exceptions can be raised,
+ FetchError when don't have network access or upstream site don't response.
+ NoMethodError when uri latest_versionstring method isn't implemented.
+
+ Returns a dictonary with version, type and datetime.
+ Type can be A for Automatic, M for Manual and U for Unknown.
+ """
+ from bb.fetch2 import decodeurl
+ from datetime import datetime
+
+ ru = {}
+ ru['version'] = ''
+ ru['type'] = 'U'
+ ru['datetime'] = ''
+
+ # XXX: If don't have SRC_URI means that don't have upstream sources so
+ # returns 1.0.
+ src_uris = rd.getVar('SRC_URI', True)
+ if not src_uris:
+ ru['version'] = '1.0'
+ ru['type'] = 'M'
+ ru['datetime'] = datetime.now()
+ return ru
+
+ # XXX: we suppose that the first entry points to the upstream sources
+ src_uri = src_uris.split()[0]
+ uri_type, _, _, _, _, _ = decodeurl(src_uri)
+
+ pv = rd.getVar('PV', True)
+
+ manual_upstream_version = rd.getVar("RECIPE_UPSTREAM_VERSION", True)
+ if manual_upstream_version:
+ # manual tracking of upstream version.
+ ru['version'] = manual_upstream_version
+ ru['type'] = 'M'
+
+ manual_upstream_date = rd.getVar("CHECK_DATE", True)
+ if manual_upstream_date:
+ date = datetime.strptime(manual_upstream_date, "%b %d, %Y")
+ else:
+ date = datetime.now()
+ ru['datetime'] = date
+
+ elif uri_type == "file":
+ # files are always up-to-date
+ ru['version'] = pv
+ ru['type'] = 'A'
+ ru['datetime'] = datetime.now()
+ else:
+ ud = bb.fetch2.FetchData(src_uri, rd)
+ pupver = ud.method.latest_versionstring(ud, rd)
+ (upversion, revision) = pupver
+
+ # format git version version+gitAUTOINC+HASH
+ if uri_type == 'git':
+ (pv, pfx, sfx) = get_recipe_pv_without_srcpv(pv, uri_type)
+
+ # if contains revision but not upversion use current pv
+ if upversion == '' and revision:
+ upversion = pv
+
+ if upversion:
+ tmp = upversion
+ upversion = ''
+
+ if pfx:
+ upversion = pfx + tmp
+ else:
+ upversion = tmp
+
+ if sfx:
+ upversion = upversion + sfx + revision[:10]
+
+ if upversion:
+ ru['version'] = upversion
+ ru['type'] = 'A'
+
+ ru['datetime'] = datetime.now()
+
+ return ru
diff --git a/meta/lib/oe/rootfs.py b/meta/lib/oe/rootfs.py
new file mode 100644
index 0000000..3b53fce
--- /dev/null
+++ b/meta/lib/oe/rootfs.py
@@ -0,0 +1,984 @@
+from abc import ABCMeta, abstractmethod
+from oe.utils import execute_pre_post_process
+from oe.package_manager import *
+from oe.manifest import *
+import oe.path
+import filecmp
+import shutil
+import os
+import subprocess
+import re
+
+
+class Rootfs(object):
+ """
+ This is an abstract class. Do not instantiate this directly.
+ """
+ __metaclass__ = ABCMeta
+
+ def __init__(self, d):
+ self.d = d
+ self.pm = None
+ self.image_rootfs = self.d.getVar('IMAGE_ROOTFS', True)
+ self.deploy_dir_image = self.d.getVar('DEPLOY_DIR_IMAGE', True)
+
+ self.install_order = Manifest.INSTALL_ORDER
+
+ @abstractmethod
+ def _create(self):
+ pass
+
+ @abstractmethod
+ def _get_delayed_postinsts(self):
+ pass
+
+ @abstractmethod
+ def _save_postinsts(self):
+ pass
+
+ @abstractmethod
+ def _log_check(self):
+ pass
+
+ def _log_check_warn(self):
+ r = re.compile('^(warn|Warn|NOTE: warn|NOTE: Warn|WARNING:)')
+ log_path = self.d.expand("${T}/log.do_rootfs")
+ with open(log_path, 'r') as log:
+ for line in log:
+ if 'log_check' in line or 'NOTE:' in line:
+ continue
+
+ m = r.search(line)
+ if m:
+ bb.warn('[log_check] %s: found a warning message in the logfile (keyword \'%s\'):\n[log_check] %s'
+ % (self.d.getVar('PN', True), m.group(), line))
+
+ def _log_check_error(self):
+ r = re.compile(self.log_check_regex)
+ log_path = self.d.expand("${T}/log.do_rootfs")
+ with open(log_path, 'r') as log:
+ found_error = 0
+ message = "\n"
+ for line in log:
+ if 'log_check' in line:
+ continue
+
+ m = r.search(line)
+ if m:
+ found_error = 1
+ bb.warn('[log_check] %s: found an error message in the logfile (keyword \'%s\'):\n[log_check] %s'
+ % (self.d.getVar('PN', True), m.group(), line))
+
+ if found_error >= 1 and found_error <= 5:
+ message += line + '\n'
+ found_error += 1
+
+ if found_error == 6:
+ bb.fatal(message)
+
+ def _insert_feed_uris(self):
+ if bb.utils.contains("IMAGE_FEATURES", "package-management",
+ True, False, self.d):
+ self.pm.insert_feeds_uris()
+
+ @abstractmethod
+ def _handle_intercept_failure(self, failed_script):
+ pass
+
+ """
+ The _cleanup() method should be used to clean-up stuff that we don't really
+ want to end up on target. For example, in the case of RPM, the DB locks.
+ The method is called, once, at the end of create() method.
+ """
+ @abstractmethod
+ def _cleanup(self):
+ pass
+
+ def _setup_dbg_rootfs(self, dirs):
+ gen_debugfs = self.d.getVar('IMAGE_GEN_DEBUGFS', True) or '0'
+ if gen_debugfs != '1':
+ return
+
+ bb.note(" Renaming the original rootfs...")
+ try:
+ shutil.rmtree(self.image_rootfs + '-orig')
+ except:
+ pass
+ os.rename(self.image_rootfs, self.image_rootfs + '-orig')
+
+ bb.note(" Creating debug rootfs...")
+ bb.utils.mkdirhier(self.image_rootfs)
+
+ bb.note(" Copying back package database...")
+ for dir in dirs:
+ bb.utils.mkdirhier(self.image_rootfs + os.path.dirname(dir))
+ shutil.copytree(self.image_rootfs + '-orig' + dir, self.image_rootfs + dir)
+
+ cpath = oe.cachedpath.CachedPath()
+ # Copy files located in /usr/lib/debug or /usr/src/debug
+ for dir in ["/usr/lib/debug", "/usr/src/debug"]:
+ src = self.image_rootfs + '-orig' + dir
+ if cpath.exists(src):
+ dst = self.image_rootfs + dir
+ bb.utils.mkdirhier(os.path.dirname(dst))
+ shutil.copytree(src, dst)
+
+ # Copy files with suffix '.debug' or located in '.debug' dir.
+ for root, dirs, files in cpath.walk(self.image_rootfs + '-orig'):
+ relative_dir = root[len(self.image_rootfs + '-orig'):]
+ for f in files:
+ if f.endswith('.debug') or '/.debug' in relative_dir:
+ bb.utils.mkdirhier(self.image_rootfs + relative_dir)
+ shutil.copy(os.path.join(root, f),
+ self.image_rootfs + relative_dir)
+
+ bb.note(" Install complementary '*-dbg' packages...")
+ self.pm.install_complementary('*-dbg')
+
+ bb.note(" Rename debug rootfs...")
+ try:
+ shutil.rmtree(self.image_rootfs + '-dbg')
+ except:
+ pass
+ os.rename(self.image_rootfs, self.image_rootfs + '-dbg')
+
+ bb.note(" Restoreing original rootfs...")
+ os.rename(self.image_rootfs + '-orig', self.image_rootfs)
+
+ def _exec_shell_cmd(self, cmd):
+ fakerootcmd = self.d.getVar('FAKEROOT', True)
+ if fakerootcmd is not None:
+ exec_cmd = [fakerootcmd, cmd]
+ else:
+ exec_cmd = cmd
+
+ try:
+ subprocess.check_output(exec_cmd, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ return("Command '%s' returned %d:\n%s" % (e.cmd, e.returncode, e.output))
+
+ return None
+
+ def create(self):
+ bb.note("###### Generate rootfs #######")
+ pre_process_cmds = self.d.getVar("ROOTFS_PREPROCESS_COMMAND", True)
+ post_process_cmds = self.d.getVar("ROOTFS_POSTPROCESS_COMMAND", True)
+
+ postinst_intercepts_dir = self.d.getVar("POSTINST_INTERCEPTS_DIR", True)
+ if not postinst_intercepts_dir:
+ postinst_intercepts_dir = self.d.expand("${COREBASE}/scripts/postinst-intercepts")
+ intercepts_dir = os.path.join(self.d.getVar('WORKDIR', True),
+ "intercept_scripts")
+
+ bb.utils.remove(intercepts_dir, True)
+
+ bb.utils.mkdirhier(self.image_rootfs)
+
+ bb.utils.mkdirhier(self.deploy_dir_image)
+
+ shutil.copytree(postinst_intercepts_dir, intercepts_dir)
+
+ shutil.copy(self.d.expand("${COREBASE}/meta/files/deploydir_readme.txt"),
+ self.deploy_dir_image +
+ "/README_-_DO_NOT_DELETE_FILES_IN_THIS_DIRECTORY.txt")
+
+ execute_pre_post_process(self.d, pre_process_cmds)
+
+ # call the package manager dependent create method
+ self._create()
+
+ sysconfdir = self.image_rootfs + self.d.getVar('sysconfdir', True)
+ bb.utils.mkdirhier(sysconfdir)
+ with open(sysconfdir + "/version", "w+") as ver:
+ ver.write(self.d.getVar('BUILDNAME', True) + "\n")
+
+ self._run_intercepts()
+
+ execute_pre_post_process(self.d, post_process_cmds)
+
+ if bb.utils.contains("IMAGE_FEATURES", "read-only-rootfs",
+ True, False, self.d):
+ delayed_postinsts = self._get_delayed_postinsts()
+ if delayed_postinsts is not None:
+ bb.fatal("The following packages could not be configured "
+ "offline and rootfs is read-only: %s" %
+ delayed_postinsts)
+
+ if self.d.getVar('USE_DEVFS', True) != "1":
+ self._create_devfs()
+
+ self._uninstall_unneeded()
+
+ self._insert_feed_uris()
+
+ self._run_ldconfig()
+
+ if self.d.getVar('USE_DEPMOD', True) != "0":
+ self._generate_kernel_module_deps()
+
+ self._cleanup()
+ self._log_check()
+
+ def _uninstall_unneeded(self):
+ # Remove unneeded init script symlinks
+ delayed_postinsts = self._get_delayed_postinsts()
+ if delayed_postinsts is None:
+ if os.path.exists(self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/init.d/run-postinsts")):
+ self._exec_shell_cmd(["update-rc.d", "-f", "-r",
+ self.d.getVar('IMAGE_ROOTFS', True),
+ "run-postinsts", "remove"])
+
+ runtime_pkgmanage = bb.utils.contains("IMAGE_FEATURES", "package-management",
+ True, False, self.d)
+ sysvcompat_in_distro = bb.utils.contains("DISTRO_FEATURES", [ "systemd", "sysvinit" ],
+ True, False, self.d)
+ image_rorfs = bb.utils.contains("IMAGE_FEATURES", "read-only-rootfs",
+ True, False, self.d)
+ if sysvcompat_in_distro and not image_rorfs:
+ pkg_to_remove = ""
+ else:
+ pkg_to_remove = "update-rc.d"
+ if not runtime_pkgmanage:
+ # Remove components that we don't need if we're not going to install
+ # additional packages at runtime
+ if delayed_postinsts is None:
+ installed_pkgs_dir = self.d.expand('${WORKDIR}/installed_pkgs.txt')
+ pkgs_to_remove = list()
+ with open(installed_pkgs_dir, "r+") as installed_pkgs:
+ pkgs_installed = installed_pkgs.read().splitlines()
+ for pkg_installed in pkgs_installed[:]:
+ pkg = pkg_installed.split()[0]
+ if pkg in ["update-rc.d",
+ "base-passwd",
+ "shadow",
+ "update-alternatives", pkg_to_remove,
+ self.d.getVar("ROOTFS_BOOTSTRAP_INSTALL", True)
+ ]:
+ pkgs_to_remove.append(pkg)
+ pkgs_installed.remove(pkg_installed)
+
+ if len(pkgs_to_remove) > 0:
+ self.pm.remove(pkgs_to_remove, False)
+ # Update installed_pkgs.txt
+ open(installed_pkgs_dir, "w+").write('\n'.join(pkgs_installed))
+
+ else:
+ self._save_postinsts()
+
+ post_uninstall_cmds = self.d.getVar("ROOTFS_POSTUNINSTALL_COMMAND", True)
+ execute_pre_post_process(self.d, post_uninstall_cmds)
+
+ if not runtime_pkgmanage:
+ # Remove the package manager data files
+ self.pm.remove_packaging_data()
+
+ def _run_intercepts(self):
+ intercepts_dir = os.path.join(self.d.getVar('WORKDIR', True),
+ "intercept_scripts")
+
+ bb.note("Running intercept scripts:")
+ os.environ['D'] = self.image_rootfs
+ for script in os.listdir(intercepts_dir):
+ script_full = os.path.join(intercepts_dir, script)
+
+ if script == "postinst_intercept" or not os.access(script_full, os.X_OK):
+ continue
+
+ bb.note("> Executing %s intercept ..." % script)
+
+ try:
+ subprocess.check_call(script_full)
+ except subprocess.CalledProcessError as e:
+ bb.warn("The postinstall intercept hook '%s' failed (exit code: %d)! See log for details!" %
+ (script, e.returncode))
+
+ with open(script_full) as intercept:
+ registered_pkgs = None
+ for line in intercept.read().split("\n"):
+ m = re.match("^##PKGS:(.*)", line)
+ if m is not None:
+ registered_pkgs = m.group(1).strip()
+ break
+
+ if registered_pkgs is not None:
+ bb.warn("The postinstalls for the following packages "
+ "will be postponed for first boot: %s" %
+ registered_pkgs)
+
+ # call the backend dependent handler
+ self._handle_intercept_failure(registered_pkgs)
+
+ def _run_ldconfig(self):
+ if self.d.getVar('LDCONFIGDEPEND', True):
+ bb.note("Executing: ldconfig -r" + self.image_rootfs + "-c new -v")
+ self._exec_shell_cmd(['ldconfig', '-r', self.image_rootfs, '-c',
+ 'new', '-v'])
+
+ def _check_for_kernel_modules(self, modules_dir):
+ for root, dirs, files in os.walk(modules_dir, topdown=True):
+ for name in files:
+ found_ko = name.endswith(".ko")
+ if found_ko:
+ return found_ko
+ return False
+
+ def _generate_kernel_module_deps(self):
+ modules_dir = os.path.join(self.image_rootfs, 'lib', 'modules')
+ # if we don't have any modules don't bother to do the depmod
+ if not self._check_for_kernel_modules(modules_dir):
+ bb.note("No Kernel Modules found, not running depmod")
+ return
+
+ kernel_abi_ver_file = oe.path.join(self.d.getVar('PKGDATA_DIR', True), "kernel-depmod",
+ 'kernel-abiversion')
+ if not os.path.exists(kernel_abi_ver_file):
+ bb.fatal("No kernel-abiversion file found (%s), cannot run depmod, aborting" % kernel_abi_ver_file)
+
+ kernel_ver = open(kernel_abi_ver_file).read().strip(' \n')
+ versioned_modules_dir = os.path.join(self.image_rootfs, modules_dir, kernel_ver)
+
+ bb.utils.mkdirhier(versioned_modules_dir)
+
+ self._exec_shell_cmd(['depmodwrapper', '-a', '-b', self.image_rootfs, kernel_ver])
+
+ """
+ Create devfs:
+ * IMAGE_DEVICE_TABLE is the old name to an absolute path to a device table file
+ * IMAGE_DEVICE_TABLES is a new name for a file, or list of files, seached
+ for in the BBPATH
+ If neither are specified then the default name of files/device_table-minimal.txt
+ is searched for in the BBPATH (same as the old version.)
+ """
+ def _create_devfs(self):
+ devtable_list = []
+ devtable = self.d.getVar('IMAGE_DEVICE_TABLE', True)
+ if devtable is not None:
+ devtable_list.append(devtable)
+ else:
+ devtables = self.d.getVar('IMAGE_DEVICE_TABLES', True)
+ if devtables is None:
+ devtables = 'files/device_table-minimal.txt'
+ for devtable in devtables.split():
+ devtable_list.append("%s" % bb.utils.which(self.d.getVar('BBPATH', True), devtable))
+
+ for devtable in devtable_list:
+ self._exec_shell_cmd(["makedevs", "-r",
+ self.image_rootfs, "-D", devtable])
+
+
+class RpmRootfs(Rootfs):
+ def __init__(self, d, manifest_dir):
+ super(RpmRootfs, self).__init__(d)
+ self.log_check_regex = '(unpacking of archive failed|Cannot find package'\
+ '|exit 1|ERROR: |Error: |Error |ERROR '\
+ '|Failed |Failed: |Failed$|Failed\(\d+\):)'
+ self.manifest = RpmManifest(d, manifest_dir)
+
+ self.pm = RpmPM(d,
+ d.getVar('IMAGE_ROOTFS', True),
+ self.d.getVar('TARGET_VENDOR', True)
+ )
+
+ self.inc_rpm_image_gen = self.d.getVar('INC_RPM_IMAGE_GEN', True)
+ if self.inc_rpm_image_gen != "1":
+ bb.utils.remove(self.image_rootfs, True)
+ else:
+ self.pm.recovery_packaging_data()
+ bb.utils.remove(self.d.getVar('MULTILIB_TEMP_ROOTFS', True), True)
+
+ self.pm.create_configs()
+
+ '''
+ While rpm incremental image generation is enabled, it will remove the
+ unneeded pkgs by comparing the new install solution manifest and the
+ old installed manifest.
+ '''
+ def _create_incremental(self, pkgs_initial_install):
+ if self.inc_rpm_image_gen == "1":
+
+ pkgs_to_install = list()
+ for pkg_type in pkgs_initial_install:
+ pkgs_to_install += pkgs_initial_install[pkg_type]
+
+ installed_manifest = self.pm.load_old_install_solution()
+ solution_manifest = self.pm.dump_install_solution(pkgs_to_install)
+
+ pkg_to_remove = list()
+ for pkg in installed_manifest:
+ if pkg not in solution_manifest:
+ pkg_to_remove.append(pkg)
+
+ self.pm.update()
+
+ bb.note('incremental update -- upgrade packages in place ')
+ self.pm.upgrade()
+ if pkg_to_remove != []:
+ bb.note('incremental removed: %s' % ' '.join(pkg_to_remove))
+ self.pm.remove(pkg_to_remove)
+
+ def _create(self):
+ pkgs_to_install = self.manifest.parse_initial_manifest()
+ rpm_pre_process_cmds = self.d.getVar('RPM_PREPROCESS_COMMANDS', True)
+ rpm_post_process_cmds = self.d.getVar('RPM_POSTPROCESS_COMMANDS', True)
+
+ # update PM index files
+ self.pm.write_index()
+
+ execute_pre_post_process(self.d, rpm_pre_process_cmds)
+
+ self.pm.dump_all_available_pkgs()
+
+ if self.inc_rpm_image_gen == "1":
+ self._create_incremental(pkgs_to_install)
+
+ self.pm.update()
+
+ pkgs = []
+ pkgs_attempt = []
+ for pkg_type in pkgs_to_install:
+ if pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY:
+ pkgs_attempt += pkgs_to_install[pkg_type]
+ else:
+ pkgs += pkgs_to_install[pkg_type]
+
+ self.pm.install(pkgs)
+
+ self.pm.install(pkgs_attempt, True)
+
+ self.pm.install_complementary()
+
+ self._setup_dbg_rootfs(['/etc/rpm', '/var/lib/rpm', '/var/lib/smart'])
+
+ execute_pre_post_process(self.d, rpm_post_process_cmds)
+
+ self._log_check()
+
+ if self.inc_rpm_image_gen == "1":
+ self.pm.backup_packaging_data()
+
+ self.pm.rpm_setup_smart_target_config()
+
+ @staticmethod
+ def _depends_list():
+ return ['DEPLOY_DIR_RPM', 'INC_RPM_IMAGE_GEN', 'RPM_PREPROCESS_COMMANDS',
+ 'RPM_POSTPROCESS_COMMANDS', 'RPM_PREFER_ELF_ARCH']
+
+ def _get_delayed_postinsts(self):
+ postinst_dir = self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/rpm-postinsts")
+ if os.path.isdir(postinst_dir):
+ files = os.listdir(postinst_dir)
+ for f in files:
+ bb.note('Delayed package scriptlet: %s' % f)
+ return files
+
+ return None
+
+ def _save_postinsts(self):
+ # this is just a stub. For RPM, the failed postinstalls are
+ # already saved in /etc/rpm-postinsts
+ pass
+
+ def _log_check_error(self):
+ r = re.compile('(unpacking of archive failed|Cannot find package|exit 1|ERR|Fail)')
+ log_path = self.d.expand("${T}/log.do_rootfs")
+ with open(log_path, 'r') as log:
+ found_error = 0
+ message = "\n"
+ for line in log.read().split('\n'):
+ if 'log_check' in line:
+ continue
+ # sh -x may emit code which isn't actually executed
+ if line.startswith('+'):
+ continue
+
+ m = r.search(line)
+ if m:
+ found_error = 1
+ bb.warn('log_check: There were error messages in the logfile')
+ bb.warn('log_check: Matched keyword: [%s]\n\n' % m.group())
+
+ if found_error >= 1 and found_error <= 5:
+ message += line + '\n'
+ found_error += 1
+
+ if found_error == 6:
+ bb.fatal(message)
+
+ def _log_check(self):
+ self._log_check_warn()
+ self._log_check_error()
+
+ def _handle_intercept_failure(self, registered_pkgs):
+ rpm_postinsts_dir = self.image_rootfs + self.d.expand('${sysconfdir}/rpm-postinsts/')
+ bb.utils.mkdirhier(rpm_postinsts_dir)
+
+ # Save the package postinstalls in /etc/rpm-postinsts
+ for pkg in registered_pkgs.split():
+ self.pm.save_rpmpostinst(pkg)
+
+ def _cleanup(self):
+ # during the execution of postprocess commands, rpm is called several
+ # times to get the files installed, dependencies, etc. This creates the
+ # __db.00* (Berkeley DB files that hold locks, rpm specific environment
+ # settings, etc.), that should not get into the final rootfs
+ self.pm.unlock_rpm_db()
+ if os.path.isdir(self.pm.install_dir_path + "/tmp") and not os.listdir(self.pm.install_dir_path + "/tmp"):
+ bb.utils.remove(self.pm.install_dir_path + "/tmp", True)
+ if os.path.isdir(self.pm.install_dir_path) and not os.listdir(self.pm.install_dir_path):
+ bb.utils.remove(self.pm.install_dir_path, True)
+
+class DpkgOpkgRootfs(Rootfs):
+ def __init__(self, d):
+ super(DpkgOpkgRootfs, self).__init__(d)
+
+ def _get_pkgs_postinsts(self, status_file):
+ def _get_pkg_depends_list(pkg_depends):
+ pkg_depends_list = []
+ # filter version requirements like libc (>= 1.1)
+ for dep in pkg_depends.split(', '):
+ m_dep = re.match("^(.*) \(.*\)$", dep)
+ if m_dep:
+ dep = m_dep.group(1)
+ pkg_depends_list.append(dep)
+
+ return pkg_depends_list
+
+ pkgs = {}
+ pkg_name = ""
+ pkg_status_match = False
+ pkg_depends = ""
+
+ with open(status_file) as status:
+ data = status.read()
+ status.close()
+ for line in data.split('\n'):
+ m_pkg = re.match("^Package: (.*)", line)
+ m_status = re.match("^Status:.*unpacked", line)
+ m_depends = re.match("^Depends: (.*)", line)
+
+ if m_pkg is not None:
+ if pkg_name and pkg_status_match:
+ pkgs[pkg_name] = _get_pkg_depends_list(pkg_depends)
+
+ pkg_name = m_pkg.group(1)
+ pkg_status_match = False
+ pkg_depends = ""
+ elif m_status is not None:
+ pkg_status_match = True
+ elif m_depends is not None:
+ pkg_depends = m_depends.group(1)
+
+ # remove package dependencies not in postinsts
+ pkg_names = pkgs.keys()
+ for pkg_name in pkg_names:
+ deps = pkgs[pkg_name][:]
+
+ for d in deps:
+ if d not in pkg_names:
+ pkgs[pkg_name].remove(d)
+
+ return pkgs
+
+ def _get_delayed_postinsts_common(self, status_file):
+ def _dep_resolve(graph, node, resolved, seen):
+ seen.append(node)
+
+ for edge in graph[node]:
+ if edge not in resolved:
+ if edge in seen:
+ raise RuntimeError("Packages %s and %s have " \
+ "a circular dependency in postinsts scripts." \
+ % (node, edge))
+ _dep_resolve(graph, edge, resolved, seen)
+
+ resolved.append(node)
+
+ pkg_list = []
+
+ pkgs = self._get_pkgs_postinsts(status_file)
+ if pkgs:
+ root = "__packagegroup_postinst__"
+ pkgs[root] = pkgs.keys()
+ _dep_resolve(pkgs, root, pkg_list, [])
+ pkg_list.remove(root)
+
+ if len(pkg_list) == 0:
+ return None
+
+ return pkg_list
+
+ def _save_postinsts_common(self, dst_postinst_dir, src_postinst_dir):
+ num = 0
+ for p in self._get_delayed_postinsts():
+ bb.utils.mkdirhier(dst_postinst_dir)
+
+ if os.path.exists(os.path.join(src_postinst_dir, p + ".postinst")):
+ shutil.copy(os.path.join(src_postinst_dir, p + ".postinst"),
+ os.path.join(dst_postinst_dir, "%03d-%s" % (num, p)))
+
+ num += 1
+
+class DpkgRootfs(DpkgOpkgRootfs):
+ def __init__(self, d, manifest_dir):
+ super(DpkgRootfs, self).__init__(d)
+ self.log_check_regex = '^E:'
+
+ bb.utils.remove(self.image_rootfs, True)
+ bb.utils.remove(self.d.getVar('MULTILIB_TEMP_ROOTFS', True), True)
+ self.manifest = DpkgManifest(d, manifest_dir)
+ self.pm = DpkgPM(d, d.getVar('IMAGE_ROOTFS', True),
+ d.getVar('PACKAGE_ARCHS', True),
+ d.getVar('DPKG_ARCH', True))
+
+
+ def _create(self):
+ pkgs_to_install = self.manifest.parse_initial_manifest()
+ deb_pre_process_cmds = self.d.getVar('DEB_PREPROCESS_COMMANDS', True)
+ deb_post_process_cmds = self.d.getVar('DEB_POSTPROCESS_COMMANDS', True)
+
+ alt_dir = self.d.expand("${IMAGE_ROOTFS}/var/lib/dpkg/alternatives")
+ bb.utils.mkdirhier(alt_dir)
+
+ # update PM index files
+ self.pm.write_index()
+
+ execute_pre_post_process(self.d, deb_pre_process_cmds)
+
+ self.pm.update()
+
+ for pkg_type in self.install_order:
+ if pkg_type in pkgs_to_install:
+ self.pm.install(pkgs_to_install[pkg_type],
+ [False, True][pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY])
+
+ self.pm.install_complementary()
+
+ self._setup_dbg_rootfs(['/var/lib/dpkg'])
+
+ self.pm.fix_broken_dependencies()
+
+ self.pm.mark_packages("installed")
+
+ self.pm.run_pre_post_installs()
+
+ execute_pre_post_process(self.d, deb_post_process_cmds)
+
+ @staticmethod
+ def _depends_list():
+ return ['DEPLOY_DIR_DEB', 'DEB_SDK_ARCH', 'APTCONF_TARGET', 'APT_ARGS', 'DPKG_ARCH', 'DEB_PREPROCESS_COMMANDS', 'DEB_POSTPROCESS_COMMANDS']
+
+ def _get_delayed_postinsts(self):
+ status_file = self.image_rootfs + "/var/lib/dpkg/status"
+ return self._get_delayed_postinsts_common(status_file)
+
+ def _save_postinsts(self):
+ dst_postinst_dir = self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/deb-postinsts")
+ src_postinst_dir = self.d.expand("${IMAGE_ROOTFS}/var/lib/dpkg/info")
+ return self._save_postinsts_common(dst_postinst_dir, src_postinst_dir)
+
+ def _handle_intercept_failure(self, registered_pkgs):
+ self.pm.mark_packages("unpacked", registered_pkgs.split())
+
+ def _log_check(self):
+ self._log_check_warn()
+ self._log_check_error()
+
+ def _cleanup(self):
+ pass
+
+
+class OpkgRootfs(DpkgOpkgRootfs):
+ def __init__(self, d, manifest_dir):
+ super(OpkgRootfs, self).__init__(d)
+ self.log_check_regex = '(exit 1|Collected errors)'
+
+ self.manifest = OpkgManifest(d, manifest_dir)
+ self.opkg_conf = self.d.getVar("IPKGCONF_TARGET", True)
+ self.pkg_archs = self.d.getVar("ALL_MULTILIB_PACKAGE_ARCHS", True)
+
+ self.inc_opkg_image_gen = self.d.getVar('INC_IPK_IMAGE_GEN', True) or ""
+ if self._remove_old_rootfs():
+ bb.utils.remove(self.image_rootfs, True)
+ self.pm = OpkgPM(d,
+ self.image_rootfs,
+ self.opkg_conf,
+ self.pkg_archs)
+ else:
+ self.pm = OpkgPM(d,
+ self.image_rootfs,
+ self.opkg_conf,
+ self.pkg_archs)
+ self.pm.recover_packaging_data()
+
+ bb.utils.remove(self.d.getVar('MULTILIB_TEMP_ROOTFS', True), True)
+
+ def _prelink_file(self, root_dir, filename):
+ bb.note('prelink %s in %s' % (filename, root_dir))
+ prelink_cfg = oe.path.join(root_dir,
+ self.d.expand('${sysconfdir}/prelink.conf'))
+ if not os.path.exists(prelink_cfg):
+ shutil.copy(self.d.expand('${STAGING_DIR_NATIVE}${sysconfdir_native}/prelink.conf'),
+ prelink_cfg)
+
+ cmd_prelink = self.d.expand('${STAGING_DIR_NATIVE}${sbindir_native}/prelink')
+ self._exec_shell_cmd([cmd_prelink,
+ '--root',
+ root_dir,
+ '-amR',
+ '-N',
+ '-c',
+ self.d.expand('${sysconfdir}/prelink.conf')])
+
+ '''
+ Compare two files with the same key twice to see if they are equal.
+ If they are not equal, it means they are duplicated and come from
+ different packages.
+ 1st: Comapre them directly;
+ 2nd: While incremental image creation is enabled, one of the
+ files could be probaly prelinked in the previous image
+ creation and the file has been changed, so we need to
+ prelink the other one and compare them.
+ '''
+ def _file_equal(self, key, f1, f2):
+
+ # Both of them are not prelinked
+ if filecmp.cmp(f1, f2):
+ return True
+
+ if self.image_rootfs not in f1:
+ self._prelink_file(f1.replace(key, ''), f1)
+
+ if self.image_rootfs not in f2:
+ self._prelink_file(f2.replace(key, ''), f2)
+
+ # Both of them are prelinked
+ if filecmp.cmp(f1, f2):
+ return True
+
+ # Not equal
+ return False
+
+ """
+ This function was reused from the old implementation.
+ See commit: "image.bbclass: Added variables for multilib support." by
+ Lianhao Lu.
+ """
+ def _multilib_sanity_test(self, dirs):
+
+ allow_replace = self.d.getVar("MULTILIBRE_ALLOW_REP", True)
+ if allow_replace is None:
+ allow_replace = ""
+
+ allow_rep = re.compile(re.sub("\|$", "", allow_replace))
+ error_prompt = "Multilib check error:"
+
+ files = {}
+ for dir in dirs:
+ for root, subfolders, subfiles in os.walk(dir):
+ for file in subfiles:
+ item = os.path.join(root, file)
+ key = str(os.path.join("/", os.path.relpath(item, dir)))
+
+ valid = True
+ if key in files:
+ #check whether the file is allow to replace
+ if allow_rep.match(key):
+ valid = True
+ else:
+ if os.path.exists(files[key]) and \
+ os.path.exists(item) and \
+ not self._file_equal(key, files[key], item):
+ valid = False
+ bb.fatal("%s duplicate files %s %s is not the same\n" %
+ (error_prompt, item, files[key]))
+
+ #pass the check, add to list
+ if valid:
+ files[key] = item
+
+ def _multilib_test_install(self, pkgs):
+ ml_temp = self.d.getVar("MULTILIB_TEMP_ROOTFS", True)
+ bb.utils.mkdirhier(ml_temp)
+
+ dirs = [self.image_rootfs]
+
+ for variant in self.d.getVar("MULTILIB_VARIANTS", True).split():
+ ml_target_rootfs = os.path.join(ml_temp, variant)
+
+ bb.utils.remove(ml_target_rootfs, True)
+
+ ml_opkg_conf = os.path.join(ml_temp,
+ variant + "-" + os.path.basename(self.opkg_conf))
+
+ ml_pm = OpkgPM(self.d, ml_target_rootfs, ml_opkg_conf, self.pkg_archs)
+
+ ml_pm.update()
+ ml_pm.install(pkgs)
+
+ dirs.append(ml_target_rootfs)
+
+ self._multilib_sanity_test(dirs)
+
+ '''
+ While ipk incremental image generation is enabled, it will remove the
+ unneeded pkgs by comparing the old full manifest in previous existing
+ image and the new full manifest in the current image.
+ '''
+ def _remove_extra_packages(self, pkgs_initial_install):
+ if self.inc_opkg_image_gen == "1":
+ # Parse full manifest in previous existing image creation session
+ old_full_manifest = self.manifest.parse_full_manifest()
+
+ # Create full manifest for the current image session, the old one
+ # will be replaced by the new one.
+ self.manifest.create_full(self.pm)
+
+ # Parse full manifest in current image creation session
+ new_full_manifest = self.manifest.parse_full_manifest()
+
+ pkg_to_remove = list()
+ for pkg in old_full_manifest:
+ if pkg not in new_full_manifest:
+ pkg_to_remove.append(pkg)
+
+ if pkg_to_remove != []:
+ bb.note('decremental removed: %s' % ' '.join(pkg_to_remove))
+ self.pm.remove(pkg_to_remove)
+
+ '''
+ Compare with previous existing image creation, if some conditions
+ triggered, the previous old image should be removed.
+ The conditions include any of 'PACKAGE_EXCLUDE, NO_RECOMMENDATIONS
+ and BAD_RECOMMENDATIONS' has been changed.
+ '''
+ def _remove_old_rootfs(self):
+ if self.inc_opkg_image_gen != "1":
+ return True
+
+ vars_list_file = self.d.expand('${T}/vars_list')
+
+ old_vars_list = ""
+ if os.path.exists(vars_list_file):
+ old_vars_list = open(vars_list_file, 'r+').read()
+
+ new_vars_list = '%s:%s:%s\n' % \
+ ((self.d.getVar('BAD_RECOMMENDATIONS', True) or '').strip(),
+ (self.d.getVar('NO_RECOMMENDATIONS', True) or '').strip(),
+ (self.d.getVar('PACKAGE_EXCLUDE', True) or '').strip())
+ open(vars_list_file, 'w+').write(new_vars_list)
+
+ if old_vars_list != new_vars_list:
+ return True
+
+ return False
+
+ def _create(self):
+ pkgs_to_install = self.manifest.parse_initial_manifest()
+ opkg_pre_process_cmds = self.d.getVar('OPKG_PREPROCESS_COMMANDS', True)
+ opkg_post_process_cmds = self.d.getVar('OPKG_POSTPROCESS_COMMANDS', True)
+ rootfs_post_install_cmds = self.d.getVar('ROOTFS_POSTINSTALL_COMMAND', True)
+
+ # update PM index files, unless users provide their own feeds
+ if (self.d.getVar('BUILD_IMAGES_FROM_FEEDS', True) or "") != "1":
+ self.pm.write_index()
+
+ execute_pre_post_process(self.d, opkg_pre_process_cmds)
+
+ self.pm.update()
+
+ self.pm.handle_bad_recommendations()
+
+ if self.inc_opkg_image_gen == "1":
+ self._remove_extra_packages(pkgs_to_install)
+
+ for pkg_type in self.install_order:
+ if pkg_type in pkgs_to_install:
+ # For multilib, we perform a sanity test before final install
+ # If sanity test fails, it will automatically do a bb.fatal()
+ # and the installation will stop
+ if pkg_type == Manifest.PKG_TYPE_MULTILIB:
+ self._multilib_test_install(pkgs_to_install[pkg_type])
+
+ self.pm.install(pkgs_to_install[pkg_type],
+ [False, True][pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY])
+
+ self.pm.install_complementary()
+
+ self._setup_dbg_rootfs(['/var/lib/opkg'])
+
+ execute_pre_post_process(self.d, opkg_post_process_cmds)
+ execute_pre_post_process(self.d, rootfs_post_install_cmds)
+
+ if self.inc_opkg_image_gen == "1":
+ self.pm.backup_packaging_data()
+
+ @staticmethod
+ def _depends_list():
+ return ['IPKGCONF_SDK', 'IPK_FEED_URIS', 'DEPLOY_DIR_IPK', 'IPKGCONF_TARGET', 'INC_IPK_IMAGE_GEN', 'OPKG_ARGS', 'OPKGLIBDIR', 'OPKG_PREPROCESS_COMMANDS', 'OPKG_POSTPROCESS_COMMANDS', 'OPKGLIBDIR']
+
+ def _get_delayed_postinsts(self):
+ status_file = os.path.join(self.image_rootfs,
+ self.d.getVar('OPKGLIBDIR', True).strip('/'),
+ "opkg", "status")
+ return self._get_delayed_postinsts_common(status_file)
+
+ def _save_postinsts(self):
+ dst_postinst_dir = self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/ipk-postinsts")
+ src_postinst_dir = self.d.expand("${IMAGE_ROOTFS}${OPKGLIBDIR}/opkg/info")
+ return self._save_postinsts_common(dst_postinst_dir, src_postinst_dir)
+
+ def _handle_intercept_failure(self, registered_pkgs):
+ self.pm.mark_packages("unpacked", registered_pkgs.split())
+
+ def _log_check(self):
+ self._log_check_warn()
+ self._log_check_error()
+
+ def _cleanup(self):
+ pass
+
+def get_class_for_type(imgtype):
+ return {"rpm": RpmRootfs,
+ "ipk": OpkgRootfs,
+ "deb": DpkgRootfs}[imgtype]
+
+def variable_depends(d, manifest_dir=None):
+ img_type = d.getVar('IMAGE_PKGTYPE', True)
+ cls = get_class_for_type(img_type)
+ return cls._depends_list()
+
+def create_rootfs(d, manifest_dir=None):
+ env_bkp = os.environ.copy()
+
+ img_type = d.getVar('IMAGE_PKGTYPE', True)
+ if img_type == "rpm":
+ RpmRootfs(d, manifest_dir).create()
+ elif img_type == "ipk":
+ OpkgRootfs(d, manifest_dir).create()
+ elif img_type == "deb":
+ DpkgRootfs(d, manifest_dir).create()
+
+ os.environ.clear()
+ os.environ.update(env_bkp)
+
+
+def image_list_installed_packages(d, format=None, rootfs_dir=None):
+ if not rootfs_dir:
+ rootfs_dir = d.getVar('IMAGE_ROOTFS', True)
+
+ img_type = d.getVar('IMAGE_PKGTYPE', True)
+ if img_type == "rpm":
+ return RpmPkgsList(d, rootfs_dir).list(format)
+ elif img_type == "ipk":
+ return OpkgPkgsList(d, rootfs_dir, d.getVar("IPKGCONF_TARGET", True)).list(format)
+ elif img_type == "deb":
+ return DpkgPkgsList(d, rootfs_dir).list(format)
+
+if __name__ == "__main__":
+ """
+ We should be able to run this as a standalone script, from outside bitbake
+ environment.
+ """
+ """
+ TBD
+ """
diff --git a/meta/lib/oe/sdk.py b/meta/lib/oe/sdk.py
new file mode 100644
index 0000000..53da0f0
--- /dev/null
+++ b/meta/lib/oe/sdk.py
@@ -0,0 +1,349 @@
+from abc import ABCMeta, abstractmethod
+from oe.utils import execute_pre_post_process
+from oe.manifest import *
+from oe.package_manager import *
+import os
+import shutil
+import glob
+
+
+class Sdk(object):
+ __metaclass__ = ABCMeta
+
+ def __init__(self, d, manifest_dir):
+ self.d = d
+ self.sdk_output = self.d.getVar('SDK_OUTPUT', True)
+ self.sdk_native_path = self.d.getVar('SDKPATHNATIVE', True).strip('/')
+ self.target_path = self.d.getVar('SDKTARGETSYSROOT', True).strip('/')
+ self.sysconfdir = self.d.getVar('sysconfdir', True).strip('/')
+
+ self.sdk_target_sysroot = os.path.join(self.sdk_output, self.target_path)
+ self.sdk_host_sysroot = self.sdk_output
+
+ if manifest_dir is None:
+ self.manifest_dir = self.d.getVar("SDK_DIR", True)
+ else:
+ self.manifest_dir = manifest_dir
+
+ bb.utils.remove(self.sdk_output, True)
+
+ self.install_order = Manifest.INSTALL_ORDER
+
+ @abstractmethod
+ def _populate(self):
+ pass
+
+ def populate(self):
+ bb.utils.mkdirhier(self.sdk_output)
+
+ # call backend dependent implementation
+ self._populate()
+
+ # Don't ship any libGL in the SDK
+ bb.utils.remove(os.path.join(self.sdk_output, self.sdk_native_path,
+ self.d.getVar('libdir_nativesdk', True).strip('/'),
+ "libGL*"))
+
+ # Fix or remove broken .la files
+ bb.utils.remove(os.path.join(self.sdk_output, self.sdk_native_path,
+ self.d.getVar('libdir_nativesdk', True).strip('/'),
+ "*.la"))
+
+ # Link the ld.so.cache file into the hosts filesystem
+ link_name = os.path.join(self.sdk_output, self.sdk_native_path,
+ self.sysconfdir, "ld.so.cache")
+ bb.utils.mkdirhier(os.path.dirname(link_name))
+ os.symlink("/etc/ld.so.cache", link_name)
+
+ execute_pre_post_process(self.d, self.d.getVar('SDK_POSTPROCESS_COMMAND', True))
+
+
+class RpmSdk(Sdk):
+ def __init__(self, d, manifest_dir=None):
+ super(RpmSdk, self).__init__(d, manifest_dir)
+
+ self.target_manifest = RpmManifest(d, self.manifest_dir,
+ Manifest.MANIFEST_TYPE_SDK_TARGET)
+ self.host_manifest = RpmManifest(d, self.manifest_dir,
+ Manifest.MANIFEST_TYPE_SDK_HOST)
+
+ target_providename = ['/bin/sh',
+ '/bin/bash',
+ '/usr/bin/env',
+ '/usr/bin/perl',
+ 'pkgconfig'
+ ]
+
+ self.target_pm = RpmPM(d,
+ self.sdk_target_sysroot,
+ self.d.getVar('TARGET_VENDOR', True),
+ 'target',
+ target_providename
+ )
+
+ sdk_providename = ['/bin/sh',
+ '/bin/bash',
+ '/usr/bin/env',
+ '/usr/bin/perl',
+ 'pkgconfig',
+ 'libGL.so()(64bit)',
+ 'libGL.so'
+ ]
+
+ self.host_pm = RpmPM(d,
+ self.sdk_host_sysroot,
+ self.d.getVar('SDK_VENDOR', True),
+ 'host',
+ sdk_providename,
+ "SDK_PACKAGE_ARCHS",
+ "SDK_OS"
+ )
+
+ def _populate_sysroot(self, pm, manifest):
+ pkgs_to_install = manifest.parse_initial_manifest()
+
+ pm.create_configs()
+ pm.write_index()
+ pm.dump_all_available_pkgs()
+ pm.update()
+
+ pkgs = []
+ pkgs_attempt = []
+ for pkg_type in pkgs_to_install:
+ if pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY:
+ pkgs_attempt += pkgs_to_install[pkg_type]
+ else:
+ pkgs += pkgs_to_install[pkg_type]
+
+ pm.install(pkgs)
+
+ pm.install(pkgs_attempt, True)
+
+ def _populate(self):
+ bb.note("Installing TARGET packages")
+ self._populate_sysroot(self.target_pm, self.target_manifest)
+
+ self.target_pm.install_complementary(self.d.getVar('SDKIMAGE_INSTALL_COMPLEMENTARY', True))
+
+ execute_pre_post_process(self.d, self.d.getVar("POPULATE_SDK_POST_TARGET_COMMAND", True))
+
+ self.target_pm.remove_packaging_data()
+
+ bb.note("Installing NATIVESDK packages")
+ self._populate_sysroot(self.host_pm, self.host_manifest)
+
+ execute_pre_post_process(self.d, self.d.getVar("POPULATE_SDK_POST_HOST_COMMAND", True))
+
+ self.host_pm.remove_packaging_data()
+
+ # Move host RPM library data
+ native_rpm_state_dir = os.path.join(self.sdk_output,
+ self.sdk_native_path,
+ self.d.getVar('localstatedir_nativesdk', True).strip('/'),
+ "lib",
+ "rpm"
+ )
+ bb.utils.mkdirhier(native_rpm_state_dir)
+ for f in glob.glob(os.path.join(self.sdk_output,
+ "var",
+ "lib",
+ "rpm",
+ "*")):
+ bb.utils.movefile(f, native_rpm_state_dir)
+
+ bb.utils.remove(os.path.join(self.sdk_output, "var"), True)
+
+ # Move host sysconfig data
+ native_sysconf_dir = os.path.join(self.sdk_output,
+ self.sdk_native_path,
+ self.d.getVar('sysconfdir',
+ True).strip('/'),
+ )
+ bb.utils.mkdirhier(native_sysconf_dir)
+ for f in glob.glob(os.path.join(self.sdk_output, "etc", "*")):
+ bb.utils.movefile(f, native_sysconf_dir)
+ bb.utils.remove(os.path.join(self.sdk_output, "etc"), True)
+
+
+class OpkgSdk(Sdk):
+ def __init__(self, d, manifest_dir=None):
+ super(OpkgSdk, self).__init__(d, manifest_dir)
+
+ self.target_conf = self.d.getVar("IPKGCONF_TARGET", True)
+ self.host_conf = self.d.getVar("IPKGCONF_SDK", True)
+
+ self.target_manifest = OpkgManifest(d, self.manifest_dir,
+ Manifest.MANIFEST_TYPE_SDK_TARGET)
+ self.host_manifest = OpkgManifest(d, self.manifest_dir,
+ Manifest.MANIFEST_TYPE_SDK_HOST)
+
+ self.target_pm = OpkgPM(d, self.sdk_target_sysroot, self.target_conf,
+ self.d.getVar("ALL_MULTILIB_PACKAGE_ARCHS", True))
+
+ self.host_pm = OpkgPM(d, self.sdk_host_sysroot, self.host_conf,
+ self.d.getVar("SDK_PACKAGE_ARCHS", True))
+
+ def _populate_sysroot(self, pm, manifest):
+ pkgs_to_install = manifest.parse_initial_manifest()
+
+ if (self.d.getVar('BUILD_IMAGES_FROM_FEEDS', True) or "") != "1":
+ pm.write_index()
+
+ pm.update()
+
+ pkgs = []
+ pkgs_attempt = []
+ for pkg_type in pkgs_to_install:
+ if pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY:
+ pkgs_attempt += pkgs_to_install[pkg_type]
+ else:
+ pkgs += pkgs_to_install[pkg_type]
+
+ pm.install(pkgs)
+
+ pm.install(pkgs_attempt, True)
+
+ def _populate(self):
+ bb.note("Installing TARGET packages")
+ self._populate_sysroot(self.target_pm, self.target_manifest)
+
+ self.target_pm.install_complementary(self.d.getVar('SDKIMAGE_INSTALL_COMPLEMENTARY', True))
+
+ execute_pre_post_process(self.d, self.d.getVar("POPULATE_SDK_POST_TARGET_COMMAND", True))
+
+ bb.note("Installing NATIVESDK packages")
+ self._populate_sysroot(self.host_pm, self.host_manifest)
+
+ execute_pre_post_process(self.d, self.d.getVar("POPULATE_SDK_POST_HOST_COMMAND", True))
+
+ target_sysconfdir = os.path.join(self.sdk_target_sysroot, self.sysconfdir)
+ host_sysconfdir = os.path.join(self.sdk_host_sysroot, self.sysconfdir)
+
+ bb.utils.mkdirhier(target_sysconfdir)
+ shutil.copy(self.target_conf, target_sysconfdir)
+ os.chmod(os.path.join(target_sysconfdir,
+ os.path.basename(self.target_conf)), 0644)
+
+ bb.utils.mkdirhier(host_sysconfdir)
+ shutil.copy(self.host_conf, host_sysconfdir)
+ os.chmod(os.path.join(host_sysconfdir,
+ os.path.basename(self.host_conf)), 0644)
+
+ native_opkg_state_dir = os.path.join(self.sdk_output, self.sdk_native_path,
+ self.d.getVar('localstatedir_nativesdk', True).strip('/'),
+ "lib", "opkg")
+ bb.utils.mkdirhier(native_opkg_state_dir)
+ for f in glob.glob(os.path.join(self.sdk_output, "var", "lib", "opkg", "*")):
+ bb.utils.movefile(f, native_opkg_state_dir)
+
+ bb.utils.remove(os.path.join(self.sdk_output, "var"), True)
+
+
+class DpkgSdk(Sdk):
+ def __init__(self, d, manifest_dir=None):
+ super(DpkgSdk, self).__init__(d, manifest_dir)
+
+ self.target_conf_dir = os.path.join(self.d.getVar("APTCONF_TARGET", True), "apt")
+ self.host_conf_dir = os.path.join(self.d.getVar("APTCONF_TARGET", True), "apt-sdk")
+
+ self.target_manifest = DpkgManifest(d, self.manifest_dir,
+ Manifest.MANIFEST_TYPE_SDK_TARGET)
+ self.host_manifest = DpkgManifest(d, self.manifest_dir,
+ Manifest.MANIFEST_TYPE_SDK_HOST)
+
+ self.target_pm = DpkgPM(d, self.sdk_target_sysroot,
+ self.d.getVar("PACKAGE_ARCHS", True),
+ self.d.getVar("DPKG_ARCH", True),
+ self.target_conf_dir)
+
+ self.host_pm = DpkgPM(d, self.sdk_host_sysroot,
+ self.d.getVar("SDK_PACKAGE_ARCHS", True),
+ self.d.getVar("DEB_SDK_ARCH", True),
+ self.host_conf_dir)
+
+ def _copy_apt_dir_to(self, dst_dir):
+ staging_etcdir_native = self.d.getVar("STAGING_ETCDIR_NATIVE", True)
+
+ bb.utils.remove(dst_dir, True)
+
+ shutil.copytree(os.path.join(staging_etcdir_native, "apt"), dst_dir)
+
+ def _populate_sysroot(self, pm, manifest):
+ pkgs_to_install = manifest.parse_initial_manifest()
+
+ pm.write_index()
+ pm.update()
+
+ pkgs = []
+ pkgs_attempt = []
+ for pkg_type in pkgs_to_install:
+ if pkg_type == Manifest.PKG_TYPE_ATTEMPT_ONLY:
+ pkgs_attempt += pkgs_to_install[pkg_type]
+ else:
+ pkgs += pkgs_to_install[pkg_type]
+
+ pm.install(pkgs)
+
+ pm.install(pkgs_attempt, True)
+
+ def _populate(self):
+ bb.note("Installing TARGET packages")
+ self._populate_sysroot(self.target_pm, self.target_manifest)
+
+ self.target_pm.install_complementary(self.d.getVar('SDKIMAGE_INSTALL_COMPLEMENTARY', True))
+
+ execute_pre_post_process(self.d, self.d.getVar("POPULATE_SDK_POST_TARGET_COMMAND", True))
+
+ self._copy_apt_dir_to(os.path.join(self.sdk_target_sysroot, "etc", "apt"))
+
+ bb.note("Installing NATIVESDK packages")
+ self._populate_sysroot(self.host_pm, self.host_manifest)
+
+ execute_pre_post_process(self.d, self.d.getVar("POPULATE_SDK_POST_HOST_COMMAND", True))
+
+ self._copy_apt_dir_to(os.path.join(self.sdk_output, self.sdk_native_path,
+ "etc", "apt"))
+
+ native_dpkg_state_dir = os.path.join(self.sdk_output, self.sdk_native_path,
+ "var", "lib", "dpkg")
+ bb.utils.mkdirhier(native_dpkg_state_dir)
+ for f in glob.glob(os.path.join(self.sdk_output, "var", "lib", "dpkg", "*")):
+ bb.utils.movefile(f, native_dpkg_state_dir)
+
+ bb.utils.remove(os.path.join(self.sdk_output, "var"), True)
+
+
+def sdk_list_installed_packages(d, target, format=None, rootfs_dir=None):
+ if rootfs_dir is None:
+ sdk_output = d.getVar('SDK_OUTPUT', True)
+ target_path = d.getVar('SDKTARGETSYSROOT', True).strip('/')
+
+ rootfs_dir = [sdk_output, os.path.join(sdk_output, target_path)][target is True]
+
+ img_type = d.getVar('IMAGE_PKGTYPE', True)
+ if img_type == "rpm":
+ arch_var = ["SDK_PACKAGE_ARCHS", None][target is True]
+ os_var = ["SDK_OS", None][target is True]
+ return RpmPkgsList(d, rootfs_dir, arch_var, os_var).list(format)
+ elif img_type == "ipk":
+ conf_file_var = ["IPKGCONF_SDK", "IPKGCONF_TARGET"][target is True]
+ return OpkgPkgsList(d, rootfs_dir, d.getVar(conf_file_var, True)).list(format)
+ elif img_type == "deb":
+ return DpkgPkgsList(d, rootfs_dir).list(format)
+
+def populate_sdk(d, manifest_dir=None):
+ env_bkp = os.environ.copy()
+
+ img_type = d.getVar('IMAGE_PKGTYPE', True)
+ if img_type == "rpm":
+ RpmSdk(d, manifest_dir).populate()
+ elif img_type == "ipk":
+ OpkgSdk(d, manifest_dir).populate()
+ elif img_type == "deb":
+ DpkgSdk(d, manifest_dir).populate()
+
+ os.environ.clear()
+ os.environ.update(env_bkp)
+
+if __name__ == "__main__":
+ pass
diff --git a/meta/lib/oe/sstatesig.py b/meta/lib/oe/sstatesig.py
new file mode 100644
index 0000000..cb46712
--- /dev/null
+++ b/meta/lib/oe/sstatesig.py
@@ -0,0 +1,291 @@
+import bb.siggen
+
+def sstate_rundepfilter(siggen, fn, recipename, task, dep, depname, dataCache):
+ # Return True if we should keep the dependency, False to drop it
+ def isNative(x):
+ return x.endswith("-native")
+ def isCross(x):
+ return "-cross-" in x
+ def isNativeSDK(x):
+ return x.startswith("nativesdk-")
+ def isKernel(fn):
+ inherits = " ".join(dataCache.inherits[fn])
+ return inherits.find("/module-base.bbclass") != -1 or inherits.find("/linux-kernel-base.bbclass") != -1
+ def isPackageGroup(fn):
+ inherits = " ".join(dataCache.inherits[fn])
+ return "/packagegroup.bbclass" in inherits
+ def isAllArch(fn):
+ inherits = " ".join(dataCache.inherits[fn])
+ return "/allarch.bbclass" in inherits
+ def isImage(fn):
+ return "/image.bbclass" in " ".join(dataCache.inherits[fn])
+
+ # Always include our own inter-task dependencies
+ if recipename == depname:
+ return True
+
+ # Quilt (patch application) changing isn't likely to affect anything
+ excludelist = ['quilt-native', 'subversion-native', 'git-native']
+ if depname in excludelist and recipename != depname:
+ return False
+
+ # Exclude well defined recipe->dependency
+ if "%s->%s" % (recipename, depname) in siggen.saferecipedeps:
+ return False
+
+ # Don't change native/cross/nativesdk recipe dependencies any further
+ if isNative(recipename) or isCross(recipename) or isNativeSDK(recipename):
+ return True
+
+ # Only target packages beyond here
+
+ # allarch packagegroups are assumed to have well behaved names which don't change between architecures/tunes
+ if isPackageGroup(fn) and isAllArch(fn):
+ return False
+
+ # Exclude well defined machine specific configurations which don't change ABI
+ if depname in siggen.abisaferecipes and not isImage(fn):
+ return False
+
+ # Kernel modules are well namespaced. We don't want to depend on the kernel's checksum
+ # if we're just doing an RRECOMMENDS_xxx = "kernel-module-*", not least because the checksum
+ # is machine specific.
+ # Therefore if we're not a kernel or a module recipe (inheriting the kernel classes)
+ # and we reccomend a kernel-module, we exclude the dependency.
+ depfn = dep.rsplit(".", 1)[0]
+ if dataCache and isKernel(depfn) and not isKernel(fn):
+ for pkg in dataCache.runrecs[fn]:
+ if " ".join(dataCache.runrecs[fn][pkg]).find("kernel-module-") != -1:
+ return False
+
+ # Default to keep dependencies
+ return True
+
+def sstate_lockedsigs(d):
+ sigs = {}
+ types = (d.getVar("SIGGEN_LOCKEDSIGS_TYPES", True) or "").split()
+ for t in types:
+ lockedsigs = (d.getVar("SIGGEN_LOCKEDSIGS_%s" % t, True) or "").split()
+ for ls in lockedsigs:
+ pn, task, h = ls.split(":", 2)
+ if pn not in sigs:
+ sigs[pn] = {}
+ sigs[pn][task] = h
+ return sigs
+
+class SignatureGeneratorOEBasic(bb.siggen.SignatureGeneratorBasic):
+ name = "OEBasic"
+ def init_rundepcheck(self, data):
+ self.abisaferecipes = (data.getVar("SIGGEN_EXCLUDERECIPES_ABISAFE", True) or "").split()
+ self.saferecipedeps = (data.getVar("SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS", True) or "").split()
+ pass
+ def rundep_check(self, fn, recipename, task, dep, depname, dataCache = None):
+ return sstate_rundepfilter(self, fn, recipename, task, dep, depname, dataCache)
+
+class SignatureGeneratorOEBasicHash(bb.siggen.SignatureGeneratorBasicHash):
+ name = "OEBasicHash"
+ def init_rundepcheck(self, data):
+ self.abisaferecipes = (data.getVar("SIGGEN_EXCLUDERECIPES_ABISAFE", True) or "").split()
+ self.saferecipedeps = (data.getVar("SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS", True) or "").split()
+ self.lockedsigs = sstate_lockedsigs(data)
+ self.lockedhashes = {}
+ self.lockedpnmap = {}
+ self.lockedhashfn = {}
+ self.machine = data.getVar("MACHINE", True)
+ self.mismatch_msgs = []
+ pass
+ def rundep_check(self, fn, recipename, task, dep, depname, dataCache = None):
+ return sstate_rundepfilter(self, fn, recipename, task, dep, depname, dataCache)
+
+ def get_taskdata(self):
+ data = super(bb.siggen.SignatureGeneratorBasicHash, self).get_taskdata()
+ return (data, self.lockedpnmap, self.lockedhashfn)
+
+ def set_taskdata(self, data):
+ coredata, self.lockedpnmap, self.lockedhashfn = data
+ super(bb.siggen.SignatureGeneratorBasicHash, self).set_taskdata(coredata)
+
+ def dump_sigs(self, dataCache, options):
+ self.dump_lockedsigs()
+ return super(bb.siggen.SignatureGeneratorBasicHash, self).dump_sigs(dataCache, options)
+
+ def get_taskhash(self, fn, task, deps, dataCache):
+ h = super(bb.siggen.SignatureGeneratorBasicHash, self).get_taskhash(fn, task, deps, dataCache)
+
+ recipename = dataCache.pkg_fn[fn]
+ self.lockedpnmap[fn] = recipename
+ self.lockedhashfn[fn] = dataCache.hashfn[fn]
+ if recipename in self.lockedsigs:
+ if task in self.lockedsigs[recipename]:
+ k = fn + "." + task
+ h_locked = self.lockedsigs[recipename][task]
+ self.lockedhashes[k] = h_locked
+ self.taskhash[k] = h_locked
+ #bb.warn("Using %s %s %s" % (recipename, task, h))
+
+ if h != h_locked:
+ self.mismatch_msgs.append('The %s:%s sig (%s) changed, use locked sig %s to instead'
+ % (recipename, task, h, h_locked))
+
+ return h_locked
+ #bb.warn("%s %s %s" % (recipename, task, h))
+ return h
+
+ def dump_sigtask(self, fn, task, stampbase, runtime):
+ k = fn + "." + task
+ if k in self.lockedhashes:
+ return
+ super(bb.siggen.SignatureGeneratorBasicHash, self).dump_sigtask(fn, task, stampbase, runtime)
+
+ def dump_lockedsigs(self, sigfile=None, taskfilter=None):
+ if not sigfile:
+ sigfile = os.getcwd() + "/locked-sigs.inc"
+
+ bb.plain("Writing locked sigs to %s" % sigfile)
+ types = {}
+ for k in self.runtaskdeps:
+ if taskfilter:
+ if not k in taskfilter:
+ continue
+ fn = k.rsplit(".",1)[0]
+ t = self.lockedhashfn[fn].split(" ")[1].split(":")[5]
+ t = 't-' + t.replace('_', '-')
+ if t not in types:
+ types[t] = []
+ types[t].append(k)
+
+ with open(sigfile, "w") as f:
+ for t in types:
+ f.write('SIGGEN_LOCKEDSIGS_%s = "\\\n' % t)
+ types[t].sort()
+ sortedk = sorted(types[t], key=lambda k: self.lockedpnmap[k.rsplit(".",1)[0]])
+ for k in sortedk:
+ fn = k.rsplit(".",1)[0]
+ task = k.rsplit(".",1)[1]
+ if k not in self.taskhash:
+ continue
+ f.write(" " + self.lockedpnmap[fn] + ":" + task + ":" + self.taskhash[k] + " \\\n")
+ f.write(' "\n')
+ f.write('SIGGEN_LOCKEDSIGS_TYPES_%s = "%s"' % (self.machine, " ".join(types.keys())))
+
+ def checkhashes(self, missed, ret, sq_fn, sq_task, sq_hash, sq_hashfn, d):
+ checklevel = d.getVar("SIGGEN_LOCKEDSIGS_CHECK_LEVEL", True)
+ for task in range(len(sq_fn)):
+ if task not in ret:
+ for pn in self.lockedsigs:
+ if sq_hash[task] in self.lockedsigs[pn].itervalues():
+ self.mismatch_msgs.append("Locked sig is set for %s:%s (%s) yet not in sstate cache?"
+ % (pn, sq_task[task], sq_hash[task]))
+
+ if self.mismatch_msgs and checklevel == 'warn':
+ bb.warn("\n".join(self.mismatch_msgs))
+ elif self.mismatch_msgs and checklevel == 'error':
+ bb.fatal("\n".join(self.mismatch_msgs))
+
+
+# Insert these classes into siggen's namespace so it can see and select them
+bb.siggen.SignatureGeneratorOEBasic = SignatureGeneratorOEBasic
+bb.siggen.SignatureGeneratorOEBasicHash = SignatureGeneratorOEBasicHash
+
+
+def find_siginfo(pn, taskname, taskhashlist, d):
+ """ Find signature data files for comparison purposes """
+
+ import fnmatch
+ import glob
+
+ if taskhashlist:
+ hashfiles = {}
+
+ if not taskname:
+ # We have to derive pn and taskname
+ key = pn
+ splitit = key.split('.bb.')
+ taskname = splitit[1]
+ pn = os.path.basename(splitit[0]).split('_')[0]
+ if key.startswith('virtual:native:'):
+ pn = pn + '-native'
+
+ filedates = {}
+
+ # First search in stamps dir
+ localdata = d.createCopy()
+ localdata.setVar('MULTIMACH_TARGET_SYS', '*')
+ localdata.setVar('PN', pn)
+ localdata.setVar('PV', '*')
+ localdata.setVar('PR', '*')
+ localdata.setVar('EXTENDPE', '')
+ stamp = localdata.getVar('STAMP', True)
+ filespec = '%s.%s.sigdata.*' % (stamp, taskname)
+ foundall = False
+ import glob
+ for fullpath in glob.glob(filespec):
+ match = False
+ if taskhashlist:
+ for taskhash in taskhashlist:
+ if fullpath.endswith('.%s' % taskhash):
+ hashfiles[taskhash] = fullpath
+ if len(hashfiles) == len(taskhashlist):
+ foundall = True
+ break
+ else:
+ try:
+ filedates[fullpath] = os.stat(fullpath).st_mtime
+ except OSError:
+ continue
+
+ if not taskhashlist or (len(filedates) < 2 and not foundall):
+ # That didn't work, look in sstate-cache
+ hashes = taskhashlist or ['*']
+ localdata = bb.data.createCopy(d)
+ for hashval in hashes:
+ localdata.setVar('PACKAGE_ARCH', '*')
+ localdata.setVar('TARGET_VENDOR', '*')
+ localdata.setVar('TARGET_OS', '*')
+ localdata.setVar('PN', pn)
+ localdata.setVar('PV', '*')
+ localdata.setVar('PR', '*')
+ localdata.setVar('BB_TASKHASH', hashval)
+ swspec = localdata.getVar('SSTATE_SWSPEC', True)
+ if taskname in ['do_fetch', 'do_unpack', 'do_patch', 'do_populate_lic', 'do_preconfigure'] and swspec:
+ localdata.setVar('SSTATE_PKGSPEC', '${SSTATE_SWSPEC}')
+ elif pn.endswith('-native') or "-cross-" in pn or "-crosssdk-" in pn:
+ localdata.setVar('SSTATE_EXTRAPATH', "${NATIVELSBSTRING}/")
+ sstatename = taskname[3:]
+ filespec = '%s_%s.*.siginfo' % (localdata.getVar('SSTATE_PKG', True), sstatename)
+
+ if hashval != '*':
+ sstatedir = "%s/%s" % (d.getVar('SSTATE_DIR', True), hashval[:2])
+ else:
+ sstatedir = d.getVar('SSTATE_DIR', True)
+
+ for root, dirs, files in os.walk(sstatedir):
+ for fn in files:
+ fullpath = os.path.join(root, fn)
+ if fnmatch.fnmatch(fullpath, filespec):
+ if taskhashlist:
+ hashfiles[hashval] = fullpath
+ else:
+ try:
+ filedates[fullpath] = os.stat(fullpath).st_mtime
+ except:
+ continue
+
+ if taskhashlist:
+ return hashfiles
+ else:
+ return filedates
+
+bb.siggen.find_siginfo = find_siginfo
+
+
+def sstate_get_manifest_filename(task, d):
+ """
+ Return the sstate manifest file path for a particular task.
+ Also returns the datastore that can be used to query related variables.
+ """
+ d2 = d.createCopy()
+ extrainf = d.getVarFlag("do_" + task, 'stamp-extra-info', True)
+ if extrainf:
+ d2.setVar("SSTATE_MANMACH", extrainf)
+ return (d2.expand("${SSTATE_MANFILEPREFIX}.%s" % task), d2)
diff --git a/meta/lib/oe/terminal.py b/meta/lib/oe/terminal.py
new file mode 100644
index 0000000..52a8913
--- /dev/null
+++ b/meta/lib/oe/terminal.py
@@ -0,0 +1,263 @@
+import logging
+import oe.classutils
+import shlex
+from bb.process import Popen, ExecutionError
+from distutils.version import LooseVersion
+
+logger = logging.getLogger('BitBake.OE.Terminal')
+
+
+class UnsupportedTerminal(Exception):
+ pass
+
+class NoSupportedTerminals(Exception):
+ pass
+
+
+class Registry(oe.classutils.ClassRegistry):
+ command = None
+
+ def __init__(cls, name, bases, attrs):
+ super(Registry, cls).__init__(name.lower(), bases, attrs)
+
+ @property
+ def implemented(cls):
+ return bool(cls.command)
+
+
+class Terminal(Popen):
+ __metaclass__ = Registry
+
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ fmt_sh_cmd = self.format_command(sh_cmd, title)
+ try:
+ Popen.__init__(self, fmt_sh_cmd, env=env)
+ except OSError as exc:
+ import errno
+ if exc.errno == errno.ENOENT:
+ raise UnsupportedTerminal(self.name)
+ else:
+ raise
+
+ def format_command(self, sh_cmd, title):
+ fmt = {'title': title or 'Terminal', 'command': sh_cmd}
+ if isinstance(self.command, basestring):
+ return shlex.split(self.command.format(**fmt))
+ else:
+ return [element.format(**fmt) for element in self.command]
+
+class XTerminal(Terminal):
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ Terminal.__init__(self, sh_cmd, title, env, d)
+ if not os.environ.get('DISPLAY'):
+ raise UnsupportedTerminal(self.name)
+
+class Gnome(XTerminal):
+ command = 'gnome-terminal -t "{title}" --disable-factory -x {command}'
+ priority = 2
+
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ # Recent versions of gnome-terminal does not support non-UTF8 charset:
+ # https://bugzilla.gnome.org/show_bug.cgi?id=732127; as a workaround,
+ # clearing the LC_ALL environment variable so it uses the locale.
+ # Once fixed on the gnome-terminal project, this should be removed.
+ if os.getenv('LC_ALL'): os.putenv('LC_ALL','')
+
+ # Check version
+ vernum = check_terminal_version("gnome-terminal")
+ if vernum and LooseVersion(vernum) >= '3.10':
+ logger.debug(1, 'Gnome-Terminal 3.10 or later does not support --disable-factory')
+ self.command = 'gnome-terminal -t "{title}" -x {command}'
+ XTerminal.__init__(self, sh_cmd, title, env, d)
+
+class Mate(XTerminal):
+ command = 'mate-terminal -t "{title}" -x {command}'
+ priority = 2
+
+class Xfce(XTerminal):
+ command = 'xfce4-terminal -T "{title}" -e "{command}"'
+ priority = 2
+
+class Terminology(XTerminal):
+ command = 'terminology -T="{title}" -e {command}'
+ priority = 2
+
+class Konsole(XTerminal):
+ command = 'konsole --nofork -p tabtitle="{title}" -e {command}'
+ priority = 2
+
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ # Check version
+ vernum = check_terminal_version("konsole")
+ if vernum and LooseVersion(vernum) < '2.0.0':
+ # Konsole from KDE 3.x
+ self.command = 'konsole -T "{title}" -e {command}'
+ XTerminal.__init__(self, sh_cmd, title, env, d)
+
+class XTerm(XTerminal):
+ command = 'xterm -T "{title}" -e {command}'
+ priority = 1
+
+class Rxvt(XTerminal):
+ command = 'rxvt -T "{title}" -e {command}'
+ priority = 1
+
+class Screen(Terminal):
+ command = 'screen -D -m -t "{title}" -S devshell {command}'
+
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ s_id = "devshell_%i" % os.getpid()
+ self.command = "screen -D -m -t \"{title}\" -S %s {command}" % s_id
+ Terminal.__init__(self, sh_cmd, title, env, d)
+ msg = 'Screen started. Please connect in another terminal with ' \
+ '"screen -r %s"' % s_id
+ if (d):
+ bb.event.fire(bb.event.LogExecTTY(msg, "screen -r %s" % s_id,
+ 0.5, 10), d)
+ else:
+ logger.warn(msg)
+
+class TmuxRunning(Terminal):
+ """Open a new pane in the current running tmux window"""
+ name = 'tmux-running'
+ command = 'tmux split-window "{command}"'
+ priority = 2.75
+
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ if not bb.utils.which(os.getenv('PATH'), 'tmux'):
+ raise UnsupportedTerminal('tmux is not installed')
+
+ if not os.getenv('TMUX'):
+ raise UnsupportedTerminal('tmux is not running')
+
+ if not check_tmux_pane_size('tmux'):
+ raise UnsupportedTerminal('tmux pane too small')
+
+ Terminal.__init__(self, sh_cmd, title, env, d)
+
+class TmuxNewWindow(Terminal):
+ """Open a new window in the current running tmux session"""
+ name = 'tmux-new-window'
+ command = 'tmux new-window -n "{title}" "{command}"'
+ priority = 2.70
+
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ if not bb.utils.which(os.getenv('PATH'), 'tmux'):
+ raise UnsupportedTerminal('tmux is not installed')
+
+ if not os.getenv('TMUX'):
+ raise UnsupportedTerminal('tmux is not running')
+
+ Terminal.__init__(self, sh_cmd, title, env, d)
+
+class Tmux(Terminal):
+ """Start a new tmux session and window"""
+ command = 'tmux new -d -s devshell -n devshell "{command}"'
+ priority = 0.75
+
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ if not bb.utils.which(os.getenv('PATH'), 'tmux'):
+ raise UnsupportedTerminal('tmux is not installed')
+
+ # TODO: consider using a 'devshell' session shared amongst all
+ # devshells, if it's already there, add a new window to it.
+ window_name = 'devshell-%i' % os.getpid()
+
+ self.command = 'tmux new -d -s {0} -n {0} "{{command}}"'.format(window_name)
+ Terminal.__init__(self, sh_cmd, title, env, d)
+
+ attach_cmd = 'tmux att -t {0}'.format(window_name)
+ msg = 'Tmux started. Please connect in another terminal with `tmux att -t {0}`'.format(window_name)
+ if d:
+ bb.event.fire(bb.event.LogExecTTY(msg, attach_cmd, 0.5, 10), d)
+ else:
+ logger.warn(msg)
+
+class Custom(Terminal):
+ command = 'false' # This is a placeholder
+ priority = 3
+
+ def __init__(self, sh_cmd, title=None, env=None, d=None):
+ self.command = d and d.getVar('OE_TERMINAL_CUSTOMCMD', True)
+ if self.command:
+ if not '{command}' in self.command:
+ self.command += ' {command}'
+ Terminal.__init__(self, sh_cmd, title, env, d)
+ logger.warn('Custom terminal was started.')
+ else:
+ logger.debug(1, 'No custom terminal (OE_TERMINAL_CUSTOMCMD) set')
+ raise UnsupportedTerminal('OE_TERMINAL_CUSTOMCMD not set')
+
+
+def prioritized():
+ return Registry.prioritized()
+
+def spawn_preferred(sh_cmd, title=None, env=None, d=None):
+ """Spawn the first supported terminal, by priority"""
+ for terminal in prioritized():
+ try:
+ spawn(terminal.name, sh_cmd, title, env, d)
+ break
+ except UnsupportedTerminal:
+ continue
+ else:
+ raise NoSupportedTerminals()
+
+def spawn(name, sh_cmd, title=None, env=None, d=None):
+ """Spawn the specified terminal, by name"""
+ logger.debug(1, 'Attempting to spawn terminal "%s"', name)
+ try:
+ terminal = Registry.registry[name]
+ except KeyError:
+ raise UnsupportedTerminal(name)
+
+ pipe = terminal(sh_cmd, title, env, d)
+ output = pipe.communicate()[0]
+ if pipe.returncode != 0:
+ raise ExecutionError(sh_cmd, pipe.returncode, output)
+
+def check_tmux_pane_size(tmux):
+ import subprocess as sub
+ try:
+ p = sub.Popen('%s list-panes -F "#{?pane_active,#{pane_height},}"' % tmux,
+ shell=True,stdout=sub.PIPE,stderr=sub.PIPE)
+ out, err = p.communicate()
+ size = int(out.strip())
+ except OSError as exc:
+ import errno
+ if exc.errno == errno.ENOENT:
+ return None
+ else:
+ raise
+ if size/2 >= 19:
+ return True
+ return False
+
+def check_terminal_version(terminalName):
+ import subprocess as sub
+ try:
+ p = sub.Popen(['sh', '-c', '%s --version' % terminalName],stdout=sub.PIPE,stderr=sub.PIPE)
+ out, err = p.communicate()
+ ver_info = out.rstrip().split('\n')
+ except OSError as exc:
+ import errno
+ if exc.errno == errno.ENOENT:
+ return None
+ else:
+ raise
+ vernum = None
+ for ver in ver_info:
+ if ver.startswith('Konsole'):
+ vernum = ver.split(' ')[-1]
+ if ver.startswith('GNOME Terminal'):
+ vernum = ver.split(' ')[-1]
+ return vernum
+
+def distro_name():
+ try:
+ p = Popen(['lsb_release', '-i'])
+ out, err = p.communicate()
+ distro = out.split(':')[1].strip().lower()
+ except:
+ distro = "unknown"
+ return distro
diff --git a/meta/lib/oe/tests/__init__.py b/meta/lib/oe/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meta/lib/oe/tests/__init__.py
diff --git a/meta/lib/oe/tests/test_license.py b/meta/lib/oe/tests/test_license.py
new file mode 100644
index 0000000..c388886
--- /dev/null
+++ b/meta/lib/oe/tests/test_license.py
@@ -0,0 +1,68 @@
+import unittest
+import oe.license
+
+class SeenVisitor(oe.license.LicenseVisitor):
+ def __init__(self):
+ self.seen = []
+ oe.license.LicenseVisitor.__init__(self)
+
+ def visit_Str(self, node):
+ self.seen.append(node.s)
+
+class TestSingleLicense(unittest.TestCase):
+ licenses = [
+ "GPLv2",
+ "LGPL-2.0",
+ "Artistic",
+ "MIT",
+ "GPLv3+",
+ "FOO_BAR",
+ ]
+ invalid_licenses = ["GPL/BSD"]
+
+ @staticmethod
+ def parse(licensestr):
+ visitor = SeenVisitor()
+ visitor.visit_string(licensestr)
+ return visitor.seen
+
+ def test_single_licenses(self):
+ for license in self.licenses:
+ licenses = self.parse(license)
+ self.assertListEqual(licenses, [license])
+
+ def test_invalid_licenses(self):
+ for license in self.invalid_licenses:
+ with self.assertRaises(oe.license.InvalidLicense) as cm:
+ self.parse(license)
+ self.assertEqual(cm.exception.license, license)
+
+class TestSimpleCombinations(unittest.TestCase):
+ tests = {
+ "FOO&BAR": ["FOO", "BAR"],
+ "BAZ & MOO": ["BAZ", "MOO"],
+ "ALPHA|BETA": ["ALPHA"],
+ "BAZ&MOO|FOO": ["FOO"],
+ "FOO&BAR|BAZ": ["FOO", "BAR"],
+ }
+ preferred = ["ALPHA", "FOO", "BAR"]
+
+ def test_tests(self):
+ def choose(a, b):
+ if all(lic in self.preferred for lic in b):
+ return b
+ else:
+ return a
+
+ for license, expected in self.tests.items():
+ licenses = oe.license.flattened_licenses(license, choose)
+ self.assertListEqual(licenses, expected)
+
+class TestComplexCombinations(TestSimpleCombinations):
+ tests = {
+ "FOO & (BAR | BAZ)&MOO": ["FOO", "BAR", "MOO"],
+ "(ALPHA|(BETA&THETA)|OMEGA)&DELTA": ["OMEGA", "DELTA"],
+ "((ALPHA|BETA)&FOO)|BAZ": ["BETA", "FOO"],
+ "(GPL-2.0|Proprietary)&BSD-4-clause&MIT": ["GPL-2.0", "BSD-4-clause", "MIT"],
+ }
+ preferred = ["BAR", "OMEGA", "BETA", "GPL-2.0"]
diff --git a/meta/lib/oe/tests/test_path.py b/meta/lib/oe/tests/test_path.py
new file mode 100644
index 0000000..3d41ce1
--- /dev/null
+++ b/meta/lib/oe/tests/test_path.py
@@ -0,0 +1,89 @@
+import unittest
+import oe, oe.path
+import tempfile
+import os
+import errno
+import shutil
+
+class TestRealPath(unittest.TestCase):
+ DIRS = [ "a", "b", "etc", "sbin", "usr", "usr/bin", "usr/binX", "usr/sbin", "usr/include", "usr/include/gdbm" ]
+ FILES = [ "etc/passwd", "b/file" ]
+ LINKS = [
+ ( "bin", "/usr/bin", "/usr/bin" ),
+ ( "binX", "usr/binX", "/usr/binX" ),
+ ( "c", "broken", "/broken" ),
+ ( "etc/passwd-1", "passwd", "/etc/passwd" ),
+ ( "etc/passwd-2", "passwd-1", "/etc/passwd" ),
+ ( "etc/passwd-3", "/etc/passwd-1", "/etc/passwd" ),
+ ( "etc/shadow-1", "/etc/shadow", "/etc/shadow" ),
+ ( "etc/shadow-2", "/etc/shadow-1", "/etc/shadow" ),
+ ( "prog-A", "bin/prog-A", "/usr/bin/prog-A" ),
+ ( "prog-B", "/bin/prog-B", "/usr/bin/prog-B" ),
+ ( "usr/bin/prog-C", "../../sbin/prog-C", "/sbin/prog-C" ),
+ ( "usr/bin/prog-D", "/sbin/prog-D", "/sbin/prog-D" ),
+ ( "usr/binX/prog-E", "../sbin/prog-E", None ),
+ ( "usr/bin/prog-F", "../../../sbin/prog-F", "/sbin/prog-F" ),
+ ( "loop", "a/loop", None ),
+ ( "a/loop", "../loop", None ),
+ ( "b/test", "file/foo", "/b/file/foo" ),
+ ]
+
+ LINKS_PHYS = [
+ ( "./", "/", "" ),
+ ( "binX/prog-E", "/usr/sbin/prog-E", "/sbin/prog-E" ),
+ ]
+
+ EXCEPTIONS = [
+ ( "loop", errno.ELOOP ),
+ ( "b/test", errno.ENOENT ),
+ ]
+
+ def __del__(self):
+ try:
+ #os.system("tree -F %s" % self.tmpdir)
+ shutil.rmtree(self.tmpdir)
+ except:
+ pass
+
+ def setUp(self):
+ self.tmpdir = tempfile.mkdtemp(prefix = "oe-test_path")
+ self.root = os.path.join(self.tmpdir, "R")
+
+ os.mkdir(os.path.join(self.tmpdir, "_real"))
+ os.symlink("_real", self.root)
+
+ for d in self.DIRS:
+ os.mkdir(os.path.join(self.root, d))
+ for f in self.FILES:
+ file(os.path.join(self.root, f), "w")
+ for l in self.LINKS:
+ os.symlink(l[1], os.path.join(self.root, l[0]))
+
+ def __realpath(self, file, use_physdir, assume_dir = True):
+ return oe.path.realpath(os.path.join(self.root, file), self.root,
+ use_physdir, assume_dir = assume_dir)
+
+ def test_norm(self):
+ for l in self.LINKS:
+ if l[2] == None:
+ continue
+
+ target_p = self.__realpath(l[0], True)
+ target_l = self.__realpath(l[0], False)
+
+ if l[2] != False:
+ self.assertEqual(target_p, target_l)
+ self.assertEqual(l[2], target_p[len(self.root):])
+
+ def test_phys(self):
+ for l in self.LINKS_PHYS:
+ target_p = self.__realpath(l[0], True)
+ target_l = self.__realpath(l[0], False)
+
+ self.assertEqual(l[1], target_p[len(self.root):])
+ self.assertEqual(l[2], target_l[len(self.root):])
+
+ def test_loop(self):
+ for e in self.EXCEPTIONS:
+ self.assertRaisesRegexp(OSError, r'\[Errno %u\]' % e[1],
+ self.__realpath, e[0], False, False)
diff --git a/meta/lib/oe/tests/test_types.py b/meta/lib/oe/tests/test_types.py
new file mode 100644
index 0000000..367cc30
--- /dev/null
+++ b/meta/lib/oe/tests/test_types.py
@@ -0,0 +1,62 @@
+import unittest
+from oe.maketype import create, factory
+
+class TestTypes(unittest.TestCase):
+ def assertIsInstance(self, obj, cls):
+ return self.assertTrue(isinstance(obj, cls))
+
+ def assertIsNot(self, obj, other):
+ return self.assertFalse(obj is other)
+
+ def assertFactoryCreated(self, value, type, **flags):
+ cls = factory(type)
+ self.assertIsNot(cls, None)
+ self.assertIsInstance(create(value, type, **flags), cls)
+
+class TestBooleanType(TestTypes):
+ def test_invalid(self):
+ self.assertRaises(ValueError, create, '', 'boolean')
+ self.assertRaises(ValueError, create, 'foo', 'boolean')
+ self.assertRaises(TypeError, create, object(), 'boolean')
+
+ def test_true(self):
+ self.assertTrue(create('y', 'boolean'))
+ self.assertTrue(create('yes', 'boolean'))
+ self.assertTrue(create('1', 'boolean'))
+ self.assertTrue(create('t', 'boolean'))
+ self.assertTrue(create('true', 'boolean'))
+ self.assertTrue(create('TRUE', 'boolean'))
+ self.assertTrue(create('truE', 'boolean'))
+
+ def test_false(self):
+ self.assertFalse(create('n', 'boolean'))
+ self.assertFalse(create('no', 'boolean'))
+ self.assertFalse(create('0', 'boolean'))
+ self.assertFalse(create('f', 'boolean'))
+ self.assertFalse(create('false', 'boolean'))
+ self.assertFalse(create('FALSE', 'boolean'))
+ self.assertFalse(create('faLse', 'boolean'))
+
+ def test_bool_equality(self):
+ self.assertEqual(create('n', 'boolean'), False)
+ self.assertNotEqual(create('n', 'boolean'), True)
+ self.assertEqual(create('y', 'boolean'), True)
+ self.assertNotEqual(create('y', 'boolean'), False)
+
+class TestList(TestTypes):
+ def assertListEqual(self, value, valid, sep=None):
+ obj = create(value, 'list', separator=sep)
+ self.assertEqual(obj, valid)
+ if sep is not None:
+ self.assertEqual(obj.separator, sep)
+ self.assertEqual(str(obj), obj.separator.join(obj))
+
+ def test_list_nosep(self):
+ testlist = ['alpha', 'beta', 'theta']
+ self.assertListEqual('alpha beta theta', testlist)
+ self.assertListEqual('alpha beta\ttheta', testlist)
+ self.assertListEqual('alpha', ['alpha'])
+
+ def test_list_usersep(self):
+ self.assertListEqual('foo:bar', ['foo', 'bar'], ':')
+ self.assertListEqual('foo:bar:baz', ['foo', 'bar', 'baz'], ':')
diff --git a/meta/lib/oe/tests/test_utils.py b/meta/lib/oe/tests/test_utils.py
new file mode 100644
index 0000000..5d9ac52
--- /dev/null
+++ b/meta/lib/oe/tests/test_utils.py
@@ -0,0 +1,51 @@
+import unittest
+from oe.utils import packages_filter_out_system
+
+class TestPackagesFilterOutSystem(unittest.TestCase):
+ def test_filter(self):
+ """
+ Test that oe.utils.packages_filter_out_system works.
+ """
+ try:
+ import bb
+ except ImportError:
+ self.skipTest("Cannot import bb")
+
+ d = bb.data_smart.DataSmart()
+ d.setVar("PN", "foo")
+
+ d.setVar("PACKAGES", "foo foo-doc foo-dev")
+ pkgs = packages_filter_out_system(d)
+ self.assertEqual(pkgs, [])
+
+ d.setVar("PACKAGES", "foo foo-doc foo-data foo-dev")
+ pkgs = packages_filter_out_system(d)
+ self.assertEqual(pkgs, ["foo-data"])
+
+ d.setVar("PACKAGES", "foo foo-locale-en-gb")
+ pkgs = packages_filter_out_system(d)
+ self.assertEqual(pkgs, [])
+
+ d.setVar("PACKAGES", "foo foo-data foo-locale-en-gb")
+ pkgs = packages_filter_out_system(d)
+ self.assertEqual(pkgs, ["foo-data"])
+
+
+class TestTrimVersion(unittest.TestCase):
+ def test_version_exception(self):
+ with self.assertRaises(TypeError):
+ trim_version(None, 2)
+ with self.assertRaises(TypeError):
+ trim_version((1, 2, 3), 2)
+
+ def test_num_exception(self):
+ with self.assertRaises(ValueError):
+ trim_version("1.2.3", 0)
+ with self.assertRaises(ValueError):
+ trim_version("1.2.3", -1)
+
+ def test_valid(self):
+ self.assertEqual(trim_version("1.2.3", 1), "1")
+ self.assertEqual(trim_version("1.2.3", 2), "1.2")
+ self.assertEqual(trim_version("1.2.3", 3), "1.2.3")
+ self.assertEqual(trim_version("1.2.3", 4), "1.2.3")
diff --git a/meta/lib/oe/types.py b/meta/lib/oe/types.py
new file mode 100644
index 0000000..7f47c17
--- /dev/null
+++ b/meta/lib/oe/types.py
@@ -0,0 +1,153 @@
+import errno
+import re
+import os
+
+
+class OEList(list):
+ """OpenEmbedded 'list' type
+
+ Acts as an ordinary list, but is constructed from a string value and a
+ separator (optional), and re-joins itself when converted to a string with
+ str(). Set the variable type flag to 'list' to use this type, and the
+ 'separator' flag may be specified (defaulting to whitespace)."""
+
+ name = "list"
+
+ def __init__(self, value, separator = None):
+ if value is not None:
+ list.__init__(self, value.split(separator))
+ else:
+ list.__init__(self)
+
+ if separator is None:
+ self.separator = " "
+ else:
+ self.separator = separator
+
+ def __str__(self):
+ return self.separator.join(self)
+
+def choice(value, choices):
+ """OpenEmbedded 'choice' type
+
+ Acts as a multiple choice for the user. To use this, set the variable
+ type flag to 'choice', and set the 'choices' flag to a space separated
+ list of valid values."""
+ if not isinstance(value, basestring):
+ raise TypeError("choice accepts a string, not '%s'" % type(value))
+
+ value = value.lower()
+ choices = choices.lower()
+ if value not in choices.split():
+ raise ValueError("Invalid choice '%s'. Valid choices: %s" %
+ (value, choices))
+ return value
+
+class NoMatch(object):
+ """Stub python regex pattern object which never matches anything"""
+ def findall(self, string, flags=0):
+ return None
+
+ def finditer(self, string, flags=0):
+ return None
+
+ def match(self, flags=0):
+ return None
+
+ def search(self, string, flags=0):
+ return None
+
+ def split(self, string, maxsplit=0):
+ return None
+
+ def sub(pattern, repl, string, count=0):
+ return None
+
+ def subn(pattern, repl, string, count=0):
+ return None
+
+NoMatch = NoMatch()
+
+def regex(value, regexflags=None):
+ """OpenEmbedded 'regex' type
+
+ Acts as a regular expression, returning the pre-compiled regular
+ expression pattern object. To use this type, set the variable type flag
+ to 'regex', and optionally, set the 'regexflags' type to a space separated
+ list of the flags to control the regular expression matching (e.g.
+ FOO[regexflags] += 'ignorecase'). See the python documentation on the
+ 're' module for a list of valid flags."""
+
+ flagval = 0
+ if regexflags:
+ for flag in regexflags.split():
+ flag = flag.upper()
+ try:
+ flagval |= getattr(re, flag)
+ except AttributeError:
+ raise ValueError("Invalid regex flag '%s'" % flag)
+
+ if not value:
+ # Let's ensure that the default behavior for an undefined or empty
+ # variable is to match nothing. If the user explicitly wants to match
+ # anything, they can match '.*' instead.
+ return NoMatch
+
+ try:
+ return re.compile(value, flagval)
+ except re.error as exc:
+ raise ValueError("Invalid regex value '%s': %s" %
+ (value, exc.args[0]))
+
+def boolean(value):
+ """OpenEmbedded 'boolean' type
+
+ Valid values for true: 'yes', 'y', 'true', 't', '1'
+ Valid values for false: 'no', 'n', 'false', 'f', '0'
+ """
+
+ if not isinstance(value, basestring):
+ raise TypeError("boolean accepts a string, not '%s'" % type(value))
+
+ value = value.lower()
+ if value in ('yes', 'y', 'true', 't', '1'):
+ return True
+ elif value in ('no', 'n', 'false', 'f', '0'):
+ return False
+ raise ValueError("Invalid boolean value '%s'" % value)
+
+def integer(value, numberbase=10):
+ """OpenEmbedded 'integer' type
+
+ Defaults to base 10, but this can be specified using the optional
+ 'numberbase' flag."""
+
+ return int(value, int(numberbase))
+
+_float = float
+def float(value, fromhex='false'):
+ """OpenEmbedded floating point type
+
+ To use this type, set the type flag to 'float', and optionally set the
+ 'fromhex' flag to a true value (obeying the same rules as for the
+ 'boolean' type) if the value is in base 16 rather than base 10."""
+
+ if boolean(fromhex):
+ return _float.fromhex(value)
+ else:
+ return _float(value)
+
+def path(value, relativeto='', normalize='true', mustexist='false'):
+ value = os.path.join(relativeto, value)
+
+ if boolean(normalize):
+ value = os.path.normpath(value)
+
+ if boolean(mustexist):
+ try:
+ open(value, 'r')
+ except IOError as exc:
+ if exc.errno == errno.ENOENT:
+ raise ValueError("{0}: {1}".format(value, os.strerror(errno.ENOENT)))
+
+ return value
diff --git a/meta/lib/oe/utils.py b/meta/lib/oe/utils.py
new file mode 100644
index 0000000..cee087f
--- /dev/null
+++ b/meta/lib/oe/utils.py
@@ -0,0 +1,273 @@
+try:
+ # Python 2
+ import commands as cmdstatus
+except ImportError:
+ # Python 3
+ import subprocess as cmdstatus
+
+def read_file(filename):
+ try:
+ f = open( filename, "r" )
+ except IOError as reason:
+ return "" # WARNING: can't raise an error now because of the new RDEPENDS handling. This is a bit ugly. :M:
+ else:
+ data = f.read().strip()
+ f.close()
+ return data
+ return None
+
+def ifelse(condition, iftrue = True, iffalse = False):
+ if condition:
+ return iftrue
+ else:
+ return iffalse
+
+def conditional(variable, checkvalue, truevalue, falsevalue, d):
+ if d.getVar(variable,1) == checkvalue:
+ return truevalue
+ else:
+ return falsevalue
+
+def less_or_equal(variable, checkvalue, truevalue, falsevalue, d):
+ if float(d.getVar(variable,1)) <= float(checkvalue):
+ return truevalue
+ else:
+ return falsevalue
+
+def version_less_or_equal(variable, checkvalue, truevalue, falsevalue, d):
+ result = bb.utils.vercmp_string(d.getVar(variable,True), checkvalue)
+ if result <= 0:
+ return truevalue
+ else:
+ return falsevalue
+
+def both_contain(variable1, variable2, checkvalue, d):
+ val1 = d.getVar(variable1, True)
+ val2 = d.getVar(variable2, True)
+ val1 = set(val1.split())
+ val2 = set(val2.split())
+ if isinstance(checkvalue, basestring):
+ checkvalue = set(checkvalue.split())
+ else:
+ checkvalue = set(checkvalue)
+ if checkvalue.issubset(val1) and checkvalue.issubset(val2):
+ return " ".join(checkvalue)
+ else:
+ return ""
+
+def set_intersect(variable1, variable2, d):
+ """
+ Expand both variables, interpret them as lists of strings, and return the
+ intersection as a flattened string.
+
+ For example:
+ s1 = "a b c"
+ s2 = "b c d"
+ s3 = set_intersect(s1, s2)
+ => s3 = "b c"
+ """
+ val1 = set(d.getVar(variable1, True).split())
+ val2 = set(d.getVar(variable2, True).split())
+ return " ".join(val1 & val2)
+
+def prune_suffix(var, suffixes, d):
+ # See if var ends with any of the suffixes listed and
+ # remove it if found
+ for suffix in suffixes:
+ if var.endswith(suffix):
+ var = var.replace(suffix, "")
+
+ prefix = d.getVar("MLPREFIX", True)
+ if prefix and var.startswith(prefix):
+ var = var.replace(prefix, "")
+
+ return var
+
+def str_filter(f, str, d):
+ from re import match
+ return " ".join(filter(lambda x: match(f, x, 0), str.split()))
+
+def str_filter_out(f, str, d):
+ from re import match
+ return " ".join(filter(lambda x: not match(f, x, 0), str.split()))
+
+def param_bool(cfg, field, dflt = None):
+ """Lookup <field> in <cfg> map and convert it to a boolean; take
+ <dflt> when this <field> does not exist"""
+ value = cfg.get(field, dflt)
+ strvalue = str(value).lower()
+ if strvalue in ('yes', 'y', 'true', 't', '1'):
+ return True
+ elif strvalue in ('no', 'n', 'false', 'f', '0'):
+ return False
+ raise ValueError("invalid value for boolean parameter '%s': '%s'" % (field, value))
+
+def inherits(d, *classes):
+ """Return True if the metadata inherits any of the specified classes"""
+ return any(bb.data.inherits_class(cls, d) for cls in classes)
+
+def features_backfill(var,d):
+ # This construct allows the addition of new features to variable specified
+ # as var
+ # Example for var = "DISTRO_FEATURES"
+ # This construct allows the addition of new features to DISTRO_FEATURES
+ # that if not present would disable existing functionality, without
+ # disturbing distributions that have already set DISTRO_FEATURES.
+ # Distributions wanting to elide a value in DISTRO_FEATURES_BACKFILL should
+ # add the feature to DISTRO_FEATURES_BACKFILL_CONSIDERED
+ features = (d.getVar(var, True) or "").split()
+ backfill = (d.getVar(var+"_BACKFILL", True) or "").split()
+ considered = (d.getVar(var+"_BACKFILL_CONSIDERED", True) or "").split()
+
+ addfeatures = []
+ for feature in backfill:
+ if feature not in features and feature not in considered:
+ addfeatures.append(feature)
+
+ if addfeatures:
+ d.appendVar(var, " " + " ".join(addfeatures))
+
+
+def packages_filter_out_system(d):
+ """
+ Return a list of packages from PACKAGES with the "system" packages such as
+ PN-dbg PN-doc PN-locale-eb-gb removed.
+ """
+ pn = d.getVar('PN', True)
+ blacklist = map(lambda suffix: pn + suffix, ('', '-dbg', '-dev', '-doc', '-locale', '-staticdev'))
+ localepkg = pn + "-locale-"
+ pkgs = []
+
+ for pkg in d.getVar('PACKAGES', True).split():
+ if pkg not in blacklist and localepkg not in pkg:
+ pkgs.append(pkg)
+ return pkgs
+
+def getstatusoutput(cmd):
+ return cmdstatus.getstatusoutput(cmd)
+
+
+def trim_version(version, num_parts=2):
+ """
+ Return just the first <num_parts> of <version>, split by periods. For
+ example, trim_version("1.2.3", 2) will return "1.2".
+ """
+ if type(version) is not str:
+ raise TypeError("Version should be a string")
+ if num_parts < 1:
+ raise ValueError("Cannot split to parts < 1")
+
+ parts = version.split(".")
+ trimmed = ".".join(parts[:num_parts])
+ return trimmed
+
+def cpu_count():
+ import multiprocessing
+ return multiprocessing.cpu_count()
+
+def execute_pre_post_process(d, cmds):
+ if cmds is None:
+ return
+
+ for cmd in cmds.strip().split(';'):
+ cmd = cmd.strip()
+ if cmd != '':
+ bb.note("Executing %s ..." % cmd)
+ bb.build.exec_func(cmd, d)
+
+def multiprocess_exec(commands, function):
+ import signal
+ import multiprocessing
+
+ if not commands:
+ return []
+
+ def init_worker():
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+ nproc = min(multiprocessing.cpu_count(), len(commands))
+ pool = bb.utils.multiprocessingpool(nproc, init_worker)
+ imap = pool.imap(function, commands)
+
+ try:
+ res = list(imap)
+ pool.close()
+ pool.join()
+ results = []
+ for result in res:
+ if result is not None:
+ results.append(result)
+ return results
+
+ except KeyboardInterrupt:
+ pool.terminate()
+ pool.join()
+ raise
+
+def squashspaces(string):
+ import re
+ return re.sub("\s+", " ", string).strip()
+
+#
+# Python 2.7 doesn't have threaded pools (just multiprocessing)
+# so implement a version here
+#
+
+from Queue import Queue
+from threading import Thread
+
+class ThreadedWorker(Thread):
+ """Thread executing tasks from a given tasks queue"""
+ def __init__(self, tasks, worker_init, worker_end):
+ Thread.__init__(self)
+ self.tasks = tasks
+ self.daemon = True
+
+ self.worker_init = worker_init
+ self.worker_end = worker_end
+
+ def run(self):
+ from Queue import Empty
+
+ if self.worker_init is not None:
+ self.worker_init(self)
+
+ while True:
+ try:
+ func, args, kargs = self.tasks.get(block=False)
+ except Empty:
+ if self.worker_end is not None:
+ self.worker_end(self)
+ break
+
+ try:
+ func(self, *args, **kargs)
+ except Exception, e:
+ print e
+ finally:
+ self.tasks.task_done()
+
+class ThreadedPool:
+ """Pool of threads consuming tasks from a queue"""
+ def __init__(self, num_workers, num_tasks, worker_init=None,
+ worker_end=None):
+ self.tasks = Queue(num_tasks)
+ self.workers = []
+
+ for _ in range(num_workers):
+ worker = ThreadedWorker(self.tasks, worker_init, worker_end)
+ self.workers.append(worker)
+
+ def start(self):
+ for worker in self.workers:
+ worker.start()
+
+ def add_task(self, func, *args, **kargs):
+ """Add a task to the queue"""
+ self.tasks.put((func, args, kargs))
+
+ def wait_completion(self):
+ """Wait for completion of all the tasks in the queue"""
+ self.tasks.join()
+ for worker in self.workers:
+ worker.join()
diff --git a/meta/lib/oeqa/__init__.py b/meta/lib/oeqa/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meta/lib/oeqa/__init__.py
diff --git a/meta/lib/oeqa/controllers/__init__.py b/meta/lib/oeqa/controllers/__init__.py
new file mode 100644
index 0000000..8eda927
--- /dev/null
+++ b/meta/lib/oeqa/controllers/__init__.py
@@ -0,0 +1,3 @@
+# Enable other layers to have modules in the same named directory
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
diff --git a/meta/lib/oeqa/controllers/masterimage.py b/meta/lib/oeqa/controllers/masterimage.py
new file mode 100644
index 0000000..522f9eb
--- /dev/null
+++ b/meta/lib/oeqa/controllers/masterimage.py
@@ -0,0 +1,201 @@
+# Copyright (C) 2014 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# This module adds support to testimage.bbclass to deploy images and run
+# tests using a "master image" - this is a "known good" image that is
+# installed onto the device as part of initial setup and will be booted into
+# with no interaction; we can then use it to deploy the image to be tested
+# to a second partition before running the tests.
+#
+# For an example master image, see core-image-testmaster
+# (meta/recipes-extended/images/core-image-testmaster.bb)
+
+import os
+import bb
+import traceback
+import time
+import subprocess
+
+import oeqa.targetcontrol
+import oeqa.utils.sshcontrol as sshcontrol
+import oeqa.utils.commands as commands
+from oeqa.utils import CommandError
+
+from abc import ABCMeta, abstractmethod
+
+class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget):
+
+ __metaclass__ = ABCMeta
+
+ supported_image_fstypes = ['tar.gz', 'tar.bz2']
+
+ def __init__(self, d):
+ super(MasterImageHardwareTarget, self).__init__(d)
+
+ # target ip
+ addr = d.getVar("TEST_TARGET_IP", True) or bb.fatal('Please set TEST_TARGET_IP with the IP address of the machine you want to run the tests on.')
+ self.ip = addr.split(":")[0]
+ try:
+ self.port = addr.split(":")[1]
+ except IndexError:
+ self.port = None
+ bb.note("Target IP: %s" % self.ip)
+ self.server_ip = d.getVar("TEST_SERVER_IP", True)
+ if not self.server_ip:
+ try:
+ self.server_ip = subprocess.check_output(['ip', 'route', 'get', self.ip ]).split("\n")[0].split()[-1]
+ except Exception as e:
+ bb.fatal("Failed to determine the host IP address (alternatively you can set TEST_SERVER_IP with the IP address of this machine): %s" % e)
+ bb.note("Server IP: %s" % self.server_ip)
+
+ # test rootfs + kernel
+ self.image_fstype = self.get_image_fstype(d)
+ self.rootfs = os.path.join(d.getVar("DEPLOY_DIR_IMAGE", True), d.getVar("IMAGE_LINK_NAME", True) + '.' + self.image_fstype)
+ self.kernel = os.path.join(d.getVar("DEPLOY_DIR_IMAGE", True), d.getVar("KERNEL_IMAGETYPE", False) + '-' + d.getVar('MACHINE', False) + '.bin')
+ if not os.path.isfile(self.rootfs):
+ # we could've checked that IMAGE_FSTYPES contains tar.gz but the config for running testimage might not be
+ # the same as the config with which the image was build, ie
+ # you bitbake core-image-sato with IMAGE_FSTYPES += "tar.gz"
+ # and your autobuilder overwrites the config, adds the test bits and runs bitbake core-image-sato -c testimage
+ bb.fatal("No rootfs found. Did you build the image ?\nIf yes, did you build it with IMAGE_FSTYPES += \"tar.gz\" ? \
+ \nExpected path: %s" % self.rootfs)
+ if not os.path.isfile(self.kernel):
+ bb.fatal("No kernel found. Expected path: %s" % self.kernel)
+
+ # master ssh connection
+ self.master = None
+ # if the user knows what they are doing, then by all means...
+ self.user_cmds = d.getVar("TEST_DEPLOY_CMDS", True)
+ self.deploy_cmds = None
+
+ # this is the name of the command that controls the power for a board
+ # e.g: TEST_POWERCONTROL_CMD = "/home/user/myscripts/powercontrol.py ${MACHINE} what-ever-other-args-the-script-wants"
+ # the command should take as the last argument "off" and "on" and "cycle" (off, on)
+ self.powercontrol_cmd = d.getVar("TEST_POWERCONTROL_CMD", True) or None
+ self.powercontrol_args = d.getVar("TEST_POWERCONTROL_EXTRA_ARGS", False) or ""
+
+ self.serialcontrol_cmd = d.getVar("TEST_SERIALCONTROL_CMD", True) or None
+ self.serialcontrol_args = d.getVar("TEST_SERIALCONTROL_EXTRA_ARGS", False) or ""
+
+ self.origenv = os.environ
+ if self.powercontrol_cmd or self.serialcontrol_cmd:
+ # the external script for controlling power might use ssh
+ # ssh + keys means we need the original user env
+ bborigenv = d.getVar("BB_ORIGENV", False) or {}
+ for key in bborigenv:
+ val = bborigenv.getVar(key, True)
+ if val is not None:
+ self.origenv[key] = str(val)
+
+ if self.powercontrol_cmd:
+ if self.powercontrol_args:
+ self.powercontrol_cmd = "%s %s" % (self.powercontrol_cmd, self.powercontrol_args)
+ if self.serialcontrol_cmd:
+ if self.serialcontrol_args:
+ self.serialcontrol_cmd = "%s %s" % (self.serialcontrol_cmd, self.serialcontrol_args)
+
+ def power_ctl(self, msg):
+ if self.powercontrol_cmd:
+ cmd = "%s %s" % (self.powercontrol_cmd, msg)
+ try:
+ commands.runCmd(cmd, assert_error=False, preexec_fn=os.setsid, env=self.origenv)
+ except CommandError as e:
+ bb.fatal(str(e))
+
+ def power_cycle(self, conn):
+ if self.powercontrol_cmd:
+ # be nice, don't just cut power
+ conn.run("shutdown -h now")
+ time.sleep(10)
+ self.power_ctl("cycle")
+ else:
+ status, output = conn.run("reboot")
+ if status != 0:
+ bb.error("Failed rebooting target and no power control command defined. You need to manually reset the device.\n%s" % output)
+
+ def _wait_until_booted(self):
+ ''' Waits until the target device has booted (if we have just power cycled it) '''
+ # Subclasses with better methods of determining boot can override this
+ time.sleep(120)
+
+ def deploy(self):
+ # base class just sets the ssh log file for us
+ super(MasterImageHardwareTarget, self).deploy()
+ self.master = sshcontrol.SSHControl(ip=self.ip, logfile=self.sshlog, timeout=600, port=self.port)
+ status, output = self.master.run("cat /etc/masterimage")
+ if status != 0:
+ # We're not booted into the master image, so try rebooting
+ bb.plain("%s - booting into the master image" % self.pn)
+ self.power_ctl("cycle")
+ self._wait_until_booted()
+
+ bb.plain("%s - deploying image on target" % self.pn)
+ status, output = self.master.run("cat /etc/masterimage")
+ if status != 0:
+ bb.fatal("No ssh connectivity or target isn't running a master image.\n%s" % output)
+ if self.user_cmds:
+ self.deploy_cmds = self.user_cmds.split("\n")
+ try:
+ self._deploy()
+ except Exception as e:
+ bb.fatal("Failed deploying test image: %s" % e)
+
+ @abstractmethod
+ def _deploy(self):
+ pass
+
+ def start(self, params=None):
+ bb.plain("%s - boot test image on target" % self.pn)
+ self._start()
+ # set the ssh object for the target/test image
+ self.connection = sshcontrol.SSHControl(self.ip, logfile=self.sshlog, port=self.port)
+ bb.plain("%s - start running tests" % self.pn)
+
+ @abstractmethod
+ def _start(self):
+ pass
+
+ def stop(self):
+ bb.plain("%s - reboot/powercycle target" % self.pn)
+ self.power_cycle(self.connection)
+
+
+class GummibootTarget(MasterImageHardwareTarget):
+
+ def __init__(self, d):
+ super(GummibootTarget, self).__init__(d)
+ # this the value we need to set in the LoaderEntryOneShot EFI variable
+ # so the system boots the 'test' bootloader label and not the default
+ # The first four bytes are EFI bits, and the rest is an utf-16le string
+ # (EFI vars values need to be utf-16)
+ # $ echo -en "test\0" | iconv -f ascii -t utf-16le | hexdump -C
+ # 00000000 74 00 65 00 73 00 74 00 00 00 |t.e.s.t...|
+ self.efivarvalue = r'\x07\x00\x00\x00\x74\x00\x65\x00\x73\x00\x74\x00\x00\x00'
+ self.deploy_cmds = [
+ 'mount -L boot /boot',
+ 'mkdir -p /mnt/testrootfs',
+ 'mount -L testrootfs /mnt/testrootfs',
+ 'modprobe efivarfs',
+ 'mount -t efivarfs efivarfs /sys/firmware/efi/efivars',
+ 'cp ~/test-kernel /boot',
+ 'rm -rf /mnt/testrootfs/*',
+ 'tar xvf ~/test-rootfs.%s -C /mnt/testrootfs' % self.image_fstype,
+ 'printf "%s" > /sys/firmware/efi/efivars/LoaderEntryOneShot-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f' % self.efivarvalue
+ ]
+
+ def _deploy(self):
+ # make sure these aren't mounted
+ self.master.run("umount /boot; umount /mnt/testrootfs; umount /sys/firmware/efi/efivars;")
+ # from now on, every deploy cmd should return 0
+ # else an exception will be thrown by sshcontrol
+ self.master.ignore_status = False
+ self.master.copy_to(self.rootfs, "~/test-rootfs." + self.image_fstype)
+ self.master.copy_to(self.kernel, "~/test-kernel")
+ for cmd in self.deploy_cmds:
+ self.master.run(cmd)
+
+ def _start(self, params=None):
+ self.power_cycle(self.master)
+ # there are better ways than a timeout but this should work for now
+ time.sleep(120)
diff --git a/meta/lib/oeqa/controllers/testtargetloader.py b/meta/lib/oeqa/controllers/testtargetloader.py
new file mode 100644
index 0000000..a1b7b1d
--- /dev/null
+++ b/meta/lib/oeqa/controllers/testtargetloader.py
@@ -0,0 +1,70 @@
+import types
+import bb
+import os
+
+# This class is responsible for loading a test target controller
+class TestTargetLoader:
+
+ # Search oeqa.controllers module directory for and return a controller
+ # corresponding to the given target name.
+ # AttributeError raised if not found.
+ # ImportError raised if a provided module can not be imported.
+ def get_controller_module(self, target, bbpath):
+ controllerslist = self.get_controller_modulenames(bbpath)
+ bb.note("Available controller modules: %s" % str(controllerslist))
+ controller = self.load_controller_from_name(target, controllerslist)
+ return controller
+
+ # Return a list of all python modules in lib/oeqa/controllers for each
+ # layer in bbpath
+ def get_controller_modulenames(self, bbpath):
+
+ controllerslist = []
+
+ def add_controller_list(path):
+ if not os.path.exists(os.path.join(path, '__init__.py')):
+ bb.fatal('Controllers directory %s exists but is missing __init__.py' % path)
+ files = sorted([f for f in os.listdir(path) if f.endswith('.py') and not f.startswith('_')])
+ for f in files:
+ module = 'oeqa.controllers.' + f[:-3]
+ if module not in controllerslist:
+ controllerslist.append(module)
+ else:
+ bb.warn("Duplicate controller module found for %s, only one added. Layers should create unique controller module names" % module)
+
+ for p in bbpath:
+ controllerpath = os.path.join(p, 'lib', 'oeqa', 'controllers')
+ bb.debug(2, 'Searching for target controllers in %s' % controllerpath)
+ if os.path.exists(controllerpath):
+ add_controller_list(controllerpath)
+ return controllerslist
+
+ # Search for and return a controller from given target name and
+ # set of module names.
+ # Raise AttributeError if not found.
+ # Raise ImportError if a provided module can not be imported
+ def load_controller_from_name(self, target, modulenames):
+ for name in modulenames:
+ obj = self.load_controller_from_module(target, name)
+ if obj:
+ return obj
+ raise AttributeError("Unable to load {0} from available modules: {1}".format(target, str(modulenames)))
+
+ # Search for and return a controller or None from given module name
+ def load_controller_from_module(self, target, modulename):
+ obj = None
+ # import module, allowing it to raise import exception
+ module = __import__(modulename, globals(), locals(), [target])
+ # look for target class in the module, catching any exceptions as it
+ # is valid that a module may not have the target class.
+ try:
+ obj = getattr(module, target)
+ if obj:
+ from oeqa.targetcontrol import BaseTarget
+ if (not isinstance(obj, (type, types.ClassType))):
+ bb.warn("Target {0} found, but not of type Class".format(target))
+ if( not issubclass(obj, BaseTarget)):
+ bb.warn("Target {0} found, but subclass is not BaseTarget".format(target))
+ except:
+ obj = None
+ return obj
diff --git a/meta/lib/oeqa/oetest.py b/meta/lib/oeqa/oetest.py
new file mode 100644
index 0000000..0fe68d4
--- /dev/null
+++ b/meta/lib/oeqa/oetest.py
@@ -0,0 +1,214 @@
+# Copyright (C) 2013 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# Main unittest module used by testimage.bbclass
+# This provides the oeRuntimeTest base class which is inherited by all tests in meta/lib/oeqa/runtime.
+
+# It also has some helper functions and it's responsible for actually starting the tests
+
+import os, re, mmap
+import unittest
+import inspect
+import subprocess
+import bb
+from oeqa.utils.decorators import LogResults, gettag
+from sys import exc_info, exc_clear
+
+def getVar(obj):
+ #extend form dict, if a variable didn't exists, need find it in testcase
+ class VarDict(dict):
+ def __getitem__(self, key):
+ return gettag(obj, key)
+ return VarDict()
+
+def checkTags(tc, tagexp):
+ return eval(tagexp, None, getVar(tc))
+
+
+def filterByTagExp(testsuite, tagexp):
+ if not tagexp:
+ return testsuite
+ caseList = []
+ for each in testsuite:
+ if not isinstance(each, unittest.BaseTestSuite):
+ if checkTags(each, tagexp):
+ caseList.append(each)
+ else:
+ caseList.append(filterByTagExp(each, tagexp))
+ return testsuite.__class__(caseList)
+
+def loadTests(tc, type="runtime"):
+ if type == "runtime":
+ # set the context object passed from the test class
+ setattr(oeTest, "tc", tc)
+ # set ps command to use
+ setattr(oeRuntimeTest, "pscmd", "ps -ef" if oeTest.hasPackage("procps") else "ps")
+ # prepare test suite, loader and runner
+ suite = unittest.TestSuite()
+ elif type == "sdk":
+ # set the context object passed from the test class
+ setattr(oeTest, "tc", tc)
+ testloader = unittest.TestLoader()
+ testloader.sortTestMethodsUsing = None
+ suites = [testloader.loadTestsFromName(name) for name in tc.testslist]
+ suites = filterByTagExp(suites, getattr(tc, "tagexp", None))
+
+ def getTests(test):
+ '''Return all individual tests executed when running the suite.'''
+ # Unfortunately unittest does not have an API for this, so we have
+ # to rely on implementation details. This only needs to work
+ # for TestSuite containing TestCase.
+ method = getattr(test, '_testMethodName', None)
+ if method:
+ # leaf case: a TestCase
+ yield test
+ else:
+ # Look into TestSuite.
+ tests = getattr(test, '_tests', [])
+ for t1 in tests:
+ for t2 in getTests(t1):
+ yield t2
+
+ # Determine dependencies between suites by looking for @skipUnlessPassed
+ # method annotations. Suite A depends on suite B if any method in A
+ # depends on a method on B.
+ for suite in suites:
+ suite.dependencies = []
+ suite.depth = 0
+ for test in getTests(suite):
+ methodname = getattr(test, '_testMethodName', None)
+ if methodname:
+ method = getattr(test, methodname)
+ depends_on = getattr(method, '_depends_on', None)
+ if depends_on:
+ for dep_suite in suites:
+ if depends_on in [getattr(t, '_testMethodName', None) for t in getTests(dep_suite)]:
+ if dep_suite not in suite.dependencies and \
+ dep_suite is not suite:
+ suite.dependencies.append(dep_suite)
+ break
+ else:
+ bb.warn("Test %s was declared as @skipUnlessPassed('%s') but that test is either not defined or not active. Will run the test anyway." %
+ (test, depends_on))
+ # Use brute-force topological sort to determine ordering. Sort by
+ # depth (higher depth = must run later), with original ordering to
+ # break ties.
+ def set_suite_depth(suite):
+ for dep in suite.dependencies:
+ new_depth = set_suite_depth(dep) + 1
+ if new_depth > suite.depth:
+ suite.depth = new_depth
+ return suite.depth
+ for index, suite in enumerate(suites):
+ set_suite_depth(suite)
+ suite.index = index
+ suites.sort(cmp=lambda a,b: cmp((a.depth, a.index), (b.depth, b.index)))
+ return testloader.suiteClass(suites)
+
+def runTests(tc, type="runtime"):
+
+ suite = loadTests(tc, type)
+ bb.note("Test modules %s" % tc.testslist)
+ if hasattr(tc, "tagexp") and tc.tagexp:
+ bb.note("Filter test cases by tags: %s" % tc.tagexp)
+ bb.note("Found %s tests" % suite.countTestCases())
+ runner = unittest.TextTestRunner(verbosity=2)
+ result = runner.run(suite)
+
+ return result
+
+@LogResults
+class oeTest(unittest.TestCase):
+
+ longMessage = True
+
+ @classmethod
+ def hasPackage(self, pkg):
+ for item in oeTest.tc.pkgmanifest.split('\n'):
+ if re.match(pkg, item):
+ return True
+ return False
+
+ @classmethod
+ def hasFeature(self,feature):
+
+ if feature in oeTest.tc.imagefeatures or \
+ feature in oeTest.tc.distrofeatures:
+ return True
+ else:
+ return False
+
+class oeRuntimeTest(oeTest):
+ def __init__(self, methodName='runTest'):
+ self.target = oeRuntimeTest.tc.target
+ super(oeRuntimeTest, self).__init__(methodName)
+
+ def setUp(self):
+ # Check if test needs to run
+ if self.tc.sigterm:
+ self.fail("Got SIGTERM")
+ elif (type(self.target).__name__ == "QemuTarget"):
+ self.assertTrue(self.target.check(), msg = "Qemu not running?")
+
+ def tearDown(self):
+ # If a test fails or there is an exception
+ if not exc_info() == (None, None, None):
+ exc_clear()
+ #Only dump for QemuTarget
+ if (type(self.target).__name__ == "QemuTarget"):
+ self.tc.host_dumper.create_dir(self._testMethodName)
+ self.tc.host_dumper.dump_host()
+ self.target.target_dumper.dump_target(
+ self.tc.host_dumper.dump_dir)
+ print ("%s dump data stored in %s" % (self._testMethodName,
+ self.tc.host_dumper.dump_dir))
+
+ #TODO: use package_manager.py to install packages on any type of image
+ def install_packages(self, packagelist):
+ for package in packagelist:
+ (status, result) = self.target.run("smart install -y "+package)
+ if status != 0:
+ return status
+
+class oeSDKTest(oeTest):
+ def __init__(self, methodName='runTest'):
+ self.sdktestdir = oeSDKTest.tc.sdktestdir
+ super(oeSDKTest, self).__init__(methodName)
+
+ @classmethod
+ def hasHostPackage(self, pkg):
+
+ if re.search(pkg, oeTest.tc.hostpkgmanifest):
+ return True
+ return False
+
+ def _run(self, cmd):
+ return subprocess.check_output(cmd, shell=True)
+
+def getmodule(pos=2):
+ # stack returns a list of tuples containg frame information
+ # First element of the list the is current frame, caller is 1
+ frameinfo = inspect.stack()[pos]
+ modname = inspect.getmodulename(frameinfo[1])
+ #modname = inspect.getmodule(frameinfo[0]).__name__
+ return modname
+
+def skipModule(reason, pos=2):
+ modname = getmodule(pos)
+ if modname not in oeTest.tc.testsrequired:
+ raise unittest.SkipTest("%s: %s" % (modname, reason))
+ else:
+ raise Exception("\nTest %s wants to be skipped.\nReason is: %s" \
+ "\nTest was required in TEST_SUITES, so either the condition for skipping is wrong" \
+ "\nor the image really doesn't have the required feature/package when it should." % (modname, reason))
+
+def skipModuleIf(cond, reason):
+
+ if cond:
+ skipModule(reason, 3)
+
+def skipModuleUnless(cond, reason):
+
+ if not cond:
+ skipModule(reason, 3)
diff --git a/meta/lib/oeqa/runexported.py b/meta/lib/oeqa/runexported.py
new file mode 100755
index 0000000..96442b1
--- /dev/null
+++ b/meta/lib/oeqa/runexported.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+
+
+# Copyright (C) 2013 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# This script should be used outside of the build system to run image tests.
+# It needs a json file as input as exported by the build.
+# E.g for an already built image:
+#- export the tests:
+# TEST_EXPORT_ONLY = "1"
+# TEST_TARGET = "simpleremote"
+# TEST_TARGET_IP = "192.168.7.2"
+# TEST_SERVER_IP = "192.168.7.1"
+# bitbake core-image-sato -c testimage
+# Setup your target, e.g for qemu: runqemu core-image-sato
+# cd build/tmp/testimage/core-image-sato
+# ./runexported.py testdata.json
+
+import sys
+import os
+import time
+from optparse import OptionParser
+
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "oeqa")))
+
+from oeqa.oetest import runTests
+from oeqa.utils.sshcontrol import SSHControl
+from oeqa.utils.dump import get_host_dumper
+
+# this isn't pretty but we need a fake target object
+# for running the tests externally as we don't care
+# about deploy/start we only care about the connection methods (run, copy)
+class FakeTarget(object):
+ def __init__(self, d):
+ self.connection = None
+ self.ip = None
+ self.server_ip = None
+ self.datetime = time.strftime('%Y%m%d%H%M%S',time.gmtime())
+ self.testdir = d.getVar("TEST_LOG_DIR", True)
+ self.pn = d.getVar("PN", True)
+
+ def exportStart(self):
+ self.sshlog = os.path.join(self.testdir, "ssh_target_log.%s" % self.datetime)
+ sshloglink = os.path.join(self.testdir, "ssh_target_log")
+ if os.path.islink(sshloglink):
+ os.unlink(sshloglink)
+ os.symlink(self.sshlog, sshloglink)
+ print("SSH log file: %s" % self.sshlog)
+ self.connection = SSHControl(self.ip, logfile=self.sshlog)
+
+ def run(self, cmd, timeout=None):
+ return self.connection.run(cmd, timeout)
+
+ def copy_to(self, localpath, remotepath):
+ return self.connection.copy_to(localpath, remotepath)
+
+ def copy_from(self, remotepath, localpath):
+ return self.connection.copy_from(remotepath, localpath)
+
+
+class MyDataDict(dict):
+ def getVar(self, key, unused = None):
+ return self.get(key, "")
+
+class TestContext(object):
+ def __init__(self):
+ self.d = None
+ self.target = None
+
+def main():
+
+ usage = "usage: %prog [options] <json file>"
+ parser = OptionParser(usage=usage)
+ parser.add_option("-t", "--target-ip", dest="ip", help="The IP address of the target machine. Use this to \
+ overwrite the value determined from TEST_TARGET_IP at build time")
+ parser.add_option("-s", "--server-ip", dest="server_ip", help="The IP address of this machine. Use this to \
+ overwrite the value determined from TEST_SERVER_IP at build time.")
+ parser.add_option("-d", "--deploy-dir", dest="deploy_dir", help="Full path to the package feeds, that this \
+ the contents of what used to be DEPLOY_DIR on the build machine. If not specified it will use the value \
+ specified in the json if that directory actually exists or it will error out.")
+ parser.add_option("-l", "--log-dir", dest="log_dir", help="This sets the path for TEST_LOG_DIR. If not specified \
+ the current dir is used. This is used for usually creating a ssh log file and a scp test file.")
+
+ (options, args) = parser.parse_args()
+ if len(args) != 1:
+ parser.error("Incorrect number of arguments. The one and only argument should be a json file exported by the build system")
+
+ with open(args[0], "r") as f:
+ loaded = json.load(f)
+
+ if options.ip:
+ loaded["target"]["ip"] = options.ip
+ if options.server_ip:
+ loaded["target"]["server_ip"] = options.server_ip
+
+ d = MyDataDict()
+ for key in loaded["d"].keys():
+ d[key] = loaded["d"][key]
+
+ if options.log_dir:
+ d["TEST_LOG_DIR"] = options.log_dir
+ else:
+ d["TEST_LOG_DIR"] = os.path.abspath(os.path.dirname(__file__))
+ if options.deploy_dir:
+ d["DEPLOY_DIR"] = options.deploy_dir
+ else:
+ if not os.path.isdir(d["DEPLOY_DIR"]):
+ raise Exception("The path to DEPLOY_DIR does not exists: %s" % d["DEPLOY_DIR"])
+
+
+ target = FakeTarget(d)
+ for key in loaded["target"].keys():
+ setattr(target, key, loaded["target"][key])
+
+ host_dumper = get_host_dumper(d)
+ host_dumper.parent_dir = loaded["host_dumper"]["parent_dir"]
+ host_dumper.cmds = loaded["host_dumper"]["cmds"]
+
+ tc = TestContext()
+ setattr(tc, "d", d)
+ setattr(tc, "target", target)
+ setattr(tc, "host_dumper", host_dumper)
+ for key in loaded.keys():
+ if key != "d" and key != "target" and key != "host_dumper":
+ setattr(tc, key, loaded[key])
+
+ target.exportStart()
+ runTests(tc)
+
+ return 0
+
+if __name__ == "__main__":
+ try:
+ ret = main()
+ except Exception:
+ ret = 1
+ import traceback
+ traceback.print_exc(5)
+ sys.exit(ret)
diff --git a/meta/lib/oeqa/runtime/__init__.py b/meta/lib/oeqa/runtime/__init__.py
new file mode 100644
index 0000000..4cf3fa7
--- /dev/null
+++ b/meta/lib/oeqa/runtime/__init__.py
@@ -0,0 +1,3 @@
+# Enable other layers to have tests in the same named directory
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
diff --git a/meta/lib/oeqa/runtime/_ptest.py b/meta/lib/oeqa/runtime/_ptest.py
new file mode 100644
index 0000000..81c9c43
--- /dev/null
+++ b/meta/lib/oeqa/runtime/_ptest.py
@@ -0,0 +1,125 @@
+import unittest, os, shutil
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+from oeqa.utils.logparser import *
+from oeqa.utils.httpserver import HTTPService
+import bb
+import glob
+from oe.package_manager import RpmPkgsList
+import subprocess
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("package-management"):
+ skipModule("Image doesn't have package management feature")
+ if not oeRuntimeTest.hasPackage("smart"):
+ skipModule("Image doesn't have smart installed")
+ if "package_rpm" != oeRuntimeTest.tc.d.getVar("PACKAGE_CLASSES", True).split()[0]:
+ skipModule("Rpm is not the primary package manager")
+
+class PtestRunnerTest(oeRuntimeTest):
+
+ # a ptest log parser
+ def parse_ptest(self, logfile):
+ parser = Lparser(test_0_pass_regex="^PASS:(.+)", test_0_fail_regex="^FAIL:(.+)", section_0_begin_regex="^BEGIN: .*/(.+)/ptest", section_0_end_regex="^END: .*/(.+)/ptest")
+ parser.init()
+ result = Result()
+
+ with open(logfile) as f:
+ for line in f:
+ result_tuple = parser.parse_line(line)
+ if not result_tuple:
+ continue
+ result_tuple = line_type, category, status, name = parser.parse_line(line)
+
+ if line_type == 'section' and status == 'begin':
+ current_section = name
+ continue
+
+ if line_type == 'section' and status == 'end':
+ current_section = None
+ continue
+
+ if line_type == 'test' and status == 'pass':
+ result.store(current_section, name, status)
+ continue
+
+ if line_type == 'test' and status == 'fail':
+ result.store(current_section, name, status)
+ continue
+
+ result.sort_tests()
+ return result
+
+ @classmethod
+ def setUpClass(self):
+ #note the existing channels that are on the board before creating new ones
+# self.existingchannels = set()
+# (status, result) = oeRuntimeTest.tc.target.run('smart channel --show | grep "\["', 0)
+# for x in result.split("\n"):
+# self.existingchannels.add(x)
+ self.repo_server = HTTPService(oeRuntimeTest.tc.d.getVar('DEPLOY_DIR', True), oeRuntimeTest.tc.target.server_ip)
+ self.repo_server.start()
+
+ @classmethod
+ def tearDownClass(self):
+ self.repo_server.stop()
+ #remove created channels to be able to repeat the tests on same image
+# (status, result) = oeRuntimeTest.tc.target.run('smart channel --show | grep "\["', 0)
+# for x in result.split("\n"):
+# if x not in self.existingchannels:
+# oeRuntimeTest.tc.target.run('smart channel --remove '+x[1:-1]+' -y', 0)
+
+ def add_smart_channel(self):
+ image_pkgtype = self.tc.d.getVar('IMAGE_PKGTYPE', True)
+ deploy_url = 'http://%s:%s/%s' %(self.target.server_ip, self.repo_server.port, image_pkgtype)
+ pkgarchs = self.tc.d.getVar('PACKAGE_ARCHS', True).replace("-","_").split()
+ for arch in os.listdir('%s/%s' % (self.repo_server.root_dir, image_pkgtype)):
+ if arch in pkgarchs:
+ self.target.run('smart channel -y --add {a} type=rpm-md baseurl={u}/{a}'.format(a=arch, u=deploy_url), 0)
+ self.target.run('smart update', 0)
+
+ def install_complementary(self, globs=None):
+ installed_pkgs_file = os.path.join(oeRuntimeTest.tc.d.getVar('WORKDIR', True),
+ "installed_pkgs.txt")
+ self.pkgs_list = RpmPkgsList(oeRuntimeTest.tc.d, oeRuntimeTest.tc.d.getVar('IMAGE_ROOTFS', True), oeRuntimeTest.tc.d.getVar('arch_var', True), oeRuntimeTest.tc.d.getVar('os_var', True))
+ with open(installed_pkgs_file, "w+") as installed_pkgs:
+ installed_pkgs.write(self.pkgs_list.list("arch"))
+
+ cmd = [bb.utils.which(os.getenv('PATH'), "oe-pkgdata-util"),
+ "-p", oeRuntimeTest.tc.d.getVar('PKGDATA_DIR', True), "glob", installed_pkgs_file,
+ globs]
+ try:
+ bb.note("Installing complementary packages ...")
+ complementary_pkgs = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ bb.fatal("Could not compute complementary packages list. Command "
+ "'%s' returned %d:\n%s" %
+ (' '.join(cmd), e.returncode, e.output))
+
+ return complementary_pkgs.split()
+
+ def setUp(self):
+ self.ptest_log = os.path.join(oeRuntimeTest.tc.d.getVar("TEST_LOG_DIR",True), "ptest-%s.log" % oeRuntimeTest.tc.d.getVar('DATETIME', True))
+
+ @skipUnlessPassed('test_ssh')
+ def test_ptestrunner(self):
+ self.add_smart_channel()
+ (runnerstatus, result) = self.target.run('which ptest-runner', 0)
+ cond = oeRuntimeTest.hasPackage("ptest-runner") and oeRuntimeTest.hasFeature("ptest") and oeRuntimeTest.hasPackage("-ptest") and (runnerstatus != 0)
+ if cond:
+ self.install_packages(self.install_complementary("*-ptest"))
+ self.install_packages(['ptest-runner'])
+
+ (runnerstatus, result) = self.target.run('/usr/bin/ptest-runner > /tmp/ptest.log 2>&1', 0)
+ #exit code is !=0 even if ptest-runner executes because some ptest tests fail.
+ self.assertTrue(runnerstatus != 127, msg="Cannot execute ptest-runner!")
+ self.target.copy_from('/tmp/ptest.log', self.ptest_log)
+ shutil.copyfile(self.ptest_log, "ptest.log")
+
+ result = self.parse_ptest("ptest.log")
+ log_results_to_location = "./results"
+ if os.path.exists(log_results_to_location):
+ shutil.rmtree(log_results_to_location)
+ os.makedirs(log_results_to_location)
+
+ result.log_as_files(log_results_to_location, test_status = ['pass','fail'])
diff --git a/meta/lib/oeqa/runtime/_qemutiny.py b/meta/lib/oeqa/runtime/_qemutiny.py
new file mode 100644
index 0000000..a3c29f3
--- /dev/null
+++ b/meta/lib/oeqa/runtime/_qemutiny.py
@@ -0,0 +1,9 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest
+from oeqa.utils.qemutinyrunner import *
+
+class QemuTinyTest(oeRuntimeTest):
+
+ def test_boot_tiny(self):
+ (status, output) = self.target.run_serial('uname -a')
+ self.assertTrue("yocto-tiny" in output, msg="Cannot detect poky tiny boot!")
\ No newline at end of file
diff --git a/meta/lib/oeqa/runtime/buildcvs.py b/meta/lib/oeqa/runtime/buildcvs.py
new file mode 100644
index 0000000..fe6cbfb
--- /dev/null
+++ b/meta/lib/oeqa/runtime/buildcvs.py
@@ -0,0 +1,31 @@
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+from oeqa.utils.targetbuild import TargetBuildProject
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("tools-sdk"):
+ skipModule("Image doesn't have tools-sdk in IMAGE_FEATURES")
+
+class BuildCvsTest(oeRuntimeTest):
+
+ @classmethod
+ def setUpClass(self):
+ self.project = TargetBuildProject(oeRuntimeTest.tc.target, oeRuntimeTest.tc.d,
+ "http://ftp.gnu.org/non-gnu/cvs/source/feature/1.12.13/cvs-1.12.13.tar.bz2")
+ self.project.download_archive()
+
+ @testcase(205)
+ @skipUnlessPassed("test_ssh")
+ def test_cvs(self):
+ self.assertEqual(self.project.run_configure(), 0,
+ msg="Running configure failed")
+
+ self.assertEqual(self.project.run_make(), 0,
+ msg="Running make failed")
+
+ self.assertEqual(self.project.run_install(), 0,
+ msg="Running make install failed")
+
+ @classmethod
+ def tearDownClass(self):
+ self.project.clean()
diff --git a/meta/lib/oeqa/runtime/buildiptables.py b/meta/lib/oeqa/runtime/buildiptables.py
new file mode 100644
index 0000000..09e252d
--- /dev/null
+++ b/meta/lib/oeqa/runtime/buildiptables.py
@@ -0,0 +1,31 @@
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+from oeqa.utils.targetbuild import TargetBuildProject
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("tools-sdk"):
+ skipModule("Image doesn't have tools-sdk in IMAGE_FEATURES")
+
+class BuildIptablesTest(oeRuntimeTest):
+
+ @classmethod
+ def setUpClass(self):
+ self.project = TargetBuildProject(oeRuntimeTest.tc.target, oeRuntimeTest.tc.d,
+ "http://netfilter.org/projects/iptables/files/iptables-1.4.13.tar.bz2")
+ self.project.download_archive()
+
+ @testcase(206)
+ @skipUnlessPassed("test_ssh")
+ def test_iptables(self):
+ self.assertEqual(self.project.run_configure(), 0,
+ msg="Running configure failed")
+
+ self.assertEqual(self.project.run_make(), 0,
+ msg="Running make failed")
+
+ self.assertEqual(self.project.run_install(), 0,
+ msg="Running make install failed")
+
+ @classmethod
+ def tearDownClass(self):
+ self.project.clean()
diff --git a/meta/lib/oeqa/runtime/buildsudoku.py b/meta/lib/oeqa/runtime/buildsudoku.py
new file mode 100644
index 0000000..802b060
--- /dev/null
+++ b/meta/lib/oeqa/runtime/buildsudoku.py
@@ -0,0 +1,28 @@
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+from oeqa.utils.targetbuild import TargetBuildProject
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("tools-sdk"):
+ skipModule("Image doesn't have tools-sdk in IMAGE_FEATURES")
+
+class SudokuTest(oeRuntimeTest):
+
+ @classmethod
+ def setUpClass(self):
+ self.project = TargetBuildProject(oeRuntimeTest.tc.target, oeRuntimeTest.tc.d,
+ "http://downloads.sourceforge.net/project/sudoku-savant/sudoku-savant/sudoku-savant-1.3/sudoku-savant-1.3.tar.bz2")
+ self.project.download_archive()
+
+ @testcase(207)
+ @skipUnlessPassed("test_ssh")
+ def test_sudoku(self):
+ self.assertEqual(self.project.run_configure(), 0,
+ msg="Running configure failed")
+
+ self.assertEqual(self.project.run_make(), 0,
+ msg="Running make failed")
+
+ @classmethod
+ def tearDownClass(self):
+ self.project.clean()
diff --git a/meta/lib/oeqa/runtime/connman.py b/meta/lib/oeqa/runtime/connman.py
new file mode 100644
index 0000000..ee69e5d
--- /dev/null
+++ b/meta/lib/oeqa/runtime/connman.py
@@ -0,0 +1,54 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasPackage("connman"):
+ skipModule("No connman package in image")
+
+
+class ConnmanTest(oeRuntimeTest):
+
+ def service_status(self, service):
+ if oeRuntimeTest.hasFeature("systemd"):
+ (status, output) = self.target.run('systemctl status -l %s' % service)
+ return output
+ else:
+ return "Unable to get status or logs for %s" % service
+
+ @testcase(961)
+ @skipUnlessPassed('test_ssh')
+ def test_connmand_help(self):
+ (status, output) = self.target.run('/usr/sbin/connmand --help')
+ self.assertEqual(status, 0, msg="status and output: %s and %s" % (status,output))
+
+ @testcase(221)
+ @skipUnlessPassed('test_connmand_help')
+ def test_connmand_running(self):
+ (status, output) = self.target.run(oeRuntimeTest.pscmd + ' | grep [c]onnmand')
+ if status != 0:
+ print self.service_status("connman")
+ self.fail("No connmand process running")
+
+ @testcase(223)
+ def test_only_one_connmand_in_background(self):
+ """
+ Summary: Only one connmand in background
+ Expected: There will be only one connmand instance in background.
+ Product: BSPs
+ Author: Alexandru Georgescu <alexandru.c.georgescu@intel.com>
+ AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com>
+ """
+
+ # Make sure that 'connmand' is running in background
+ (status, output) = self.target.run(oeRuntimeTest.pscmd + ' | grep [c]onnmand')
+ self.assertEqual(0, status, 'Failed to find "connmand" process running in background.')
+
+ # Start a new instance of 'connmand'
+ (status, output) = self.target.run('connmand')
+ self.assertEqual(0, status, 'Failed to start a new "connmand" process.')
+
+ # Make sure that only one 'connmand' is running in background
+ (status, output) = self.target.run(oeRuntimeTest.pscmd + ' | grep [c]onnmand | wc -l')
+ self.assertEqual(0, status, 'Failed to find "connmand" process running in background.')
+ self.assertEqual(1, int(output), 'Found {} connmand processes running, expected 1.'.format(output))
diff --git a/meta/lib/oeqa/runtime/date.py b/meta/lib/oeqa/runtime/date.py
new file mode 100644
index 0000000..3a8fe84
--- /dev/null
+++ b/meta/lib/oeqa/runtime/date.py
@@ -0,0 +1,31 @@
+from oeqa.oetest import oeRuntimeTest
+from oeqa.utils.decorators import *
+import re
+
+class DateTest(oeRuntimeTest):
+
+ def setUp(self):
+ if oeRuntimeTest.tc.d.getVar("VIRTUAL-RUNTIME_init_manager", True) == "systemd":
+ self.target.run('systemctl stop systemd-timesyncd')
+
+ def tearDown(self):
+ if oeRuntimeTest.tc.d.getVar("VIRTUAL-RUNTIME_init_manager", True) == "systemd":
+ self.target.run('systemctl start systemd-timesyncd')
+
+ @testcase(211)
+ @skipUnlessPassed("test_ssh")
+ def test_date(self):
+ (status, output) = self.target.run('date +"%Y-%m-%d %T"')
+ self.assertEqual(status, 0, msg="Failed to get initial date, output: %s" % output)
+ oldDate = output
+
+ sampleDate = '"2016-08-09 10:00:00"'
+ (status, output) = self.target.run("date -s %s" % sampleDate)
+ self.assertEqual(status, 0, msg="Date set failed, output: %s" % output)
+
+ (status, output) = self.target.run("date -R")
+ p = re.match('Tue, 09 Aug 2016 10:00:.. \+0000', output)
+ self.assertTrue(p, msg="The date was not set correctly, output: %s" % output)
+
+ (status, output) = self.target.run('date -s "%s"' % oldDate)
+ self.assertEqual(status, 0, msg="Failed to reset date, output: %s" % output)
diff --git a/meta/lib/oeqa/runtime/df.py b/meta/lib/oeqa/runtime/df.py
new file mode 100644
index 0000000..09569d5
--- /dev/null
+++ b/meta/lib/oeqa/runtime/df.py
@@ -0,0 +1,12 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest
+from oeqa.utils.decorators import *
+
+
+class DfTest(oeRuntimeTest):
+
+ @testcase(234)
+ @skipUnlessPassed("test_ssh")
+ def test_df(self):
+ (status,output) = self.target.run("df / | sed -n '2p' | awk '{print $4}'")
+ self.assertTrue(int(output)>5120, msg="Not enough space on image. Current size is %s" % output)
diff --git a/meta/lib/oeqa/runtime/dmesg.py b/meta/lib/oeqa/runtime/dmesg.py
new file mode 100644
index 0000000..5831471
--- /dev/null
+++ b/meta/lib/oeqa/runtime/dmesg.py
@@ -0,0 +1,12 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest
+from oeqa.utils.decorators import *
+
+
+class DmesgTest(oeRuntimeTest):
+
+ @testcase(215)
+ @skipUnlessPassed('test_ssh')
+ def test_dmesg(self):
+ (status, output) = self.target.run('dmesg | grep -v mmci-pl18x | grep -v "error changing net interface name" | grep -iv "dma timeout" | grep -v usbhid | grep -i error')
+ self.assertEqual(status, 1, msg = "Error messages in dmesg log: %s" % output)
diff --git a/meta/lib/oeqa/runtime/files/hellomod.c b/meta/lib/oeqa/runtime/files/hellomod.c
new file mode 100644
index 0000000..a383397
--- /dev/null
+++ b/meta/lib/oeqa/runtime/files/hellomod.c
@@ -0,0 +1,19 @@
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+
+static int __init hello_init(void)
+{
+ printk(KERN_INFO "Hello world!\n");
+ return 0;
+}
+
+static void __exit hello_cleanup(void)
+{
+ printk(KERN_INFO "Cleaning up hellomod.\n");
+}
+
+module_init(hello_init);
+module_exit(hello_cleanup);
+
+MODULE_LICENSE("GPL");
diff --git a/meta/lib/oeqa/runtime/files/hellomod_makefile b/meta/lib/oeqa/runtime/files/hellomod_makefile
new file mode 100644
index 0000000..b92d5c8
--- /dev/null
+++ b/meta/lib/oeqa/runtime/files/hellomod_makefile
@@ -0,0 +1,8 @@
+obj-m := hellomod.o
+KDIR := /usr/src/kernel
+
+all:
+ $(MAKE) -C $(KDIR) M=$(PWD) modules
+
+clean:
+ $(MAKE) -C $(KDIR) M=$(PWD) clean
diff --git a/meta/lib/oeqa/runtime/files/test.c b/meta/lib/oeqa/runtime/files/test.c
new file mode 100644
index 0000000..2d8389c
--- /dev/null
+++ b/meta/lib/oeqa/runtime/files/test.c
@@ -0,0 +1,26 @@
+#include <stdio.h>
+#include <math.h>
+#include <stdlib.h>
+
+double convert(long long l)
+{
+ return (double)l;
+}
+
+int main(int argc, char * argv[]) {
+
+ long long l = 10;
+ double f;
+ double check = 10.0;
+
+ f = convert(l);
+ printf("convert: %lld => %f\n", l, f);
+ if ( f != check ) exit(1);
+
+ f = 1234.67;
+ check = 1234.0;
+ printf("floorf(%f) = %f\n", f, floorf(f));
+ if ( floorf(f) != check) exit(1);
+
+ return 0;
+}
diff --git a/meta/lib/oeqa/runtime/files/test.cpp b/meta/lib/oeqa/runtime/files/test.cpp
new file mode 100644
index 0000000..9e1a764
--- /dev/null
+++ b/meta/lib/oeqa/runtime/files/test.cpp
@@ -0,0 +1,3 @@
+#include <limits>
+
+int main() {}
\ No newline at end of file
diff --git a/meta/lib/oeqa/runtime/files/test.pl b/meta/lib/oeqa/runtime/files/test.pl
new file mode 100644
index 0000000..689c8f1
--- /dev/null
+++ b/meta/lib/oeqa/runtime/files/test.pl
@@ -0,0 +1,2 @@
+$a = 9.01e+21 - 9.01e+21 + 0.01;
+print ("the value of a is ", $a, "\n");
diff --git a/meta/lib/oeqa/runtime/files/test.py b/meta/lib/oeqa/runtime/files/test.py
new file mode 100644
index 0000000..f3a2273
--- /dev/null
+++ b/meta/lib/oeqa/runtime/files/test.py
@@ -0,0 +1,6 @@
+import os
+
+os.system('touch /tmp/testfile.python')
+
+a = 9.01e+21 - 9.01e+21 + 0.01
+print "the value of a is %s" % a
diff --git a/meta/lib/oeqa/runtime/files/testmakefile b/meta/lib/oeqa/runtime/files/testmakefile
new file mode 100644
index 0000000..ca1844e
--- /dev/null
+++ b/meta/lib/oeqa/runtime/files/testmakefile
@@ -0,0 +1,5 @@
+test: test.o
+ gcc -o test test.o -lm
+test.o: test.c
+ gcc -c test.c
+
diff --git a/meta/lib/oeqa/runtime/gcc.py b/meta/lib/oeqa/runtime/gcc.py
new file mode 100644
index 0000000..d90cd17
--- /dev/null
+++ b/meta/lib/oeqa/runtime/gcc.py
@@ -0,0 +1,47 @@
+import unittest
+import os
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("tools-sdk"):
+ skipModule("Image doesn't have tools-sdk in IMAGE_FEATURES")
+
+
+class GccCompileTest(oeRuntimeTest):
+
+ @classmethod
+ def setUpClass(self):
+ oeRuntimeTest.tc.target.copy_to(os.path.join(oeRuntimeTest.tc.filesdir, "test.c"), "/tmp/test.c")
+ oeRuntimeTest.tc.target.copy_to(os.path.join(oeRuntimeTest.tc.filesdir, "testmakefile"), "/tmp/testmakefile")
+ oeRuntimeTest.tc.target.copy_to(os.path.join(oeRuntimeTest.tc.filesdir, "test.cpp"), "/tmp/test.cpp")
+
+ @testcase(203)
+ def test_gcc_compile(self):
+ (status, output) = self.target.run('gcc /tmp/test.c -o /tmp/test -lm')
+ self.assertEqual(status, 0, msg="gcc compile failed, output: %s" % output)
+ (status, output) = self.target.run('/tmp/test')
+ self.assertEqual(status, 0, msg="running compiled file failed, output %s" % output)
+
+ @testcase(200)
+ def test_gpp_compile(self):
+ (status, output) = self.target.run('g++ /tmp/test.c -o /tmp/test -lm')
+ self.assertEqual(status, 0, msg="g++ compile failed, output: %s" % output)
+ (status, output) = self.target.run('/tmp/test')
+ self.assertEqual(status, 0, msg="running compiled file failed, output %s" % output)
+
+ @testcase(1142)
+ def test_gpp2_compile(self):
+ (status, output) = self.target.run('g++ /tmp/test.cpp -o /tmp/test -lm')
+ self.assertEqual(status, 0, msg="g++ compile failed, output: %s" % output)
+ (status, output) = self.target.run('/tmp/test')
+ self.assertEqual(status, 0, msg="running compiled file failed, output %s" % output)
+
+ @testcase(204)
+ def test_make(self):
+ (status, output) = self.target.run('cd /tmp; make -f testmakefile')
+ self.assertEqual(status, 0, msg="running make failed, output %s" % output)
+
+ @classmethod
+ def tearDownClass(self):
+ oeRuntimeTest.tc.target.run("rm /tmp/test.c /tmp/test.o /tmp/test /tmp/testmakefile")
diff --git a/meta/lib/oeqa/runtime/kernelmodule.py b/meta/lib/oeqa/runtime/kernelmodule.py
new file mode 100644
index 0000000..2e81720
--- /dev/null
+++ b/meta/lib/oeqa/runtime/kernelmodule.py
@@ -0,0 +1,34 @@
+import unittest
+import os
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("tools-sdk"):
+ skipModule("Image doesn't have tools-sdk in IMAGE_FEATURES")
+
+
+class KernelModuleTest(oeRuntimeTest):
+
+ def setUp(self):
+ self.target.copy_to(os.path.join(oeRuntimeTest.tc.filesdir, "hellomod.c"), "/tmp/hellomod.c")
+ self.target.copy_to(os.path.join(oeRuntimeTest.tc.filesdir, "hellomod_makefile"), "/tmp/Makefile")
+
+ @testcase('316')
+ @skipUnlessPassed('test_ssh')
+ @skipUnlessPassed('test_gcc_compile')
+ def test_kernel_module(self):
+ cmds = [
+ 'cd /usr/src/kernel && make scripts',
+ 'cd /tmp && make',
+ 'cd /tmp && insmod hellomod.ko',
+ 'lsmod | grep hellomod',
+ 'dmesg | grep Hello',
+ 'rmmod hellomod', 'dmesg | grep "Cleaning up hellomod"'
+ ]
+ for cmd in cmds:
+ (status, output) = self.target.run(cmd, 900)
+ self.assertEqual(status, 0, msg="\n".join([cmd, output]))
+
+ def tearDown(self):
+ self.target.run('rm -f /tmp/Makefile /tmp/hellomod.c')
diff --git a/meta/lib/oeqa/runtime/ldd.py b/meta/lib/oeqa/runtime/ldd.py
new file mode 100644
index 0000000..47b3885
--- /dev/null
+++ b/meta/lib/oeqa/runtime/ldd.py
@@ -0,0 +1,21 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("tools-sdk"):
+ skipModule("Image doesn't have tools-sdk in IMAGE_FEATURES")
+
+class LddTest(oeRuntimeTest):
+
+ @testcase(962)
+ @skipUnlessPassed('test_ssh')
+ def test_ldd_exists(self):
+ (status, output) = self.target.run('which ldd')
+ self.assertEqual(status, 0, msg = "ldd does not exist in PATH: which ldd: %s" % output)
+
+ @testcase(239)
+ @skipUnlessPassed('test_ldd_exists')
+ def test_ldd_rtldlist_check(self):
+ (status, output) = self.target.run('for i in $(which ldd | xargs cat | grep "^RTLDLIST"|cut -d\'=\' -f2|tr -d \'"\'); do test -f $i && echo $i && break; done')
+ self.assertEqual(status, 0, msg = "ldd path not correct or RTLDLIST files don't exist. ")
diff --git a/meta/lib/oeqa/runtime/logrotate.py b/meta/lib/oeqa/runtime/logrotate.py
new file mode 100644
index 0000000..86d791c
--- /dev/null
+++ b/meta/lib/oeqa/runtime/logrotate.py
@@ -0,0 +1,28 @@
+# This test should cover https://bugzilla.yoctoproject.org/tr_show_case.cgi?case_id=289 testcase
+# Note that the image under test must have logrotate installed
+
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasPackage("logrotate"):
+ skipModule("No logrotate package in image")
+
+
+class LogrotateTest(oeRuntimeTest):
+
+ @skipUnlessPassed("test_ssh")
+ def test_1_logrotate_setup(self):
+ (status, output) = self.target.run('mkdir /home/root/logrotate_dir')
+ self.assertEqual(status, 0, msg = "Could not create logrotate_dir. Output: %s" % output)
+ (status, output) = self.target.run("sed -i 's#wtmp {#wtmp {\\n olddir /home/root/logrotate_dir#' /etc/logrotate.conf")
+ self.assertEqual(status, 0, msg = "Could not write to logrotate.conf file. Status and output: %s and %s)" % (status, output))
+
+ @testcase(289)
+ @skipUnlessPassed("test_1_logrotate_setup")
+ def test_2_logrotate(self):
+ (status, output) = self.target.run('logrotate -f /etc/logrotate.conf')
+ self.assertEqual(status, 0, msg = "logrotate service could not be reloaded. Status and output: %s and %s" % (status, output))
+ output = self.target.run('ls -la /home/root/logrotate_dir/ | wc -l')[1]
+ self.assertTrue(int(output)>=3, msg = "new logfile could not be created. List of files within log directory: %s" %(self.target.run('ls -la /home/root/logrotate_dir')[1]))
diff --git a/meta/lib/oeqa/runtime/multilib.py b/meta/lib/oeqa/runtime/multilib.py
new file mode 100644
index 0000000..e1bcc42
--- /dev/null
+++ b/meta/lib/oeqa/runtime/multilib.py
@@ -0,0 +1,48 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ multilibs = oeRuntimeTest.tc.d.getVar("MULTILIBS", True) or ""
+ if "multilib:lib32" not in multilibs:
+ skipModule("this isn't a multilib:lib32 image")
+
+
+class MultilibTest(oeRuntimeTest):
+
+ def parse(self, s):
+ """
+ Parse the output of readelf -h and return the binary class, or fail.
+ """
+ l = [l.split()[1] for l in s.split('\n') if "Class:" in l]
+ if l:
+ return l[0]
+ else:
+ self.fail("Cannot parse readelf output\n" + s)
+
+ @skipUnlessPassed('test_ssh')
+ def test_check_multilib_libc(self):
+ """
+ Check that a multilib image has both 32-bit and 64-bit libc in.
+ """
+
+ (status, output) = self.target.run("readelf -h /lib/libc.so.6")
+ self.assertEqual(status, 0, "Failed to readelf /lib/libc.so.6")
+ class32 = self.parse(output)
+
+ (status, output) = self.target.run("readelf -h /lib64/libc.so.6")
+ self.assertEqual(status, 0, "Failed to readelf /lib64/libc.so.6")
+ class64 = self.parse(output)
+
+ self.assertEqual(class32, "ELF32", msg="/lib/libc.so.6 isn't ELF32 (is %s)" % class32)
+ self.assertEqual(class64, "ELF64", msg="/lib64/libc.so.6 isn't ELF64 (is %s)" % class64)
+
+ @testcase('279')
+ @skipUnlessPassed('test_check_multilib_libc')
+ def test_file_connman(self):
+ self.assertTrue(oeRuntimeTest.hasPackage('lib32-connman-gnome'), msg="This test assumes lib32-connman-gnome is installed")
+
+ (status, output) = self.target.run("readelf -h /usr/bin/connman-applet")
+ self.assertEqual(status, 0, "Failed to readelf /usr/bin/connman-applet")
+ theclass = self.parse(output)
+ self.assertEqual(theclass, "ELF32", msg="connman-applet isn't ELF32 (is %s)" % theclass)
diff --git a/meta/lib/oeqa/runtime/pam.py b/meta/lib/oeqa/runtime/pam.py
new file mode 100644
index 0000000..c8205c9
--- /dev/null
+++ b/meta/lib/oeqa/runtime/pam.py
@@ -0,0 +1,25 @@
+# This test should cover https://bugzilla.yoctoproject.org/tr_show_case.cgi?case_id=287 testcase
+# Note that the image under test must have "pam" in DISTRO_FEATURES
+
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("pam"):
+ skipModule("target doesn't have 'pam' in DISTRO_FEATURES")
+
+
+class PamBasicTest(oeRuntimeTest):
+
+ @testcase(287)
+ @skipUnlessPassed('test_ssh')
+ def test_pam(self):
+ (status, output) = self.target.run('login --help')
+ self.assertEqual(status, 1, msg = "login command does not work as expected. Status and output:%s and %s" %(status, output))
+ (status, output) = self.target.run('passwd --help')
+ self.assertEqual(status, 0, msg = "passwd command does not work as expected. Status and output:%s and %s" %(status, output))
+ (status, output) = self.target.run('su --help')
+ self.assertEqual(status, 0, msg = "su command does not work as expected. Status and output:%s and %s" %(status, output))
+ (status, output) = self.target.run('useradd --help')
+ self.assertEqual(status, 0, msg = "useradd command does not work as expected. Status and output:%s and %s" %(status, output))
diff --git a/meta/lib/oeqa/runtime/parselogs.py b/meta/lib/oeqa/runtime/parselogs.py
new file mode 100644
index 0000000..e20947b
--- /dev/null
+++ b/meta/lib/oeqa/runtime/parselogs.py
@@ -0,0 +1,257 @@
+import os
+import unittest
+import subprocess
+from oeqa.oetest import oeRuntimeTest
+from oeqa.utils.decorators import *
+
+#in the future these lists could be moved outside of module
+errors = ["error", "cannot", "can\'t", "failed"]
+
+common_errors = [
+ "(WW) warning, (EE) error, (NI) not implemented, (??) unknown.",
+ "dma timeout",
+ "can\'t add hid device:",
+ "usbhid: probe of ",
+ "_OSC failed (AE_ERROR)",
+ "_OSC failed (AE_SUPPORT)",
+ "AE_ALREADY_EXISTS",
+ "ACPI _OSC request failed (AE_SUPPORT)",
+ "can\'t disable ASPM",
+ "Failed to load module \"vesa\"",
+ "Failed to load module vesa",
+ "Failed to load module \"modesetting\"",
+ "Failed to load module modesetting",
+ "Failed to load module \"glx\"",
+ "Failed to load module \"fbdev\"",
+ "Failed to load module fbdev",
+ "Failed to load module glx",
+ "[drm] Cannot find any crtc or sizes - going 1024x768",
+ "_OSC failed (AE_NOT_FOUND); disabling ASPM",
+ "Open ACPI failed (/var/run/acpid.socket) (No such file or directory)",
+ "NX (Execute Disable) protection cannot be enabled: non-PAE kernel!",
+ "hd.: possibly failed opcode",
+ 'NETLINK INITIALIZATION FAILED',
+ 'kernel: Cannot find map file',
+ 'omap_hwmod: debugss: _wait_target_disable failed',
+ 'VGA arbiter: cannot open kernel arbiter, no multi-card support',
+ 'Failed to find URL:http://ipv4.connman.net/online/status.html',
+ 'Online check failed for',
+ ]
+
+x86_common = [
+ '[drm:psb_do_init] *ERROR* Debug is',
+ 'wrong ELF class',
+ 'Could not enable PowerButton event',
+ 'probe of LNXPWRBN:00 failed with error -22',
+] + common_errors
+
+qemux86_common = [
+ 'Fast TSC calibration',
+ 'wrong ELF class',
+ "fail to add MMCONFIG information, can't access extended PCI configuration space under this bridge.",
+ "can't claim BAR ",
+] + common_errors
+
+ignore_errors = {
+ 'default' : common_errors,
+ 'qemux86' : [
+ 'Failed to access perfctr msr (MSR',
+ ] + qemux86_common,
+ 'qemux86-64' : qemux86_common,
+ 'qemumips' : [
+ 'Failed to load module "glx"',
+ 'pci 0000:00:00.0: [Firmware Bug]: reg 0x..: invalid BAR (can\'t size)',
+ ] + common_errors,
+ 'qemumips64' : [
+ 'pci 0000:00:00.0: [Firmware Bug]: reg 0x..: invalid BAR (can\'t size)',
+ ] + common_errors,
+ 'qemuppc' : [
+ 'PCI 0000:00 Cannot reserve Legacy IO [io 0x0000-0x0fff]',
+ 'host side 80-wire cable detection failed, limiting max speed',
+ 'mode "640x480" test failed',
+ 'Failed to load module "glx"',
+ ] + common_errors,
+ 'qemuarm' : [
+ 'mmci-pl18x: probe of fpga:05 failed with error -22',
+ 'mmci-pl18x: probe of fpga:0b failed with error -22',
+ 'Failed to load module "glx"'
+ ] + common_errors,
+ 'qemuarm64' : [
+ 'Fatal server error:',
+ '(EE) Server terminated with error (1). Closing log file.',
+ ] + common_errors,
+ 'emenlow' : [
+ '[Firmware Bug]: ACPI: No _BQC method, cannot determine initial brightness',
+ '(EE) Failed to load module "psb"',
+ '(EE) Failed to load module psb',
+ '(EE) Failed to load module "psbdrv"',
+ '(EE) Failed to load module psbdrv',
+ '(EE) open /dev/fb0: No such file or directory',
+ '(EE) AIGLX: reverting to software rendering',
+ ] + x86_common,
+ 'core2_32' : [
+ 'ACPI: No _BQC method, cannot determine initial brightness',
+ '[Firmware Bug]: ACPI: No _BQC method, cannot determine initial brightness',
+ '(EE) Failed to load module "psb"',
+ '(EE) Failed to load module psb',
+ '(EE) Failed to load module "psbdrv"',
+ '(EE) Failed to load module psbdrv',
+ '(EE) open /dev/fb0: No such file or directory',
+ '(EE) AIGLX: reverting to software rendering',
+ ] + x86_common,
+ 'intel-corei7-64' : [
+ "controller can't do DEVSLP, turning off",
+ ] + common_errors,
+ 'crownbay' : x86_common,
+ 'genericx86' : x86_common,
+ 'genericx86-64' : x86_common,
+ 'edgerouter' : [
+ 'Fatal server error:',
+ ] + common_errors,
+ 'minnow' : [
+ 'netlink init failed',
+ ] + common_errors,
+ 'jasperforest' : [
+ 'Activated service \'org.bluez\' failed:',
+ 'Unable to find NFC netlink family',
+ 'netlink init failed',
+ ] + common_errors,
+}
+
+log_locations = ["/var/log/","/var/log/dmesg", "/tmp/dmesg_output.log"]
+
+class ParseLogsTest(oeRuntimeTest):
+
+ @classmethod
+ def setUpClass(self):
+ self.errors = errors
+ self.ignore_errors = ignore_errors
+ self.log_locations = log_locations
+ self.msg = ""
+
+ def getMachine(self):
+ return oeRuntimeTest.tc.d.getVar("MACHINE", True)
+
+ #get some information on the CPU of the machine to display at the beginning of the output. This info might be useful in some cases.
+ def getHardwareInfo(self):
+ hwi = ""
+ (status, cpu_name) = self.target.run("cat /proc/cpuinfo | grep \"model name\" | head -n1 | awk 'BEGIN{FS=\":\"}{print $2}'")
+ (status, cpu_physical_cores) = self.target.run("cat /proc/cpuinfo | grep \"cpu cores\" | head -n1 | awk {'print $4'}")
+ (status, cpu_logical_cores) = self.target.run("cat /proc/cpuinfo | grep \"processor\" | wc -l")
+ (status, cpu_arch) = self.target.run("uname -m")
+ hwi += "Machine information: \n"
+ hwi += "*******************************\n"
+ hwi += "Machine name: "+self.getMachine()+"\n"
+ hwi += "CPU: "+str(cpu_name)+"\n"
+ hwi += "Arch: "+str(cpu_arch)+"\n"
+ hwi += "Physical cores: "+str(cpu_physical_cores)+"\n"
+ hwi += "Logical cores: "+str(cpu_logical_cores)+"\n"
+ hwi += "*******************************\n"
+ return hwi
+
+ #go through the log locations provided and if it's a folder create a list with all the .log files in it, if it's a file just add
+ #it to that list
+ def getLogList(self, log_locations):
+ logs = []
+ for location in log_locations:
+ (status, output) = self.target.run("test -f "+str(location))
+ if (status == 0):
+ logs.append(str(location))
+ else:
+ (status, output) = self.target.run("test -d "+str(location))
+ if (status == 0):
+ (status, output) = self.target.run("find "+str(location)+"/*.log -maxdepth 1 -type f")
+ if (status == 0):
+ output = output.splitlines()
+ for logfile in output:
+ logs.append(os.path.join(location,str(logfile)))
+ return logs
+
+ #copy the log files to be parsed locally
+ def transfer_logs(self, log_list):
+ target_logs = 'target_logs'
+ if not os.path.exists(target_logs):
+ os.makedirs(target_logs)
+ for f in log_list:
+ self.target.copy_from(f, target_logs)
+
+ #get the local list of logs
+ def get_local_log_list(self, log_locations):
+ self.transfer_logs(self.getLogList(log_locations))
+ logs = [ os.path.join('target_logs',f) for f in os.listdir('target_logs') if os.path.isfile(os.path.join('target_logs',f)) ]
+ return logs
+
+ #build the grep command to be used with filters and exclusions
+ def build_grepcmd(self, errors, ignore_errors, log):
+ grepcmd = "grep "
+ grepcmd +="-Ei \""
+ for error in errors:
+ grepcmd += error+"|"
+ grepcmd = grepcmd[:-1]
+ grepcmd += "\" "+str(log)+" | grep -Eiv \'"
+ try:
+ errorlist = ignore_errors[self.getMachine()]
+ except KeyError:
+ self.msg += "No ignore list found for this machine, using default\n"
+ errorlist = ignore_errors['default']
+ for ignore_error in errorlist:
+ ignore_error = ignore_error.replace("(", "\(")
+ ignore_error = ignore_error.replace(")", "\)")
+ ignore_error = ignore_error.replace("'", ".")
+ ignore_error = ignore_error.replace("?", "\?")
+ ignore_error = ignore_error.replace("[", "\[")
+ ignore_error = ignore_error.replace("]", "\]")
+ ignore_error = ignore_error.replace("*", "\*")
+ grepcmd += ignore_error+"|"
+ grepcmd = grepcmd[:-1]
+ grepcmd += "\'"
+ return grepcmd
+
+ #grep only the errors so that their context could be collected. Default context is 10 lines before and after the error itself
+ def parse_logs(self, errors, ignore_errors, logs, lines_before = 10, lines_after = 10):
+ results = {}
+ rez = []
+ grep_output = ''
+ for log in logs:
+ result = None
+ thegrep = self.build_grepcmd(errors, ignore_errors, log)
+ try:
+ result = subprocess.check_output(thegrep, shell=True)
+ except:
+ pass
+ if (result is not None):
+ results[log.replace('target_logs/','')] = {}
+ rez = result.splitlines()
+ for xrez in rez:
+ command = "grep \"\\"+str(xrez)+"\" -B "+str(lines_before)+" -A "+str(lines_after)+" "+str(log)
+ try:
+ grep_output = subprocess.check_output(command, shell=True)
+ except:
+ pass
+ results[log.replace('target_logs/','')][xrez]=grep_output
+ return results
+
+ #get the output of dmesg and write it in a file. This file is added to log_locations.
+ def write_dmesg(self):
+ (status, dmesg) = self.target.run("dmesg")
+ (status, dmesg2) = self.target.run("echo \""+str(dmesg)+"\" > /tmp/dmesg_output.log")
+
+ @testcase(1059)
+ @skipUnlessPassed('test_ssh')
+ def test_parselogs(self):
+ self.write_dmesg()
+ log_list = self.get_local_log_list(self.log_locations)
+ result = self.parse_logs(self.errors, self.ignore_errors, log_list)
+ print self.getHardwareInfo()
+ errcount = 0
+ for log in result:
+ self.msg += "Log: "+log+"\n"
+ self.msg += "-----------------------\n"
+ for error in result[log]:
+ errcount += 1
+ self.msg += "Central error: "+str(error)+"\n"
+ self.msg += "***********************\n"
+ self.msg += result[str(log)][str(error)]+"\n"
+ self.msg += "***********************\n"
+ self.msg += "%s errors found in logs." % errcount
+ self.assertEqual(errcount, 0, msg=self.msg)
diff --git a/meta/lib/oeqa/runtime/perl.py b/meta/lib/oeqa/runtime/perl.py
new file mode 100644
index 0000000..e044d0a
--- /dev/null
+++ b/meta/lib/oeqa/runtime/perl.py
@@ -0,0 +1,30 @@
+import unittest
+import os
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasPackage("perl"):
+ skipModule("No perl package in the image")
+
+
+class PerlTest(oeRuntimeTest):
+
+ @classmethod
+ def setUpClass(self):
+ oeRuntimeTest.tc.target.copy_to(os.path.join(oeRuntimeTest.tc.filesdir, "test.pl"), "/tmp/test.pl")
+
+ @testcase(1141)
+ def test_perl_exists(self):
+ (status, output) = self.target.run('which perl')
+ self.assertEqual(status, 0, msg="Perl binary not in PATH or not on target.")
+
+ @testcase(208)
+ def test_perl_works(self):
+ (status, output) = self.target.run('perl /tmp/test.pl')
+ self.assertEqual(status, 0, msg="Exit status was not 0. Output: %s" % output)
+ self.assertEqual(output, "the value of a is 0.01", msg="Incorrect output: %s" % output)
+
+ @classmethod
+ def tearDownClass(self):
+ oeRuntimeTest.tc.target.run("rm /tmp/test.pl")
diff --git a/meta/lib/oeqa/runtime/ping.py b/meta/lib/oeqa/runtime/ping.py
new file mode 100644
index 0000000..80c4601
--- /dev/null
+++ b/meta/lib/oeqa/runtime/ping.py
@@ -0,0 +1,22 @@
+import subprocess
+import unittest
+import sys
+import time
+from oeqa.oetest import oeRuntimeTest
+from oeqa.utils.decorators import *
+
+class PingTest(oeRuntimeTest):
+
+ @testcase(964)
+ def test_ping(self):
+ output = ''
+ count = 0
+ endtime = time.time() + 60
+ while count < 5 and time.time() < endtime:
+ proc = subprocess.Popen("ping -c 1 %s" % self.target.ip, shell=True, stdout=subprocess.PIPE)
+ output += proc.communicate()[0]
+ if proc.poll() == 0:
+ count += 1
+ else:
+ count = 0
+ self.assertEqual(count, 5, msg = "Expected 5 consecutive replies, got %d.\nping output is:\n%s" % (count,output))
diff --git a/meta/lib/oeqa/runtime/python.py b/meta/lib/oeqa/runtime/python.py
new file mode 100644
index 0000000..26edb7a
--- /dev/null
+++ b/meta/lib/oeqa/runtime/python.py
@@ -0,0 +1,35 @@
+import unittest
+import os
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasPackage("python"):
+ skipModule("No python package in the image")
+
+
+class PythonTest(oeRuntimeTest):
+
+ @classmethod
+ def setUpClass(self):
+ oeRuntimeTest.tc.target.copy_to(os.path.join(oeRuntimeTest.tc.filesdir, "test.py"), "/tmp/test.py")
+
+ @testcase(1145)
+ def test_python_exists(self):
+ (status, output) = self.target.run('which python')
+ self.assertEqual(status, 0, msg="Python binary not in PATH or not on target.")
+
+ @testcase(965)
+ def test_python_stdout(self):
+ (status, output) = self.target.run('python /tmp/test.py')
+ self.assertEqual(status, 0, msg="Exit status was not 0. Output: %s" % output)
+ self.assertEqual(output, "the value of a is 0.01", msg="Incorrect output: %s" % output)
+
+ @testcase(1146)
+ def test_python_testfile(self):
+ (status, output) = self.target.run('ls /tmp/testfile.python')
+ self.assertEqual(status, 0, msg="Python test file generate failed.")
+
+ @classmethod
+ def tearDownClass(self):
+ oeRuntimeTest.tc.target.run("rm /tmp/test.py /tmp/testfile.python")
diff --git a/meta/lib/oeqa/runtime/rpm.py b/meta/lib/oeqa/runtime/rpm.py
new file mode 100644
index 0000000..32aae24
--- /dev/null
+++ b/meta/lib/oeqa/runtime/rpm.py
@@ -0,0 +1,101 @@
+import unittest
+import os
+import fnmatch
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("package-management"):
+ skipModule("rpm module skipped: target doesn't have package-management in IMAGE_FEATURES")
+ if "package_rpm" != oeRuntimeTest.tc.d.getVar("PACKAGE_CLASSES", True).split()[0]:
+ skipModule("rpm module skipped: target doesn't have rpm as primary package manager")
+
+
+class RpmBasicTest(oeRuntimeTest):
+
+ @testcase(960)
+ @skipUnlessPassed('test_ssh')
+ def test_rpm_help(self):
+ (status, output) = self.target.run('rpm --help')
+ self.assertEqual(status, 0, msg="status and output: %s and %s" % (status,output))
+
+ @testcase(191)
+ @skipUnlessPassed('test_rpm_help')
+ def test_rpm_query(self):
+ (status, output) = self.target.run('rpm -q rpm')
+ self.assertEqual(status, 0, msg="status and output: %s and %s" % (status,output))
+
+class RpmInstallRemoveTest(oeRuntimeTest):
+
+ @classmethod
+ def setUpClass(self):
+ pkgarch = oeRuntimeTest.tc.d.getVar('TUNE_PKGARCH', True).replace("-", "_")
+ rpmdir = os.path.join(oeRuntimeTest.tc.d.getVar('DEPLOY_DIR', True), "rpm", pkgarch)
+ # pick rpm-doc as a test file to get installed, because it's small and it will always be built for standard targets
+ for f in fnmatch.filter(os.listdir(rpmdir), "rpm-doc-*.%s.rpm" % pkgarch):
+ testrpmfile = f
+ oeRuntimeTest.tc.target.copy_to(os.path.join(rpmdir,testrpmfile), "/tmp/rpm-doc.rpm")
+
+ @testcase(192)
+ @skipUnlessPassed('test_rpm_help')
+ def test_rpm_install(self):
+ (status, output) = self.target.run('rpm -ivh /tmp/rpm-doc.rpm')
+ self.assertEqual(status, 0, msg="Failed to install rpm-doc package: %s" % output)
+
+ @testcase(194)
+ @skipUnlessPassed('test_rpm_install')
+ def test_rpm_remove(self):
+ (status,output) = self.target.run('rpm -e rpm-doc')
+ self.assertEqual(status, 0, msg="Failed to remove rpm-doc package: %s" % output)
+
+ @testcase(1096)
+ @skipUnlessPassed('test_ssh')
+ def test_rpm_query_nonroot(self):
+ (status, output) = self.target.run('useradd test1')
+ self.assertTrue(status == 0, msg="Failed to create new user")
+ (status, output) = self.target.run('sudo -u test1 id')
+ self.assertTrue('(test1)' in output, msg="Failed to execute as new user")
+ (status, output) = self.target.run('sudo -u test1 rpm -qa')
+ self.assertEqual(status, 0, msg="status: %s. Cannot run rpm -qa" % status)
+
+ @testcase(195)
+ @skipUnlessPassed('test_rpm_install')
+ def test_check_rpm_install_removal_log_file_size(self):
+ """
+ Summary: Check rpm install/removal log file size
+ Expected: There should be some method to keep rpm log in a small size .
+ Product: BSPs
+ Author: Alexandru Georgescu <alexandru.c.georgescu@intel.com>
+ AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com>
+ """
+ db_files_cmd = 'ls /var/lib/rpm/__db.*'
+ get_log_size_cmd = "du /var/lib/rpm/log/log.* | awk '{print $1}'"
+
+ # Make sure that some database files are under /var/lib/rpm as '__db.xxx'
+ (status, output) = self.target.run(db_files_cmd)
+ self.assertEqual(0, status, 'Failed to find database files under /var/lib/rpm/ as __db.xxx')
+
+ # Remove the package just in case
+ self.target.run('rpm -e rpm-doc')
+
+ # Install/Remove a package 10 times
+ for i in range(10):
+ (status, output) = self.target.run('rpm -ivh /tmp/rpm-doc.rpm')
+ self.assertEqual(0, status, "Failed to install rpm-doc package. Reason: {}".format(output))
+
+ (status, output) = self.target.run('rpm -e rpm-doc')
+ self.assertEqual(0, status, "Failed to remove rpm-doc package. Reason: {}".format(output))
+
+ # Get the size of log file
+ (status, output) = self.target.run(get_log_size_cmd)
+ self.assertEqual(0, status, 'Failed to get the final size of the log file.')
+
+ # Compare each log size
+ for log_file_size in output:
+ self.assertLessEqual(int(log_file_size), 11264,
+ 'Log file size is greater that expected (~10MB), found {} bytes'.format(log_file_size))
+
+ @classmethod
+ def tearDownClass(self):
+ oeRuntimeTest.tc.target.run('rm -f /tmp/rpm-doc.rpm')
+
diff --git a/meta/lib/oeqa/runtime/scanelf.py b/meta/lib/oeqa/runtime/scanelf.py
new file mode 100644
index 0000000..43a024a
--- /dev/null
+++ b/meta/lib/oeqa/runtime/scanelf.py
@@ -0,0 +1,28 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasPackage("pax-utils"):
+ skipModule("pax-utils package not installed")
+
+class ScanelfTest(oeRuntimeTest):
+
+ def setUp(self):
+ self.scancmd = 'scanelf --quiet --recursive --mount --ldpath --path'
+
+ @testcase(966)
+ @skipUnlessPassed('test_ssh')
+ def test_scanelf_textrel(self):
+ # print TEXTREL information
+ self.scancmd += " --textrel"
+ (status, output) = self.target.run(self.scancmd)
+ self.assertEqual(output.strip(), "", "\n".join([self.scancmd, output]))
+
+ @testcase(967)
+ @skipUnlessPassed('test_ssh')
+ def test_scanelf_rpath(self):
+ # print RPATH information
+ self.scancmd += " --rpath"
+ (status, output) = self.target.run(self.scancmd)
+ self.assertEqual(output.strip(), "", "\n".join([self.scancmd, output]))
diff --git a/meta/lib/oeqa/runtime/scp.py b/meta/lib/oeqa/runtime/scp.py
new file mode 100644
index 0000000..48e87d2
--- /dev/null
+++ b/meta/lib/oeqa/runtime/scp.py
@@ -0,0 +1,22 @@
+import os
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import skipUnlessPassed, testcase
+
+def setUpModule():
+ if not (oeRuntimeTest.hasPackage("dropbear") or oeRuntimeTest.hasPackage("openssh-sshd")):
+ skipModule("No ssh package in image")
+
+class ScpTest(oeRuntimeTest):
+
+ @testcase(220)
+ @skipUnlessPassed('test_ssh')
+ def test_scp_file(self):
+ test_log_dir = oeRuntimeTest.tc.d.getVar("TEST_LOG_DIR", True)
+ test_file_path = os.path.join(test_log_dir, 'test_scp_file')
+ with open(test_file_path, 'w') as test_scp_file:
+ test_scp_file.seek(2 ** 22 - 1)
+ test_scp_file.write(os.linesep)
+ (status, output) = self.target.copy_to(test_file_path, '/tmp/test_scp_file')
+ self.assertEqual(status, 0, msg = "File could not be copied. Output: %s" % output)
+ (status, output) = self.target.run("ls -la /tmp/test_scp_file")
+ self.assertEqual(status, 0, msg = "SCP test failed")
diff --git a/meta/lib/oeqa/runtime/skeletoninit.py b/meta/lib/oeqa/runtime/skeletoninit.py
new file mode 100644
index 0000000..cb0cb9b
--- /dev/null
+++ b/meta/lib/oeqa/runtime/skeletoninit.py
@@ -0,0 +1,29 @@
+# This test should cover https://bugzilla.yoctoproject.org/tr_show_case.cgi?case_id=284 testcase
+# Note that the image under test must have meta-skeleton layer in bblayers and IMAGE_INSTALL_append = " service" in local.conf
+
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasPackage("service"):
+ skipModule("No service package in image")
+
+
+class SkeletonBasicTest(oeRuntimeTest):
+
+ @skipUnlessPassed('test_ssh')
+ @unittest.skipIf("systemd" == oeRuntimeTest.tc.d.getVar("VIRTUAL-RUNTIME_init_manager", False), "Not appropiate for systemd image")
+ def test_skeleton_availability(self):
+ (status, output) = self.target.run('ls /etc/init.d/skeleton')
+ self.assertEqual(status, 0, msg = "skeleton init script not found. Output:\n%s " % output)
+ (status, output) = self.target.run('ls /usr/sbin/skeleton-test')
+ self.assertEqual(status, 0, msg = "skeleton-test not found. Output:\n%s" % output)
+
+ @testcase(284)
+ @skipUnlessPassed('test_skeleton_availability')
+ @unittest.skipIf("systemd" == oeRuntimeTest.tc.d.getVar("VIRTUAL-RUNTIME_init_manager", False), "Not appropiate for systemd image")
+ def test_skeleton_script(self):
+ output1 = self.target.run("/etc/init.d/skeleton start")[1]
+ (status, output2) = self.target.run(oeRuntimeTest.pscmd + ' | grep [s]keleton-test')
+ self.assertEqual(status, 0, msg = "Skeleton script could not be started:\n%s\n%s" % (output1, output2))
diff --git a/meta/lib/oeqa/runtime/smart.py b/meta/lib/oeqa/runtime/smart.py
new file mode 100644
index 0000000..e41668d
--- /dev/null
+++ b/meta/lib/oeqa/runtime/smart.py
@@ -0,0 +1,175 @@
+import unittest
+import re
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+from oeqa.utils.httpserver import HTTPService
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("package-management"):
+ skipModule("Image doesn't have package management feature")
+ if not oeRuntimeTest.hasPackage("smart"):
+ skipModule("Image doesn't have smart installed")
+ if "package_rpm" != oeRuntimeTest.tc.d.getVar("PACKAGE_CLASSES", True).split()[0]:
+ skipModule("Rpm is not the primary package manager")
+
+class SmartTest(oeRuntimeTest):
+
+ @skipUnlessPassed('test_smart_help')
+ def smart(self, command, expected = 0):
+ command = 'smart %s' % command
+ status, output = self.target.run(command, 1500)
+ message = os.linesep.join([command, output])
+ self.assertEqual(status, expected, message)
+ self.assertFalse("Cannot allocate memory" in output, message)
+ return output
+
+class SmartBasicTest(SmartTest):
+
+ @testcase(716)
+ @skipUnlessPassed('test_ssh')
+ def test_smart_help(self):
+ self.smart('--help')
+
+ @testcase(968)
+ def test_smart_version(self):
+ self.smart('--version')
+
+ @testcase(721)
+ def test_smart_info(self):
+ self.smart('info python-smartpm')
+
+ @testcase(421)
+ def test_smart_query(self):
+ self.smart('query python-smartpm')
+
+ @testcase(720)
+ def test_smart_search(self):
+ self.smart('search python-smartpm')
+
+ @testcase(722)
+ def test_smart_stats(self):
+ self.smart('stats')
+
+class SmartRepoTest(SmartTest):
+
+ @classmethod
+ def setUpClass(self):
+ self.repolist = []
+ self.repo_server = HTTPService(oeRuntimeTest.tc.d.getVar('DEPLOY_DIR', True), oeRuntimeTest.tc.target.server_ip)
+ self.repo_server.start()
+
+ @classmethod
+ def tearDownClass(self):
+ self.repo_server.stop()
+ for i in self.repolist:
+ oeRuntimeTest.tc.target.run('smart channel -y --remove '+str(i))
+
+ @testcase(1143)
+ def test_smart_channel(self):
+ self.smart('channel', 1)
+
+ @testcase(719)
+ def test_smart_channel_add(self):
+ image_pkgtype = self.tc.d.getVar('IMAGE_PKGTYPE', True)
+ deploy_url = 'http://%s:%s/%s' %(self.target.server_ip, self.repo_server.port, image_pkgtype)
+ pkgarchs = self.tc.d.getVar('PACKAGE_ARCHS', True).replace("-","_").split()
+ for arch in os.listdir('%s/%s' % (self.repo_server.root_dir, image_pkgtype)):
+ if arch in pkgarchs:
+ self.smart('channel -y --add {a} type=rpm-md baseurl={u}/{a}'.format(a=arch, u=deploy_url))
+ self.repolist.append(arch)
+ self.smart('update')
+
+ @testcase(969)
+ def test_smart_channel_help(self):
+ self.smart('channel --help')
+
+ @testcase(970)
+ def test_smart_channel_list(self):
+ self.smart('channel --list')
+
+ @testcase(971)
+ def test_smart_channel_show(self):
+ self.smart('channel --show')
+
+ @testcase(717)
+ def test_smart_channel_rpmsys(self):
+ self.smart('channel --show rpmsys')
+ self.smart('channel --disable rpmsys')
+ self.smart('channel --enable rpmsys')
+
+ @testcase(1144)
+ @skipUnlessPassed('test_smart_channel_add')
+ def test_smart_install(self):
+ self.smart('remove -y psplash-default')
+ self.smart('install -y psplash-default')
+
+ @testcase(728)
+ @skipUnlessPassed('test_smart_install')
+ def test_smart_install_dependency(self):
+ self.smart('remove -y psplash')
+ self.smart('install -y psplash-default')
+
+ @testcase(723)
+ @skipUnlessPassed('test_smart_channel_add')
+ def test_smart_install_from_disk(self):
+ self.smart('remove -y psplash-default')
+ self.smart('download psplash-default')
+ self.smart('install -y ./psplash-default*')
+
+ @testcase(725)
+ @skipUnlessPassed('test_smart_channel_add')
+ def test_smart_install_from_http(self):
+ output = self.smart('download --urls psplash-default')
+ url = re.search('(http://.*/psplash-default.*\.rpm)', output)
+ self.assertTrue(url, msg="Couln't find download url in %s" % output)
+ self.smart('remove -y psplash-default')
+ self.smart('install -y %s' % url.group(0))
+
+ @testcase(729)
+ @skipUnlessPassed('test_smart_install')
+ def test_smart_reinstall(self):
+ self.smart('reinstall -y psplash-default')
+
+ @testcase(727)
+ @skipUnlessPassed('test_smart_channel_add')
+ def test_smart_remote_repo(self):
+ self.smart('update')
+ self.smart('install -y psplash')
+ self.smart('remove -y psplash')
+
+ @testcase(726)
+ def test_smart_local_dir(self):
+ self.target.run('mkdir /tmp/myrpmdir')
+ self.smart('channel --add myrpmdir type=rpm-dir path=/tmp/myrpmdir -y')
+ self.target.run('cd /tmp/myrpmdir')
+ self.smart('download psplash')
+ output = self.smart('channel --list')
+ for i in output.split("\n"):
+ if ("rpmsys" != str(i)) and ("myrpmdir" != str(i)):
+ self.smart('channel --disable '+str(i))
+ self.target.run('cd /home/root')
+ self.smart('install psplash')
+ for i in output.split("\n"):
+ if ("rpmsys" != str(i)) and ("myrpmdir" != str(i)):
+ self.smart('channel --enable '+str(i))
+ self.smart('channel --remove myrpmdir -y')
+ self.target.run("rm -rf /tmp/myrpmdir")
+
+ @testcase(718)
+ def test_smart_add_rpmdir(self):
+ self.target.run('mkdir /tmp/myrpmdir')
+ self.smart('channel --add myrpmdir type=rpm-dir path=/tmp/myrpmdir -y')
+ self.smart('channel --disable myrpmdir -y')
+ output = self.smart('channel --show myrpmdir')
+ self.assertTrue("disabled = yes" in output, msg="Failed to disable rpm dir")
+ self.smart('channel --enable myrpmdir -y')
+ output = self.smart('channel --show myrpmdir')
+ self.assertFalse("disabled = yes" in output, msg="Failed to enable rpm dir")
+ self.smart('channel --remove myrpmdir -y')
+ self.target.run("rm -rf /tmp/myrpmdir")
+
+ @testcase(731)
+ @skipUnlessPassed('test_smart_channel_add')
+ def test_smart_remove_package(self):
+ self.smart('install -y psplash')
+ self.smart('remove -y psplash')
\ No newline at end of file
diff --git a/meta/lib/oeqa/runtime/ssh.py b/meta/lib/oeqa/runtime/ssh.py
new file mode 100644
index 0000000..0e76d5d
--- /dev/null
+++ b/meta/lib/oeqa/runtime/ssh.py
@@ -0,0 +1,19 @@
+import subprocess
+import unittest
+import sys
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not (oeRuntimeTest.hasPackage("dropbear") or oeRuntimeTest.hasPackage("openssh")):
+ skipModule("No ssh package in image")
+
+class SshTest(oeRuntimeTest):
+
+ @testcase(224)
+ @skipUnlessPassed('test_ping')
+ def test_ssh(self):
+ (status, output) = self.target.run('uname -a')
+ self.assertEqual(status, 0, msg="SSH Test failed: %s" % output)
+ (status, output) = self.target.run('cat /etc/masterimage')
+ self.assertEqual(status, 1, msg="This isn't the right image - /etc/masterimage shouldn't be here %s" % output)
diff --git a/meta/lib/oeqa/runtime/syslog.py b/meta/lib/oeqa/runtime/syslog.py
new file mode 100644
index 0000000..2601dd9
--- /dev/null
+++ b/meta/lib/oeqa/runtime/syslog.py
@@ -0,0 +1,45 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not (oeRuntimeTest.hasPackage("busybox-syslog") or oeRuntimeTest.hasPackage("sysklogd")):
+ skipModule("No syslog package in image")
+
+class SyslogTest(oeRuntimeTest):
+
+ @testcase(201)
+ @skipUnlessPassed("test_syslog_help")
+ def test_syslog_running(self):
+ (status,output) = self.target.run(oeRuntimeTest.pscmd + ' | grep -i [s]yslogd')
+ self.assertEqual(status, 0, msg="no syslogd process, ps output: %s" % self.target.run(oeRuntimeTest.pscmd)[1])
+
+class SyslogTestConfig(oeRuntimeTest):
+
+ @testcase(1149)
+ @skipUnlessPassed("test_syslog_running")
+ def test_syslog_logger(self):
+ (status,output) = self.target.run('logger foobar && test -e /var/log/messages && grep foobar /var/log/messages || logread | grep foobar')
+ self.assertEqual(status, 0, msg="Test log string not found in /var/log/messages. Output: %s " % output)
+
+ @testcase(1150)
+ @skipUnlessPassed("test_syslog_running")
+ def test_syslog_restart(self):
+ if "systemd" != oeRuntimeTest.tc.d.getVar("VIRTUAL-RUNTIME_init_manager", False):
+ (status,output) = self.target.run('/etc/init.d/syslog restart')
+ else:
+ (status,output) = self.target.run('systemctl restart syslog.service')
+
+ @testcase(202)
+ @skipUnlessPassed("test_syslog_restart")
+ @skipUnlessPassed("test_syslog_logger")
+ @unittest.skipIf("systemd" == oeRuntimeTest.tc.d.getVar("VIRTUAL-RUNTIME_init_manager", False), "Not appropiate for systemd image")
+ @unittest.skipIf(oeRuntimeTest.hasPackage("sysklogd") or not oeRuntimeTest.hasPackage("busybox"), "Non-busybox syslog")
+ def test_syslog_startup_config(self):
+ self.target.run('echo "LOGFILE=/var/log/test" >> /etc/syslog-startup.conf')
+ (status,output) = self.target.run('/etc/init.d/syslog restart')
+ self.assertEqual(status, 0, msg="Could not restart syslog service. Status and output: %s and %s" % (status,output))
+ (status,output) = self.target.run('logger foobar && grep foobar /var/log/test')
+ self.assertEqual(status, 0, msg="Test log string not found. Output: %s " % output)
+ self.target.run("sed -i 's#LOGFILE=/var/log/test##' /etc/syslog-startup.conf")
+ self.target.run('/etc/init.d/syslog restart')
diff --git a/meta/lib/oeqa/runtime/systemd.py b/meta/lib/oeqa/runtime/systemd.py
new file mode 100644
index 0000000..c74394c
--- /dev/null
+++ b/meta/lib/oeqa/runtime/systemd.py
@@ -0,0 +1,101 @@
+import unittest
+import re
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("systemd"):
+ skipModule("target doesn't have systemd in DISTRO_FEATURES")
+ if "systemd" != oeRuntimeTest.tc.d.getVar("VIRTUAL-RUNTIME_init_manager", True):
+ skipModule("systemd is not the init manager for this image")
+
+
+class SystemdTest(oeRuntimeTest):
+
+ def systemctl(self, action = '', target = '', expected = 0, verbose = False):
+ command = 'systemctl %s %s' % (action, target)
+ status, output = self.target.run(command)
+ message = '\n'.join([command, output])
+ if status != expected and verbose:
+ message += self.target.run('systemctl status --full %s' % target)[1]
+ self.assertEqual(status, expected, message)
+ return output
+
+
+class SystemdBasicTests(SystemdTest):
+
+ @skipUnlessPassed('test_ssh')
+ def test_systemd_basic(self):
+ self.systemctl('--version')
+
+ @testcase(551)
+ @skipUnlessPassed('test_system_basic')
+ def test_systemd_list(self):
+ self.systemctl('list-unit-files')
+
+ def settle(self):
+ """
+ Block until systemd has finished activating any units being activated,
+ or until two minutes has elapsed.
+
+ Returns a tuple, either (True, '') if all units have finished
+ activating, or (False, message string) if there are still units
+ activating (generally, failing units that restart).
+ """
+ import time
+ endtime = time.time() + (60 * 2)
+ while True:
+ status, output = self.target.run('systemctl --state=activating')
+ if "0 loaded units listed" in output:
+ return (True, '')
+ if time.time() >= endtime:
+ return (False, output)
+ time.sleep(10)
+
+ @testcase(550)
+ @skipUnlessPassed('test_systemd_basic')
+ def test_systemd_failed(self):
+ settled, output = self.settle()
+ self.assertTrue(settled, msg="Timed out waiting for systemd to settle:\n" + output)
+
+ output = self.systemctl('list-units', '--failed')
+ match = re.search("0 loaded units listed", output)
+ if not match:
+ output += self.systemctl('status --full --failed')
+ self.assertTrue(match, msg="Some systemd units failed:\n%s" % output)
+
+
+class SystemdServiceTests(SystemdTest):
+
+ def check_for_avahi(self):
+ if not self.hasPackage('avahi-daemon'):
+ raise unittest.SkipTest("Testcase dependency not met: need avahi-daemon installed on target")
+
+ @skipUnlessPassed('test_systemd_basic')
+ def test_systemd_status(self):
+ self.check_for_avahi()
+ self.systemctl('status --full', 'avahi-daemon.service')
+
+ @testcase(695)
+ @skipUnlessPassed('test_systemd_status')
+ def test_systemd_stop_start(self):
+ self.check_for_avahi()
+ self.systemctl('stop', 'avahi-daemon.service')
+ self.systemctl('is-active', 'avahi-daemon.service', expected=3, verbose=True)
+ self.systemctl('start','avahi-daemon.service')
+ self.systemctl('is-active', 'avahi-daemon.service', verbose=True)
+
+ @testcase(696)
+ @skipUnlessPassed('test_systemd_basic')
+ def test_systemd_disable_enable(self):
+ self.check_for_avahi()
+ self.systemctl('disable', 'avahi-daemon.service')
+ self.systemctl('is-enabled', 'avahi-daemon.service', expected=1)
+ self.systemctl('enable', 'avahi-daemon.service')
+ self.systemctl('is-enabled', 'avahi-daemon.service')
+
+class SystemdJournalTests(SystemdTest):
+ @skipUnlessPassed('test_ssh')
+ def test_systemd_journal(self):
+ (status, output) = self.target.run('journalctl')
+ self.assertEqual(status, 0, output)
diff --git a/meta/lib/oeqa/runtime/vnc.py b/meta/lib/oeqa/runtime/vnc.py
new file mode 100644
index 0000000..f31deff
--- /dev/null
+++ b/meta/lib/oeqa/runtime/vnc.py
@@ -0,0 +1,20 @@
+from oeqa.oetest import oeRuntimeTest, skipModuleUnless
+from oeqa.utils.decorators import *
+import re
+
+def setUpModule():
+ skipModuleUnless(oeRuntimeTest.hasPackage('x11vnc'), "No x11vnc package in image")
+
+class VNCTest(oeRuntimeTest):
+
+ @testcase(213)
+ @skipUnlessPassed('test_ssh')
+ def test_vnc(self):
+ (status, output) = self.target.run('x11vnc -display :0 -bg -o x11vnc.log')
+ self.assertEqual(status, 0, msg="x11vnc server failed to start: %s" % output)
+ port = re.search('PORT=[0-9]*', output)
+ self.assertTrue(port, msg="Listening port not specified in command output: %s" %output)
+
+ vncport = port.group(0).split('=')[1]
+ (status, output) = self.target.run('netstat -ntl | grep ":%s"' % vncport)
+ self.assertEqual(status, 0, msg="x11vnc server not running on port %s\n\n%s" % (vncport, self.target.run('netstat -ntl; cat x11vnc.log')[1]))
diff --git a/meta/lib/oeqa/runtime/x32lib.py b/meta/lib/oeqa/runtime/x32lib.py
new file mode 100644
index 0000000..ce5e214
--- /dev/null
+++ b/meta/lib/oeqa/runtime/x32lib.py
@@ -0,0 +1,18 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ #check if DEFAULTTUNE is set and it's value is: x86-64-x32
+ defaulttune = oeRuntimeTest.tc.d.getVar("DEFAULTTUNE", True)
+ if "x86-64-x32" not in defaulttune:
+ skipModule("DEFAULTTUNE is not set to x86-64-x32")
+
+class X32libTest(oeRuntimeTest):
+
+ @testcase(281)
+ @skipUnlessPassed("test_ssh")
+ def test_x32_file(self):
+ status1 = self.target.run("readelf -h /bin/ls | grep Class | grep ELF32")[0]
+ status2 = self.target.run("readelf -h /bin/ls | grep Machine | grep X86-64")[0]
+ self.assertTrue(status1 == 0 and status2 == 0, msg="/bin/ls isn't an X86-64 ELF32 binary. readelf says: %s" % self.target.run("readelf -h /bin/ls")[1])
diff --git a/meta/lib/oeqa/runtime/xorg.py b/meta/lib/oeqa/runtime/xorg.py
new file mode 100644
index 0000000..12bcd37
--- /dev/null
+++ b/meta/lib/oeqa/runtime/xorg.py
@@ -0,0 +1,16 @@
+import unittest
+from oeqa.oetest import oeRuntimeTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeRuntimeTest.hasFeature("x11-base"):
+ skipModule("target doesn't have x11 in IMAGE_FEATURES")
+
+
+class XorgTest(oeRuntimeTest):
+
+ @testcase(1151)
+ @skipUnlessPassed('test_ssh')
+ def test_xorg_running(self):
+ (status, output) = self.target.run(oeRuntimeTest.pscmd + ' | grep -v xinit | grep [X]org')
+ self.assertEqual(status, 0, msg="Xorg does not appear to be running %s" % self.target.run(oeRuntimeTest.pscmd)[1])
diff --git a/meta/lib/oeqa/sdk/__init__.py b/meta/lib/oeqa/sdk/__init__.py
new file mode 100644
index 0000000..4cf3fa7
--- /dev/null
+++ b/meta/lib/oeqa/sdk/__init__.py
@@ -0,0 +1,3 @@
+# Enable other layers to have tests in the same named directory
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
diff --git a/meta/lib/oeqa/sdk/buildcvs.py b/meta/lib/oeqa/sdk/buildcvs.py
new file mode 100644
index 0000000..c7146fa
--- /dev/null
+++ b/meta/lib/oeqa/sdk/buildcvs.py
@@ -0,0 +1,25 @@
+from oeqa.oetest import oeSDKTest, skipModule
+from oeqa.utils.decorators import *
+from oeqa.utils.targetbuild import SDKBuildProject
+
+class BuildCvsTest(oeSDKTest):
+
+ @classmethod
+ def setUpClass(self):
+ self.project = SDKBuildProject(oeSDKTest.tc.sdktestdir + "/cvs/", oeSDKTest.tc.sdkenv, oeSDKTest.tc.d,
+ "http://ftp.gnu.org/non-gnu/cvs/source/feature/1.12.13/cvs-1.12.13.tar.bz2")
+ self.project.download_archive()
+
+ def test_cvs(self):
+ self.assertEqual(self.project.run_configure(), 0,
+ msg="Running configure failed")
+
+ self.assertEqual(self.project.run_make(), 0,
+ msg="Running make failed")
+
+ self.assertEqual(self.project.run_install(), 0,
+ msg="Running make install failed")
+
+ @classmethod
+ def tearDownClass(self):
+ self.project.clean()
diff --git a/meta/lib/oeqa/sdk/buildiptables.py b/meta/lib/oeqa/sdk/buildiptables.py
new file mode 100644
index 0000000..062e531
--- /dev/null
+++ b/meta/lib/oeqa/sdk/buildiptables.py
@@ -0,0 +1,26 @@
+from oeqa.oetest import oeSDKTest
+from oeqa.utils.decorators import *
+from oeqa.utils.targetbuild import SDKBuildProject
+
+
+class BuildIptablesTest(oeSDKTest):
+
+ @classmethod
+ def setUpClass(self):
+ self.project = SDKBuildProject(oeSDKTest.tc.sdktestdir + "/iptables/", oeSDKTest.tc.sdkenv, oeSDKTest.tc.d,
+ "http://netfilter.org/projects/iptables/files/iptables-1.4.13.tar.bz2")
+ self.project.download_archive()
+
+ def test_iptables(self):
+ self.assertEqual(self.project.run_configure(), 0,
+ msg="Running configure failed")
+
+ self.assertEqual(self.project.run_make(), 0,
+ msg="Running make failed")
+
+ self.assertEqual(self.project.run_install(), 0,
+ msg="Running make install failed")
+
+ @classmethod
+ def tearDownClass(self):
+ self.project.clean()
diff --git a/meta/lib/oeqa/sdk/buildsudoku.py b/meta/lib/oeqa/sdk/buildsudoku.py
new file mode 100644
index 0000000..dea77c6
--- /dev/null
+++ b/meta/lib/oeqa/sdk/buildsudoku.py
@@ -0,0 +1,26 @@
+from oeqa.oetest import oeSDKTest, skipModule
+from oeqa.utils.decorators import *
+from oeqa.utils.targetbuild import SDKBuildProject
+
+def setUpModule():
+ if not oeSDKTest.hasPackage("gtk\+"):
+ skipModule("Image doesn't have gtk+ in manifest")
+
+class SudokuTest(oeSDKTest):
+
+ @classmethod
+ def setUpClass(self):
+ self.project = SDKBuildProject(oeSDKTest.tc.sdktestdir + "/sudoku/", oeSDKTest.tc.sdkenv, oeSDKTest.tc.d,
+ "http://downloads.sourceforge.net/project/sudoku-savant/sudoku-savant/sudoku-savant-1.3/sudoku-savant-1.3.tar.bz2")
+ self.project.download_archive()
+
+ def test_sudoku(self):
+ self.assertEqual(self.project.run_configure(), 0,
+ msg="Running configure failed")
+
+ self.assertEqual(self.project.run_make(), 0,
+ msg="Running make failed")
+
+ @classmethod
+ def tearDownClass(self):
+ self.project.clean()
diff --git a/meta/lib/oeqa/sdk/gcc.py b/meta/lib/oeqa/sdk/gcc.py
new file mode 100644
index 0000000..67994b9
--- /dev/null
+++ b/meta/lib/oeqa/sdk/gcc.py
@@ -0,0 +1,36 @@
+import unittest
+import os
+import shutil
+from oeqa.oetest import oeSDKTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ machine = oeSDKTest.tc.d.getVar("MACHINE", True)
+ if not oeSDKTest.hasHostPackage("packagegroup-cross-canadian-" + machine):
+ skipModule("SDK doesn't contain a cross-canadian toolchain")
+
+
+class GccCompileTest(oeSDKTest):
+
+ @classmethod
+ def setUpClass(self):
+ for f in ['test.c', 'test.cpp', 'testmakefile']:
+ shutil.copyfile(os.path.join(self.tc.filesdir, f), self.tc.sdktestdir + f)
+
+ def test_gcc_compile(self):
+ self._run('$CC %s/test.c -o %s/test -lm' % (self.tc.sdktestdir, self.tc.sdktestdir))
+
+ def test_gpp_compile(self):
+ self._run('$CXX %s/test.c -o %s/test -lm' % (self.tc.sdktestdir, self.tc.sdktestdir))
+
+ def test_gpp2_compile(self):
+ self._run('$CXX %s/test.cpp -o %s/test -lm' % (self.tc.sdktestdir, self.tc.sdktestdir))
+
+ def test_make(self):
+ self._run('cd %s; make -f testmakefile' % self.tc.sdktestdir)
+
+ @classmethod
+ def tearDownClass(self):
+ files = [self.tc.sdktestdir + f for f in ['test.c', 'test.cpp', 'test.o', 'test', 'testmakefile']]
+ for f in files:
+ bb.utils.remove(f)
diff --git a/meta/lib/oeqa/sdk/perl.py b/meta/lib/oeqa/sdk/perl.py
new file mode 100644
index 0000000..45f422e
--- /dev/null
+++ b/meta/lib/oeqa/sdk/perl.py
@@ -0,0 +1,28 @@
+import unittest
+import os
+import shutil
+from oeqa.oetest import oeSDKTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeSDKTest.hasHostPackage("nativesdk-perl"):
+ skipModule("No perl package in the SDK")
+
+
+class PerlTest(oeSDKTest):
+
+ @classmethod
+ def setUpClass(self):
+ for f in ['test.pl']:
+ shutil.copyfile(os.path.join(self.tc.filesdir, f), self.tc.sdktestdir + f)
+ self.testfile = self.tc.sdktestdir + "test.pl"
+
+ def test_perl_exists(self):
+ self._run('which perl')
+
+ def test_perl_works(self):
+ self._run('perl %s/test.pl' % self.tc.sdktestdir)
+
+ @classmethod
+ def tearDownClass(self):
+ bb.utils.remove("%s/test.pl" % self.tc.sdktestdir)
diff --git a/meta/lib/oeqa/sdk/python.py b/meta/lib/oeqa/sdk/python.py
new file mode 100644
index 0000000..896fab4
--- /dev/null
+++ b/meta/lib/oeqa/sdk/python.py
@@ -0,0 +1,32 @@
+import unittest
+import os
+import shutil
+from oeqa.oetest import oeSDKTest, skipModule
+from oeqa.utils.decorators import *
+
+def setUpModule():
+ if not oeSDKTest.hasHostPackage("nativesdk-python"):
+ skipModule("No python package in the SDK")
+
+
+class PythonTest(oeSDKTest):
+
+ @classmethod
+ def setUpClass(self):
+ for f in ['test.py']:
+ shutil.copyfile(os.path.join(self.tc.filesdir, f), self.tc.sdktestdir + f)
+
+ def test_python_exists(self):
+ self._run('which python')
+
+ def test_python_stdout(self):
+ output = self._run('python %s/test.py' % self.tc.sdktestdir)
+ self.assertEqual(output.strip(), "the value of a is 0.01", msg="Incorrect output: %s" % output)
+
+ def test_python_testfile(self):
+ self._run('ls /tmp/testfile.python')
+
+ @classmethod
+ def tearDownClass(self):
+ bb.utils.remove("%s/test.py" % self.tc.sdktestdir)
+ bb.utils.remove("/tmp/testfile.python")
diff --git a/meta/lib/oeqa/selftest/__init__.py b/meta/lib/oeqa/selftest/__init__.py
new file mode 100644
index 0000000..3ad9513
--- /dev/null
+++ b/meta/lib/oeqa/selftest/__init__.py
@@ -0,0 +1,2 @@
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
diff --git a/meta/lib/oeqa/selftest/_sstatetests_noauto.py b/meta/lib/oeqa/selftest/_sstatetests_noauto.py
new file mode 100644
index 0000000..fc9ae7e
--- /dev/null
+++ b/meta/lib/oeqa/selftest/_sstatetests_noauto.py
@@ -0,0 +1,95 @@
+import datetime
+import unittest
+import os
+import re
+import shutil
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_test_layer
+from oeqa.selftest.sstate import SStateBase
+
+
+class RebuildFromSState(SStateBase):
+
+ @classmethod
+ def setUpClass(self):
+ self.builddir = os.path.join(os.environ.get('BUILDDIR'))
+
+ def get_dep_targets(self, primary_targets):
+ found_targets = []
+ bitbake("-g " + ' '.join(map(str, primary_targets)))
+ with open(os.path.join(self.builddir, 'pn-buildlist'), 'r') as pnfile:
+ found_targets = pnfile.read().splitlines()
+ return found_targets
+
+ def configure_builddir(self, builddir):
+ os.mkdir(builddir)
+ self.track_for_cleanup(builddir)
+ os.mkdir(os.path.join(builddir, 'conf'))
+ shutil.copyfile(os.path.join(os.environ.get('BUILDDIR'), 'conf/local.conf'), os.path.join(builddir, 'conf/local.conf'))
+ config = {}
+ config['default_sstate_dir'] = "SSTATE_DIR ?= \"${TOPDIR}/sstate-cache\""
+ config['null_sstate_mirrors'] = "SSTATE_MIRRORS = \"\""
+ config['default_tmp_dir'] = "TMPDIR = \"${TOPDIR}/tmp\""
+ for key in config:
+ ftools.append_file(os.path.join(builddir, 'conf/selftest.inc'), config[key])
+ shutil.copyfile(os.path.join(os.environ.get('BUILDDIR'), 'conf/bblayers.conf'), os.path.join(builddir, 'conf/bblayers.conf'))
+ try:
+ shutil.copyfile(os.path.join(os.environ.get('BUILDDIR'), 'conf/auto.conf'), os.path.join(builddir, 'conf/auto.conf'))
+ except:
+ pass
+
+ def hardlink_tree(self, src, dst):
+ os.mkdir(dst)
+ self.track_for_cleanup(dst)
+ for root, dirs, files in os.walk(src):
+ if root == src:
+ continue
+ os.mkdir(os.path.join(dst, root.split(src)[1][1:]))
+ for sstate_file in files:
+ os.link(os.path.join(root, sstate_file), os.path.join(dst, root.split(src)[1][1:], sstate_file))
+
+ def run_test_sstate_rebuild(self, primary_targets, relocate=False, rebuild_dependencies=False):
+ buildA = os.path.join(self.builddir, 'buildA')
+ if relocate:
+ buildB = os.path.join(self.builddir, 'buildB')
+ else:
+ buildB = buildA
+
+ if rebuild_dependencies:
+ rebuild_targets = self.get_dep_targets(primary_targets)
+ else:
+ rebuild_targets = primary_targets
+
+ self.configure_builddir(buildA)
+ runCmd((". %s/oe-init-build-env %s && " % (get_bb_var('COREBASE'), buildA)) + 'bitbake ' + ' '.join(map(str, primary_targets)), shell=True, executable='/bin/bash')
+ self.hardlink_tree(os.path.join(buildA, 'sstate-cache'), os.path.join(self.builddir, 'sstate-cache-buildA'))
+ shutil.rmtree(buildA)
+
+ failed_rebuild = []
+ failed_cleansstate = []
+ for target in rebuild_targets:
+ self.configure_builddir(buildB)
+ self.hardlink_tree(os.path.join(self.builddir, 'sstate-cache-buildA'), os.path.join(buildB, 'sstate-cache'))
+
+ result_cleansstate = runCmd((". %s/oe-init-build-env %s && " % (get_bb_var('COREBASE'), buildB)) + 'bitbake -ccleansstate ' + target, ignore_status=True, shell=True, executable='/bin/bash')
+ if not result_cleansstate.status == 0:
+ failed_cleansstate.append(target)
+ shutil.rmtree(buildB)
+ continue
+
+ result_build = runCmd((". %s/oe-init-build-env %s && " % (get_bb_var('COREBASE'), buildB)) + 'bitbake ' + target, ignore_status=True, shell=True, executable='/bin/bash')
+ if not result_build.status == 0:
+ failed_rebuild.append(target)
+
+ shutil.rmtree(buildB)
+
+ self.assertFalse(failed_rebuild, msg="The following recipes have failed to rebuild: %s" % ' '.join(map(str, failed_rebuild)))
+ self.assertFalse(failed_cleansstate, msg="The following recipes have failed cleansstate(all others have passed both cleansstate and rebuild from sstate tests): %s" % ' '.join(map(str, failed_cleansstate)))
+
+ def test_sstate_relocation(self):
+ self.run_test_sstate_rebuild(['core-image-sato-sdk'], relocate=True, rebuild_dependencies=True)
+
+ def test_sstate_rebuild(self):
+ self.run_test_sstate_rebuild(['core-image-sato-sdk'], relocate=False, rebuild_dependencies=True)
diff --git a/meta/lib/oeqa/selftest/_toaster.py b/meta/lib/oeqa/selftest/_toaster.py
new file mode 100644
index 0000000..c424659
--- /dev/null
+++ b/meta/lib/oeqa/selftest/_toaster.py
@@ -0,0 +1,320 @@
+import unittest
+import os
+import sys
+import shlex, subprocess
+import urllib, commands, time, getpass, re, json, shlex
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd
+
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../', 'bitbake/lib/toaster')))
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "toastermain.settings")
+
+import toastermain.settings
+from django.db.models import Q
+from orm.models import *
+from oeqa.utils.decorators import testcase
+
+class ToasterSetup(oeSelfTest):
+
+ def recipe_parse(self, file_path, var):
+ for line in open(file_path,'r'):
+ if line.find(var) > -1:
+ val = line.split(" = ")[1].replace("\"", "").strip()
+ return val
+
+ def fix_file_path(self, file_path):
+ if ":" in file_path:
+ file_path=file_path.split(":")[2]
+ return file_path
+
+class Toaster_DB_Tests(ToasterSetup):
+
+ # Check if build name is unique - tc_id=795
+ @testcase(795)
+ def test_Build_Unique_Name(self):
+ all_builds = Build.objects.all().count()
+ distinct_builds = Build.objects.values('id').distinct().count()
+ self.assertEqual(distinct_builds, all_builds, msg = 'Build name is not unique')
+
+ # Check if build coocker log path is unique - tc_id=819
+ @testcase(819)
+ def test_Build_Unique_Cooker_Log_Path(self):
+ distinct_path = Build.objects.values('cooker_log_path').distinct().count()
+ total_builds = Build.objects.values('id').count()
+ self.assertEqual(distinct_path, total_builds, msg = 'Build coocker log path is not unique')
+
+ # Check if task order is unique for one build - tc=824
+ @testcase(824)
+ def test_Task_Unique_Order(self):
+ builds = Build.objects.values('id')
+ cnt_err = []
+ for build in builds:
+ total_task_order = Task.objects.filter(build = build['id']).values('order').count()
+ distinct_task_order = Task.objects.filter(build = build['id']).values('order').distinct().count()
+ if (total_task_order != distinct_task_order):
+ cnt_err.append(build['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for build id: %s' % cnt_err)
+
+ # Check task order sequence for one build - tc=825
+ @testcase(825)
+ def test_Task_Order_Sequence(self):
+ builds = builds = Build.objects.values('id')
+ cnt_err = []
+ for build in builds:
+ tasks = Task.objects.filter(Q(build = build['id']), ~Q(order = None), ~Q(task_name__contains = '_setscene')).values('id', 'order').order_by("order")
+ cnt_tasks = 0
+ for task in tasks:
+ cnt_tasks += 1
+ if (task['order'] != cnt_tasks):
+ cnt_err.append(task['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task id: %s' % cnt_err)
+
+ # Check if disk_io matches the difference between EndTimeIO and StartTimeIO in build stats - tc=828
+ ### this needs to be updated ###
+ #def test_Task_Disk_IO_TC828(self):
+
+ # Check if outcome = 2 (SSTATE) then sstate_result must be 3 (RESTORED) - tc=832
+ @testcase(832)
+ def test_Task_If_Outcome_2_Sstate_Result_Must_Be_3(self):
+ tasks = Task.objects.filter(outcome = 2).values('id', 'sstate_result')
+ cnt_err = []
+ for task in tasks:
+ if (row['sstate_result'] != 3):
+ cnt_err.append(task['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task id: %s' % cnt_err)
+
+ # Check if outcome = 1 (COVERED) or 3 (EXISTING) then sstate_result must be 0 (SSTATE_NA) - tc=833
+ @testcase(833)
+ def test_Task_If_Outcome_1_3_Sstate_Result_Must_Be_0(self):
+ tasks = Task.objects.filter(outcome__in = (1, 3)).values('id', 'sstate_result')
+ cnt_err = []
+ for task in tasks:
+ if (task['sstate_result'] != 0):
+ cnt_err.append(task['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task id: %s' % cnt_err)
+
+ # Check if outcome is 0 (SUCCESS) or 4 (FAILED) then sstate_result must be 0 (NA), 1 (MISS) or 2 (FAILED) - tc=834
+ @testcase(834)
+ def test_Task_If_Outcome_0_4_Sstate_Result_Must_Be_0_1_2(self):
+ tasks = Task.objects.filter(outcome__in = (0, 4)).values('id', 'sstate_result')
+ cnt_err = []
+ for task in tasks:
+ if (task['sstate_result'] not in [0, 1, 2]):
+ cnt_err.append(task['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task id: %s' % cnt_err)
+
+ # Check if task_executed = TRUE (1), script_type must be 0 (CODING_NA), 2 (CODING_PYTHON), 3 (CODING_SHELL) - tc=891
+ @testcase(891)
+ def test_Task_If_Task_Executed_True_Script_Type_0_2_3(self):
+ tasks = Task.objects.filter(task_executed = 1).values('id', 'script_type')
+ cnt_err = []
+ for task in tasks:
+ if (task['script_type'] not in [0, 2, 3]):
+ cnt_err.append(task['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task id: %s' % cnt_err)
+
+ # Check if task_executed = TRUE (1), outcome must be 0 (SUCCESS) or 4 (FAILED) - tc=836
+ @testcase(836)
+ def test_Task_If_Task_Executed_True_Outcome_0_4(self):
+ tasks = Task.objects.filter(task_executed = 1).values('id', 'outcome')
+ cnt_err = []
+ for task in tasks:
+ if (task['outcome'] not in [0, 4]):
+ cnt_err.append(task['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task id: %s' % cnt_err)
+
+ # Check if task_executed = FALSE (0), script_type must be 0 - tc=890
+ @testcase(890)
+ def test_Task_If_Task_Executed_False_Script_Type_0(self):
+ tasks = Task.objects.filter(task_executed = 0).values('id', 'script_type')
+ cnt_err = []
+ for task in tasks:
+ if (task['script_type'] != 0):
+ cnt_err.append(task['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task id: %s' % cnt_err)
+
+ # Check if task_executed = FALSE (0) and build outcome = SUCCEEDED (0), task outcome must be 1 (COVERED), 2 (CACHED), 3 (PREBUILT), 5 (EMPTY) - tc=837
+ @testcase(837)
+ def test_Task_If_Task_Executed_False_Outcome_1_2_3_5(self):
+ builds = Build.objects.filter(outcome = 0).values('id')
+ cnt_err = []
+ for build in builds:
+ tasks = Task.objects.filter(build = build['id'], task_executed = 0).values('id', 'outcome')
+ for task in tasks:
+ if (task['outcome'] not in [1, 2, 3, 5]):
+ cnt_err.append(task['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task id: %s' % cnt_err)
+
+ # Key verification - tc=888
+ @testcase(888)
+ def test_Target_Installed_Package(self):
+ rows = Target_Installed_Package.objects.values('id', 'target_id', 'package_id')
+ cnt_err = []
+ for row in rows:
+ target = Target.objects.filter(id = row['target_id']).values('id')
+ package = Package.objects.filter(id = row['package_id']).values('id')
+ if (not target or not package):
+ cnt_err.append(row['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for target installed package id: %s' % cnt_err)
+
+ # Key verification - tc=889
+ @testcase(889)
+ def test_Task_Dependency(self):
+ rows = Task_Dependency.objects.values('id', 'task_id', 'depends_on_id')
+ cnt_err = []
+ for row in rows:
+ task_id = Task.objects.filter(id = row['task_id']).values('id')
+ depends_on_id = Task.objects.filter(id = row['depends_on_id']).values('id')
+ if (not task_id or not depends_on_id):
+ cnt_err.append(row['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for task dependency id: %s' % cnt_err)
+
+ # Check if build target file_name is populated only if is_image=true AND orm_build.outcome=0 then if the file exists and its size matches the file_size value
+ ### Need to add the tc in the test run
+ @testcase(1037)
+ def test_Target_File_Name_Populated(self):
+ builds = Build.objects.filter(outcome = 0).values('id')
+ for build in builds:
+ targets = Target.objects.filter(build_id = build['id'], is_image = 1).values('id')
+ for target in targets:
+ target_files = Target_Image_File.objects.filter(target_id = target['id']).values('id', 'file_name', 'file_size')
+ cnt_err = []
+ for file_info in target_files:
+ target_id = file_info['id']
+ target_file_name = file_info['file_name']
+ target_file_size = file_info['file_size']
+ if (not target_file_name or not target_file_size):
+ cnt_err.append(target_id)
+ else:
+ if (not os.path.exists(target_file_name)):
+ cnt_err.append(target_id)
+ else:
+ if (os.path.getsize(target_file_name) != target_file_size):
+ cnt_err.append(target_id)
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for target image file id: %s' % cnt_err)
+
+ # Key verification - tc=884
+ @testcase(884)
+ def test_Package_Dependency(self):
+ cnt_err = []
+ deps = Package_Dependency.objects.values('id', 'package_id', 'depends_on_id')
+ for dep in deps:
+ if (dep['package_id'] == dep['depends_on_id']):
+ cnt_err.append(dep['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for package dependency id: %s' % cnt_err)
+
+ # Recipe key verification, recipe name does not depends on a recipe having the same name - tc=883
+ @testcase(883)
+ def test_Recipe_Dependency(self):
+ deps = Recipe_Dependency.objects.values('id', 'recipe_id', 'depends_on_id')
+ cnt_err = []
+ for dep in deps:
+ if (not dep['recipe_id'] or not dep['depends_on_id']):
+ cnt_err.append(dep['id'])
+ else:
+ name = Recipe.objects.filter(id = dep['recipe_id']).values('name')
+ dep_name = Recipe.objects.filter(id = dep['depends_on_id']).values('name')
+ if (name == dep_name):
+ cnt_err.append(dep['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for recipe dependency id: %s' % cnt_err)
+
+ # Check if package name does not start with a number (0-9) - tc=846
+ @testcase(846)
+ def test_Package_Name_For_Number(self):
+ packages = Package.objects.filter(~Q(size = -1)).values('id', 'name')
+ cnt_err = []
+ for package in packages:
+ if (package['name'][0].isdigit() is True):
+ cnt_err.append(package['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for package id: %s' % cnt_err)
+
+ # Check if package version starts with a number (0-9) - tc=847
+ @testcase(847)
+ def test_Package_Version_Starts_With_Number(self):
+ packages = Package.objects.filter(~Q(size = -1)).values('id', 'version')
+ cnt_err = []
+ for package in packages:
+ if (package['version'][0].isdigit() is False):
+ cnt_err.append(package['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for package id: %s' % cnt_err)
+
+ # Check if package revision starts with 'r' - tc=848
+ @testcase(848)
+ def test_Package_Revision_Starts_With_r(self):
+ packages = Package.objects.filter(~Q(size = -1)).values('id', 'revision')
+ cnt_err = []
+ for package in packages:
+ if (package['revision'][0].startswith("r") is False):
+ cnt_err.append(package['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for package id: %s' % cnt_err)
+
+ # Check the validity of the package build_id
+ ### TC must be added in test run
+ @testcase(1038)
+ def test_Package_Build_Id(self):
+ packages = Package.objects.filter(~Q(size = -1)).values('id', 'build_id')
+ cnt_err = []
+ for package in packages:
+ build_id = Build.objects.filter(id = package['build_id']).values('id')
+ if (not build_id):
+ cnt_err.append(package['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for package id: %s' % cnt_err)
+
+ # Check the validity of package recipe_id
+ ### TC must be added in test run
+ @testcase(1039)
+ def test_Package_Recipe_Id(self):
+ packages = Package.objects.filter(~Q(size = -1)).values('id', 'recipe_id')
+ cnt_err = []
+ for package in packages:
+ recipe_id = Recipe.objects.filter(id = package['recipe_id']).values('id')
+ if (not recipe_id):
+ cnt_err.append(package['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for package id: %s' % cnt_err)
+
+ # Check if package installed_size field is not null
+ ### TC must be aded in test run
+ @testcase(1040)
+ def test_Package_Installed_Size_Not_NULL(self):
+ packages = Package.objects.filter(installed_size__isnull = True).values('id')
+ cnt_err = []
+ for package in packages:
+ cnt_err.append(package['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for package id: %s' % cnt_err)
+
+ # Check if all layers requests return exit code is 200 - tc=843
+ @testcase(843)
+ def test_Layers_Requests_Exit_Code(self):
+ layers = Layer.objects.values('id', 'layer_index_url')
+ cnt_err = []
+ for layer in layers:
+ resp = urllib.urlopen(layer['layer_index_url'])
+ if (resp.getcode() != 200):
+ cnt_err.append(layer['id'])
+ self.assertEqual(len(cnt_err), 0, msg = 'Errors for layer id: %s' % cnt_err)
+
+ # Check if django server starts regardless of the timezone set on the machine - tc=905
+ @testcase(905)
+ def test_Start_Django_Timezone(self):
+ current_path = os.getcwd()
+ zonefilelist = []
+ ZONEINFOPATH = '/usr/share/zoneinfo/'
+ os.chdir("../bitbake/lib/toaster/")
+ cnt_err = 0
+ for filename in os.listdir(ZONEINFOPATH):
+ if os.path.isfile(os.path.join(ZONEINFOPATH, filename)):
+ zonefilelist.append(filename)
+ for k in range(len(zonefilelist)):
+ if k <= 5:
+ files = zonefilelist[k]
+ os.system("export TZ="+str(files)+"; python manage.py runserver > /dev/null 2>&1 &")
+ time.sleep(3)
+ pid = subprocess.check_output("ps aux | grep '[/u]sr/bin/python manage.py runserver' | awk '{print $2}'", shell = True)
+ if pid:
+ os.system("kill -9 "+str(pid))
+ else:
+ cnt_err.append(zonefilelist[k])
+ self.assertEqual(cnt_err, 0, msg = 'Errors django server does not start with timezone: %s' % cnt_err)
+ os.chdir(current_path)
diff --git a/meta/lib/oeqa/selftest/base.py b/meta/lib/oeqa/selftest/base.py
new file mode 100644
index 0000000..b2faa66
--- /dev/null
+++ b/meta/lib/oeqa/selftest/base.py
@@ -0,0 +1,153 @@
+# Copyright (c) 2013 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+
+# DESCRIPTION
+# Base class inherited by test classes in meta/lib/selftest
+
+import unittest
+import os
+import sys
+import shutil
+import logging
+import errno
+
+import oeqa.utils.ftools as ftools
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_test_layer
+from oeqa.utils.decorators import LogResults
+
+@LogResults
+class oeSelfTest(unittest.TestCase):
+
+ log = logging.getLogger("selftest.base")
+ longMessage = True
+
+ def __init__(self, methodName="runTest"):
+ self.builddir = os.environ.get("BUILDDIR")
+ self.localconf_path = os.path.join(self.builddir, "conf/local.conf")
+ self.testinc_path = os.path.join(self.builddir, "conf/selftest.inc")
+ self.local_bblayers_path = os.path.join(self.builddir, "conf/bblayers.conf")
+ self.testinc_bblayers_path = os.path.join(self.builddir, "conf/bblayers.inc")
+ self.testlayer_path = oeSelfTest.testlayer_path
+ self._extra_tear_down_commands = []
+ self._track_for_cleanup = []
+ super(oeSelfTest, self).__init__(methodName)
+
+ def setUp(self):
+ os.chdir(self.builddir)
+ # we don't know what the previous test left around in config or inc files
+ # if it failed so we need a fresh start
+ try:
+ os.remove(self.testinc_path)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ for root, _, files in os.walk(self.testlayer_path):
+ for f in files:
+ if f == 'test_recipe.inc':
+ os.remove(os.path.join(root, f))
+ try:
+ os.remove(self.testinc_bblayers_path)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ # tests might need their own setup
+ # but if they overwrite this one they have to call
+ # super each time, so let's give them an alternative
+ self.setUpLocal()
+
+ def setUpLocal(self):
+ pass
+
+ def tearDown(self):
+ if self._extra_tear_down_commands:
+ failed_extra_commands = []
+ for command in self._extra_tear_down_commands:
+ result = runCmd(command, ignore_status=True)
+ if not result.status == 0:
+ failed_extra_commands.append(command)
+ if failed_extra_commands:
+ self.log.warning("tearDown commands have failed: %s" % ', '.join(map(str, failed_extra_commands)))
+ self.log.debug("Trying to move on.")
+ self._extra_tear_down_commands = []
+
+ if self._track_for_cleanup:
+ for path in self._track_for_cleanup:
+ if os.path.isdir(path):
+ shutil.rmtree(path)
+ if os.path.isfile(path):
+ os.remove(path)
+ self._track_for_cleanup = []
+
+ self.tearDownLocal()
+
+ def tearDownLocal(self):
+ pass
+
+ # add test specific commands to the tearDown method.
+ def add_command_to_tearDown(self, command):
+ self.log.debug("Adding command '%s' to tearDown for this test." % command)
+ self._extra_tear_down_commands.append(command)
+ # add test specific files or directories to be removed in the tearDown method
+ def track_for_cleanup(self, path):
+ self.log.debug("Adding path '%s' to be cleaned up when test is over" % path)
+ self._track_for_cleanup.append(path)
+
+ # write to <builddir>/conf/selftest.inc
+ def write_config(self, data):
+ self.log.debug("Writing to: %s\n%s\n" % (self.testinc_path, data))
+ ftools.write_file(self.testinc_path, data)
+
+ # append to <builddir>/conf/selftest.inc
+ def append_config(self, data):
+ self.log.debug("Appending to: %s\n%s\n" % (self.testinc_path, data))
+ ftools.append_file(self.testinc_path, data)
+
+ # remove data from <builddir>/conf/selftest.inc
+ def remove_config(self, data):
+ self.log.debug("Removing from: %s\n\%s\n" % (self.testinc_path, data))
+ ftools.remove_from_file(self.testinc_path, data)
+
+ # write to meta-sefltest/recipes-test/<recipe>/test_recipe.inc
+ def write_recipeinc(self, recipe, data):
+ inc_file = os.path.join(self.testlayer_path, 'recipes-test', recipe, 'test_recipe.inc')
+ self.log.debug("Writing to: %s\n%s\n" % (inc_file, data))
+ ftools.write_file(inc_file, data)
+
+ # append data to meta-sefltest/recipes-test/<recipe>/test_recipe.inc
+ def append_recipeinc(self, recipe, data):
+ inc_file = os.path.join(self.testlayer_path, 'recipes-test', recipe, 'test_recipe.inc')
+ self.log.debug("Appending to: %s\n%s\n" % (inc_file, data))
+ ftools.append_file(inc_file, data)
+
+ # remove data from meta-sefltest/recipes-test/<recipe>/test_recipe.inc
+ def remove_recipeinc(self, recipe, data):
+ inc_file = os.path.join(self.testlayer_path, 'recipes-test', recipe, 'test_recipe.inc')
+ self.log.debug("Removing from: %s\n%s\n" % (inc_file, data))
+ ftools.remove_from_file(inc_file, data)
+
+ # delete meta-sefltest/recipes-test/<recipe>/test_recipe.inc file
+ def delete_recipeinc(self, recipe):
+ inc_file = os.path.join(self.testlayer_path, 'recipes-test', recipe, 'test_recipe.inc')
+ self.log.debug("Deleting file: %s" % inc_file)
+ try:
+ os.remove(inc_file)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+
+ # write to <builddir>/conf/bblayers.inc
+ def write_bblayers_config(self, data):
+ self.log.debug("Writing to: %s\n%s\n" % (self.testinc_bblayers_path, data))
+ ftools.write_file(self.testinc_bblayers_path, data)
+
+ # append to <builddir>/conf/bblayers.inc
+ def append_bblayers_config(self, data):
+ self.log.debug("Appending to: %s\n%s\n" % (self.testinc_bblayers_path, data))
+ ftools.append_file(self.testinc_bblayers_path, data)
+
+ # remove data from <builddir>/conf/bblayers.inc
+ def remove_bblayers_config(self, data):
+ self.log.debug("Removing from: %s\n\%s\n" % (self.testinc_bblayers_path, data))
+ ftools.remove_from_file(self.testinc_bblayers_path, data)
diff --git a/meta/lib/oeqa/selftest/bblayers.py b/meta/lib/oeqa/selftest/bblayers.py
new file mode 100644
index 0000000..20c17e4
--- /dev/null
+++ b/meta/lib/oeqa/selftest/bblayers.py
@@ -0,0 +1,62 @@
+import unittest
+import os
+import logging
+import re
+import shutil
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, get_bb_var
+from oeqa.utils.decorators import testcase
+
+class BitbakeLayers(oeSelfTest):
+
+ @testcase(756)
+ def test_bitbakelayers_showcrossdepends(self):
+ result = runCmd('bitbake-layers show-cross-depends')
+ self.assertTrue('aspell' in result.output, msg = "No dependencies were shown. bitbake-layers show-cross-depends output: %s" % result.output)
+
+ @testcase(83)
+ def test_bitbakelayers_showlayers(self):
+ result = runCmd('bitbake-layers show-layers')
+ self.assertTrue('meta-selftest' in result.output, msg = "No layers were shown. bitbake-layers show-layers output: %s" % result.output)
+
+ @testcase(93)
+ def test_bitbakelayers_showappends(self):
+ result = runCmd('bitbake-layers show-appends')
+ self.assertTrue('xcursor-transparent-theme_0.1.1.bbappend' in result.output, msg="xcursor-transparent-theme_0.1.1.bbappend file was not recognised. bitbake-layers show-appends output: %s" % result.output)
+
+ @testcase(90)
+ def test_bitbakelayers_showoverlayed(self):
+ result = runCmd('bitbake-layers show-overlayed')
+ self.assertTrue('aspell' in result.output, msg="aspell overlayed recipe was not recognised bitbake-layers show-overlayed %s" % result.output)
+
+ @testcase(95)
+ def test_bitbakelayers_flatten(self):
+ testoutdir = os.path.join(self.builddir, 'test_bitbakelayers_flatten')
+ self.assertFalse(os.path.isdir(testoutdir), msg = "test_bitbakelayers_flatten should not exist at this point in time")
+ self.track_for_cleanup(testoutdir)
+ result = runCmd('bitbake-layers flatten %s' % testoutdir)
+ bb_file = os.path.join(testoutdir, 'recipes-graphics/xcursor-transparent-theme/xcursor-transparent-theme_0.1.1.bb')
+ self.assertTrue(os.path.isfile(bb_file), msg = "Cannot find xcursor-transparent-theme_0.1.1.bb in the test_bitbakelayers_flatten local dir.")
+ contents = ftools.read_file(bb_file)
+ find_in_contents = re.search("##### bbappended from meta-selftest #####\n(.*\n)*include test_recipe.inc", contents)
+ self.assertTrue(find_in_contents, msg = "Flattening layers did not work. bitbake-layers flatten output: %s" % result.output)
+
+ @testcase(1195)
+ def test_bitbakelayers_add_remove(self):
+ test_layer = os.path.join(get_bb_var('COREBASE'), 'meta-skeleton')
+ result = runCmd('bitbake-layers show-layers')
+ self.assertNotIn('meta-skeleton', result.output, "This test cannot run with meta-skeleton in bblayers.conf. bitbake-layers show-layers output: %s" % result.output)
+ result = runCmd('bitbake-layers add-layer %s' % test_layer)
+ result = runCmd('bitbake-layers show-layers')
+ self.assertIn('meta-skeleton', result.output, msg = "Something wrong happened. meta-skeleton layer was not added to conf/bblayers.conf. bitbake-layers show-layers output: %s" % result.output)
+ result = runCmd('bitbake-layers remove-layer %s' % test_layer)
+ result = runCmd('bitbake-layers show-layers')
+ self.assertNotIn('meta-skeleton', result.output, msg = "meta-skeleton should have been removed at this step. bitbake-layers show-layers output: %s" % result.output)
+ result = runCmd('bitbake-layers add-layer %s' % test_layer)
+ result = runCmd('bitbake-layers show-layers')
+ self.assertIn('meta-skeleton', result.output, msg = "Something wrong happened. meta-skeleton layer was not added to conf/bblayers.conf. bitbake-layers show-layers output: %s" % result.output)
+ result = runCmd('bitbake-layers remove-layer */meta-skeleton')
+ result = runCmd('bitbake-layers show-layers')
+ self.assertNotIn('meta-skeleton', result.output, msg = "meta-skeleton should have been removed at this step. bitbake-layers show-layers output: %s" % result.output)
diff --git a/meta/lib/oeqa/selftest/bbtests.py b/meta/lib/oeqa/selftest/bbtests.py
new file mode 100644
index 0000000..3d6860f
--- /dev/null
+++ b/meta/lib/oeqa/selftest/bbtests.py
@@ -0,0 +1,201 @@
+import unittest
+import os
+import logging
+import re
+import shutil
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var
+from oeqa.utils.decorators import testcase
+
+class BitbakeTests(oeSelfTest):
+
+ @testcase(789)
+ def test_run_bitbake_from_dir_1(self):
+ os.chdir(os.path.join(self.builddir, 'conf'))
+ self.assertEqual(bitbake('-e').status, 0, msg = "bitbake couldn't run from \"conf\" dir")
+
+ @testcase(790)
+ def test_run_bitbake_from_dir_2(self):
+ my_env = os.environ.copy()
+ my_env['BBPATH'] = my_env['BUILDDIR']
+ os.chdir(os.path.dirname(os.environ['BUILDDIR']))
+ self.assertEqual(bitbake('-e', env=my_env).status, 0, msg = "bitbake couldn't run from builddir")
+
+ @testcase(806)
+ def test_event_handler(self):
+ self.write_config("INHERIT += \"test_events\"")
+ result = bitbake('m4-native')
+ find_build_started = re.search("NOTE: Test for bb\.event\.BuildStarted(\n.*)*NOTE: Preparing RunQueue", result.output)
+ find_build_completed = re.search("Tasks Summary:.*(\n.*)*NOTE: Test for bb\.event\.BuildCompleted", result.output)
+ self.assertTrue(find_build_started, msg = "Match failed in:\n%s" % result.output)
+ self.assertTrue(find_build_completed, msg = "Match failed in:\n%s" % result.output)
+ self.assertFalse('Test for bb.event.InvalidEvent' in result.output, msg = "\"Test for bb.event.InvalidEvent\" message found during bitbake process. bitbake output: %s" % result.output)
+
+ @testcase(103)
+ def test_local_sstate(self):
+ bitbake('m4-native -ccleansstate')
+ bitbake('m4-native')
+ bitbake('m4-native -cclean')
+ result = bitbake('m4-native')
+ find_setscene = re.search("m4-native.*do_.*_setscene", result.output)
+ self.assertTrue(find_setscene, msg = "No \"m4-native.*do_.*_setscene\" message found during bitbake m4-native. bitbake output: %s" % result.output )
+
+ @testcase(105)
+ def test_bitbake_invalid_recipe(self):
+ result = bitbake('-b asdf', ignore_status=True)
+ self.assertTrue("ERROR: Unable to find any recipe file matching 'asdf'" in result.output, msg = "Though asdf recipe doesn't exist, bitbake didn't output any err. message. bitbake output: %s" % result.output)
+
+ @testcase(107)
+ def test_bitbake_invalid_target(self):
+ result = bitbake('asdf', ignore_status=True)
+ self.assertTrue("ERROR: Nothing PROVIDES 'asdf'" in result.output, msg = "Though no 'asdf' target exists, bitbake didn't output any err. message. bitbake output: %s" % result.output)
+
+ @testcase(106)
+ def test_warnings_errors(self):
+ result = bitbake('-b asdf', ignore_status=True)
+ find_warnings = re.search("Summary: There w.{2,3}? [1-9][0-9]* WARNING messages* shown", result.output)
+ find_errors = re.search("Summary: There w.{2,3}? [1-9][0-9]* ERROR messages* shown", result.output)
+ self.assertTrue(find_warnings, msg="Did not find the mumber of warnings at the end of the build:\n" + result.output)
+ self.assertTrue(find_errors, msg="Did not find the mumber of errors at the end of the build:\n" + result.output)
+
+ @testcase(108)
+ def test_invalid_patch(self):
+ self.write_recipeinc('man', 'SRC_URI += "file://man-1.5h1-make.patch"')
+ result = bitbake('man -c patch', ignore_status=True)
+ self.delete_recipeinc('man')
+ bitbake('-cclean man')
+ self.assertTrue("ERROR: Function failed: patch_do_patch" in result.output, msg = "Though no man-1.5h1-make.patch file exists, bitbake didn't output any err. message. bitbake output: %s" % result.output)
+
+ @testcase(163)
+ def test_force_task(self):
+ bitbake('m4-native')
+ self.add_command_to_tearDown('bitbake -c clean m4-native')
+ result = bitbake('-C compile m4-native')
+ look_for_tasks = ['do_compile', 'do_install', 'do_populate_sysroot']
+ for task in look_for_tasks:
+ find_task = re.search("m4-native.*%s" % task, result.output)
+ self.assertTrue(find_task, msg = "Couldn't find %s task. bitbake output %s" % (task, result.output))
+
+ @testcase(167)
+ def test_bitbake_g(self):
+ result = bitbake('-g core-image-full-cmdline')
+ for f in ['pn-buildlist', 'pn-depends.dot', 'package-depends.dot', 'task-depends.dot']:
+ self.addCleanup(os.remove, f)
+ self.assertTrue('NOTE: PN build list saved to \'pn-buildlist\'' in result.output, msg = "No dependency \"pn-buildlist\" file was generated for the given task target. bitbake output: %s" % result.output)
+ self.assertTrue('openssh' in ftools.read_file(os.path.join(self.builddir, 'pn-buildlist')), msg = "No \"openssh\" dependency found in pn-buildlist file.")
+
+ @testcase(899)
+ def test_image_manifest(self):
+ bitbake('core-image-minimal')
+ deploydir = get_bb_var("DEPLOY_DIR_IMAGE", target="core-image-minimal")
+ imagename = get_bb_var("IMAGE_LINK_NAME", target="core-image-minimal")
+ manifest = os.path.join(deploydir, imagename + ".manifest")
+ self.assertTrue(os.path.islink(manifest), msg="No manifest file created for image. It should have been created in %s" % manifest)
+
+ @testcase(168)
+ def test_invalid_recipe_src_uri(self):
+ data = 'SRC_URI = "file://invalid"'
+ self.write_recipeinc('man', data)
+ self.write_config("""DL_DIR = \"${TOPDIR}/download-selftest\"
+SSTATE_DIR = \"${TOPDIR}/download-selftest\"
+""")
+ bitbake('-ccleanall man')
+ result = bitbake('-c fetch man', ignore_status=True)
+ bitbake('-ccleanall man')
+ self.delete_recipeinc('man')
+ self.assertEqual(result.status, 1, msg="Command succeded when it should have failed. bitbake output: %s" % result.output)
+ self.assertTrue('Fetcher failure: Unable to find file file://invalid anywhere. The paths that were searched were:' in result.output, msg = "\"invalid\" file \
+doesn't exist, yet no error message encountered. bitbake output: %s" % result.output)
+ self.assertTrue('ERROR: Function failed: Fetcher failure for URL: \'file://invalid\'. Unable to fetch URL from any source.' in result.output, msg = "\"invalid\" file \
+doesn't exist, yet fetcher didn't report any error. bitbake output: %s" % result.output)
+
+ @testcase(171)
+ def test_rename_downloaded_file(self):
+ self.write_config("""DL_DIR = \"${TOPDIR}/download-selftest\"
+SSTATE_DIR = \"${TOPDIR}/download-selftest\"
+""")
+ data = 'SRC_URI_append = ";downloadfilename=test-aspell.tar.gz"'
+ self.write_recipeinc('aspell', data)
+ bitbake('-ccleanall aspell')
+ result = bitbake('-c fetch aspell', ignore_status=True)
+ self.delete_recipeinc('aspell')
+ self.addCleanup(bitbake, '-ccleanall aspell')
+ self.assertEqual(result.status, 0, msg = "Couldn't fetch aspell. %s" % result.output)
+ self.assertTrue(os.path.isfile(os.path.join(get_bb_var("DL_DIR"), 'test-aspell.tar.gz')), msg = "File rename failed. No corresponding test-aspell.tar.gz file found under %s" % str(get_bb_var("DL_DIR")))
+ self.assertTrue(os.path.isfile(os.path.join(get_bb_var("DL_DIR"), 'test-aspell.tar.gz.done')), "File rename failed. No corresponding test-aspell.tar.gz.done file found under %s" % str(get_bb_var("DL_DIR")))
+
+ @testcase(1028)
+ def test_environment(self):
+ self.append_config("TEST_ENV=\"localconf\"")
+ self.addCleanup(self.remove_config, "TEST_ENV=\"localconf\"")
+ result = runCmd('bitbake -e | grep TEST_ENV=')
+ self.assertTrue('localconf' in result.output, msg = "bitbake didn't report any value for TEST_ENV variable. To test, run 'bitbake -e | grep TEST_ENV='")
+
+ @testcase(1029)
+ def test_dry_run(self):
+ result = runCmd('bitbake -n m4-native')
+ self.assertEqual(0, result.status, "bitbake dry run didn't run as expected. %s" % result.output)
+
+ @testcase(1030)
+ def test_just_parse(self):
+ result = runCmd('bitbake -p')
+ self.assertEqual(0, result.status, "errors encountered when parsing recipes. %s" % result.output)
+
+ @testcase(1031)
+ def test_version(self):
+ result = runCmd('bitbake -s | grep wget')
+ find = re.search("wget *:([0-9a-zA-Z\.\-]+)", result.output)
+ self.assertTrue(find, "No version returned for searched recipe. bitbake output: %s" % result.output)
+
+ @testcase(1032)
+ def test_prefile(self):
+ preconf = os.path.join(self.builddir, 'conf/prefile.conf')
+ self.track_for_cleanup(preconf)
+ ftools.write_file(preconf ,"TEST_PREFILE=\"prefile\"")
+ result = runCmd('bitbake -r conf/prefile.conf -e | grep TEST_PREFILE=')
+ self.assertTrue('prefile' in result.output, "Preconfigure file \"prefile.conf\"was not taken into consideration. ")
+ self.append_config("TEST_PREFILE=\"localconf\"")
+ self.addCleanup(self.remove_config, "TEST_PREFILE=\"localconf\"")
+ result = runCmd('bitbake -r conf/prefile.conf -e | grep TEST_PREFILE=')
+ self.assertTrue('localconf' in result.output, "Preconfigure file \"prefile.conf\"was not taken into consideration.")
+
+ @testcase(1033)
+ def test_postfile(self):
+ postconf = os.path.join(self.builddir, 'conf/postfile.conf')
+ self.track_for_cleanup(postconf)
+ ftools.write_file(postconf , "TEST_POSTFILE=\"postfile\"")
+ self.append_config("TEST_POSTFILE=\"localconf\"")
+ self.addCleanup(self.remove_config, "TEST_POSTFILE=\"localconf\"")
+ result = runCmd('bitbake -R conf/postfile.conf -e | grep TEST_POSTFILE=')
+ self.assertTrue('postfile' in result.output, "Postconfigure file \"postfile.conf\"was not taken into consideration.")
+
+ @testcase(1034)
+ def test_checkuri(self):
+ result = runCmd('bitbake -c checkuri m4')
+ self.assertEqual(0, result.status, msg = "\"checkuri\" task was not executed. bitbake output: %s" % result.output)
+
+ @testcase(1035)
+ def test_continue(self):
+ self.write_config("""DL_DIR = \"${TOPDIR}/download-selftest\"
+SSTATE_DIR = \"${TOPDIR}/download-selftest\"
+""")
+ self.write_recipeinc('man',"\ndo_fail_task () {\nexit 1 \n}\n\naddtask do_fail_task before do_fetch\n" )
+ runCmd('bitbake -c cleanall man xcursor-transparent-theme')
+ result = runCmd('bitbake man xcursor-transparent-theme -k', ignore_status=True)
+ errorpos = result.output.find('ERROR: Function failed: do_fail_task')
+ manver = re.search("NOTE: recipe xcursor-transparent-theme-(.*?): task do_unpack: Started", result.output)
+ continuepos = result.output.find('NOTE: recipe xcursor-transparent-theme-%s: task do_unpack: Started' % manver.group(1))
+ self.assertLess(errorpos,continuepos, msg = "bitbake didn't pass do_fail_task. bitbake output: %s" % result.output)
+
+ @testcase(1119)
+ def test_non_gplv3(self):
+ data = 'INCOMPATIBLE_LICENSE = "GPLv3"'
+ conf = os.path.join(self.builddir, 'conf/local.conf')
+ ftools.append_file(conf ,data)
+ self.addCleanup(ftools.remove_from_file, conf ,data)
+ result = bitbake('readline', ignore_status=True)
+ self.assertEqual(result.status, 0, "Bitbake failed, exit code %s, output %s" % (result.status, result.output))
+ self.assertFalse(os.path.isfile(os.path.join(self.builddir, 'tmp/deploy/licenses/readline/generic_GPLv3')))
+ self.assertTrue(os.path.isfile(os.path.join(self.builddir, 'tmp/deploy/licenses/readline/generic_GPLv2')))
diff --git a/meta/lib/oeqa/selftest/buildhistory.py b/meta/lib/oeqa/selftest/buildhistory.py
new file mode 100644
index 0000000..d8cae46
--- /dev/null
+++ b/meta/lib/oeqa/selftest/buildhistory.py
@@ -0,0 +1,45 @@
+import unittest
+import os
+import re
+import shutil
+import datetime
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import Command, runCmd, bitbake, get_bb_var, get_test_layer
+
+
+class BuildhistoryBase(oeSelfTest):
+
+ def config_buildhistory(self, tmp_bh_location=False):
+ if (not 'buildhistory' in get_bb_var('USER_CLASSES')) and (not 'buildhistory' in get_bb_var('INHERIT')):
+ add_buildhistory_config = 'INHERIT += "buildhistory"\nBUILDHISTORY_COMMIT = "1"'
+ self.append_config(add_buildhistory_config)
+
+ if tmp_bh_location:
+ # Using a temporary buildhistory location for testing
+ tmp_bh_dir = os.path.join(self.builddir, "tmp_buildhistory_%s" % datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
+ buildhistory_dir_config = "BUILDHISTORY_DIR = \"%s\"" % tmp_bh_dir
+ self.append_config(buildhistory_dir_config)
+ self.track_for_cleanup(tmp_bh_dir)
+
+ def run_buildhistory_operation(self, target, global_config='', target_config='', change_bh_location=False, expect_error=False, error_regex=''):
+ if change_bh_location:
+ tmp_bh_location = True
+ else:
+ tmp_bh_location = False
+ self.config_buildhistory(tmp_bh_location)
+
+ self.append_config(global_config)
+ self.append_recipeinc(target, target_config)
+ bitbake("-cclean %s" % target)
+ result = bitbake(target, ignore_status=True)
+ self.remove_config(global_config)
+ self.remove_recipeinc(target, target_config)
+
+ if expect_error:
+ self.assertEqual(result.status, 1, msg="Error expected for global config '%s' and target config '%s'" % (global_config, target_config))
+ search_for_error = re.search(error_regex, result.output)
+ self.assertTrue(search_for_error, msg="Could not find desired error in output: %s" % error_regex)
+ else:
+ self.assertEqual(result.status, 0, msg="Command 'bitbake %s' has failed unexpectedly: %s" % (target, result.output))
diff --git a/meta/lib/oeqa/selftest/buildoptions.py b/meta/lib/oeqa/selftest/buildoptions.py
new file mode 100644
index 0000000..483803b
--- /dev/null
+++ b/meta/lib/oeqa/selftest/buildoptions.py
@@ -0,0 +1,149 @@
+import unittest
+import os
+import logging
+import re
+import glob as g
+import pexpect as p
+
+from oeqa.selftest.base import oeSelfTest
+from oeqa.selftest.buildhistory import BuildhistoryBase
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var
+import oeqa.utils.ftools as ftools
+from oeqa.utils.decorators import testcase
+
+class ImageOptionsTests(oeSelfTest):
+
+ @testcase(761)
+ def test_incremental_image_generation(self):
+ image_pkgtype = get_bb_var("IMAGE_PKGTYPE")
+ if image_pkgtype != 'rpm':
+ self.skipTest('Not using RPM as main package format')
+ bitbake("-c cleanall core-image-minimal")
+ self.write_config('INC_RPM_IMAGE_GEN = "1"')
+ self.append_config('IMAGE_FEATURES += "ssh-server-openssh"')
+ bitbake("core-image-minimal")
+ log_data_file = os.path.join(get_bb_var("WORKDIR", "core-image-minimal"), "temp/log.do_rootfs")
+ log_data_created = ftools.read_file(log_data_file)
+ incremental_created = re.search("NOTE: load old install solution for incremental install\nNOTE: old install solution not exist\nNOTE: creating new install solution for incremental install(\n.*)*NOTE: Installing the following packages:.*packagegroup-core-ssh-openssh", log_data_created)
+ self.remove_config('IMAGE_FEATURES += "ssh-server-openssh"')
+ self.assertTrue(incremental_created, msg = "Match failed in:\n%s" % log_data_created)
+ bitbake("core-image-minimal")
+ log_data_removed = ftools.read_file(log_data_file)
+ incremental_removed = re.search("NOTE: load old install solution for incremental install\nNOTE: creating new install solution for incremental install(\n.*)*NOTE: incremental removed:.*openssh-sshd-.*", log_data_removed)
+ self.assertTrue(incremental_removed, msg = "Match failed in:\n%s" % log_data_removed)
+
+ @testcase(925)
+ def test_rm_old_image(self):
+ bitbake("core-image-minimal")
+ deploydir = get_bb_var("DEPLOY_DIR_IMAGE", target="core-image-minimal")
+ imagename = get_bb_var("IMAGE_LINK_NAME", target="core-image-minimal")
+ deploydir_files = os.listdir(deploydir)
+ track_original_files = []
+ for image_file in deploydir_files:
+ if imagename in image_file and os.path.islink(os.path.join(deploydir, image_file)):
+ track_original_files.append(os.path.realpath(os.path.join(deploydir, image_file)))
+ self.append_config("RM_OLD_IMAGE = \"1\"")
+ bitbake("-C rootfs core-image-minimal")
+ deploydir_files = os.listdir(deploydir)
+ remaining_not_expected = [path for path in track_original_files if os.path.basename(path) in deploydir_files]
+ self.assertFalse(remaining_not_expected, msg="\nThe following image files were not removed: %s" % ', '.join(map(str, remaining_not_expected)))
+
+ @testcase(286)
+ def test_ccache_tool(self):
+ bitbake("ccache-native")
+ self.assertTrue(os.path.isfile(os.path.join(get_bb_var('STAGING_BINDIR_NATIVE', 'ccache-native'), "ccache")), msg = "No ccache found under %s" % str(get_bb_var('STAGING_BINDIR_NATIVE', 'ccache-native')))
+ self.write_config('INHERIT += "ccache"')
+ bitbake("m4 -c cleansstate")
+ bitbake("m4 -c compile")
+ self.addCleanup(bitbake, 'ccache-native -ccleansstate')
+ res = runCmd("grep ccache %s" % (os.path.join(get_bb_var("WORKDIR","m4"),"temp/log.do_compile")), ignore_status=True)
+ self.assertEqual(0, res.status, msg="No match for ccache in m4 log.do_compile. For further details: %s" % os.path.join(get_bb_var("WORKDIR","m4"),"temp/log.do_compile"))
+
+
+class DiskMonTest(oeSelfTest):
+
+ @testcase(277)
+ def test_stoptask_behavior(self):
+ self.write_config('BB_DISKMON_DIRS = "STOPTASKS,${TMPDIR},100000G,100K"')
+ res = bitbake("m4", ignore_status = True)
+ self.assertTrue('ERROR: No new tasks can be executed since the disk space monitor action is "STOPTASKS"!' in res.output, msg = "Tasks should have stopped. Disk monitor is set to STOPTASK: %s" % res.output)
+ self.assertEqual(res.status, 1, msg = "bitbake reported exit code %s. It should have been 1. Bitbake output: %s" % (str(res.status), res.output))
+ self.write_config('BB_DISKMON_DIRS = "ABORT,${TMPDIR},100000G,100K"')
+ res = bitbake("m4", ignore_status = True)
+ self.assertTrue('ERROR: Immediately abort since the disk space monitor action is "ABORT"!' in res.output, "Tasks should have been aborted immediatelly. Disk monitor is set to ABORT: %s" % res.output)
+ self.assertEqual(res.status, 1, msg = "bitbake reported exit code %s. It should have been 1. Bitbake output: %s" % (str(res.status), res.output))
+ self.write_config('BB_DISKMON_DIRS = "WARN,${TMPDIR},100000G,100K"')
+ res = bitbake("m4")
+ self.assertTrue('WARNING: The free space' in res.output, msg = "A warning should have been displayed for disk monitor is set to WARN: %s" %res.output)
+
+class SanityOptionsTest(oeSelfTest):
+
+ @testcase(927)
+ def test_options_warnqa_errorqa_switch(self):
+ bitbake("xcursor-transparent-theme -ccleansstate")
+
+ if "packages-list" not in get_bb_var("ERROR_QA"):
+ self.write_config("ERROR_QA_append = \" packages-list\"")
+
+ self.write_recipeinc('xcursor-transparent-theme', 'PACKAGES += \"${PN}-dbg\"')
+ res = bitbake("xcursor-transparent-theme", ignore_status=True)
+ self.delete_recipeinc('xcursor-transparent-theme')
+ self.assertTrue("ERROR: QA Issue: xcursor-transparent-theme-dbg is listed in PACKAGES multiple times, this leads to packaging errors." in res.output, msg=res.output)
+ self.assertEqual(res.status, 1, msg = "bitbake reported exit code %s. It should have been 1. Bitbake output: %s" % (str(res.status), res.output))
+ self.write_recipeinc('xcursor-transparent-theme', 'PACKAGES += \"${PN}-dbg\"')
+ self.append_config('ERROR_QA_remove = "packages-list"')
+ self.append_config('WARN_QA_append = " packages-list"')
+ bitbake("xcursor-transparent-theme -ccleansstate")
+ res = bitbake("xcursor-transparent-theme")
+ self.delete_recipeinc('xcursor-transparent-theme')
+ self.assertTrue("WARNING: QA Issue: xcursor-transparent-theme-dbg is listed in PACKAGES multiple times, this leads to packaging errors." in res.output, msg=res.output)
+
+ @testcase(278)
+ def test_sanity_userspace_dependency(self):
+ self.append_config('WARN_QA_append = " unsafe-references-in-binaries unsafe-references-in-scripts"')
+ bitbake("-ccleansstate gzip nfs-utils")
+ res = bitbake("gzip nfs-utils")
+ self.assertTrue("WARNING: QA Issue: gzip" in res.output, "WARNING: QA Issue: gzip message is not present in bitbake's output: %s" % res.output)
+ self.assertTrue("WARNING: QA Issue: nfs-utils" in res.output, "WARNING: QA Issue: nfs-utils message is not present in bitbake's output: %s" % res.output)
+
+class BuildhistoryTests(BuildhistoryBase):
+
+ @testcase(293)
+ def test_buildhistory_basic(self):
+ self.run_buildhistory_operation('xcursor-transparent-theme')
+ self.assertTrue(os.path.isdir(get_bb_var('BUILDHISTORY_DIR')), "buildhistory dir was not created.")
+
+ @testcase(294)
+ def test_buildhistory_buildtime_pr_backwards(self):
+ self.add_command_to_tearDown('cleanup-workdir')
+ target = 'xcursor-transparent-theme'
+ error = "ERROR: QA Issue: Package version for package %s went backwards which would break package feeds from (.*-r1 to .*-r0)" % target
+ self.run_buildhistory_operation(target, target_config="PR = \"r1\"", change_bh_location=True)
+ self.run_buildhistory_operation(target, target_config="PR = \"r0\"", change_bh_location=False, expect_error=True, error_regex=error)
+
+class BuildImagesTest(oeSelfTest):
+ @testcase(563)
+ def test_directfb(self):
+ """
+ This method is used to test the build of directfb image for arm arch.
+ In essence we build a coreimagedirectfb and test the exitcode of bitbake that in case of success is 0.
+ """
+ self.add_command_to_tearDown('cleanupworkdir')
+ self.write_config("DISTRO_FEATURES_remove = \"x11\"\nDISTRO_FEATURES_append = \" directfb\"\nMACHINE ??= \"qemuarm\"")
+ res = bitbake("core-image-directfb", ignore_status=True)
+ self.assertEqual(res.status, 0, "\ncoreimagedirectfb failed to build. Please check logs for further details.\nbitbake output %s" % res.output)
+
+class ArchiverTest(oeSelfTest):
+ @testcase(926)
+ def test_arch_work_dir_and_export_source(self):
+ """
+ Test for archiving the work directory and exporting the source files.
+ """
+ self.add_command_to_tearDown('cleanupworkdir')
+ self.write_config("INHERIT = \"archiver\"\nARCHIVER_MODE[src] = \"original\"\nARCHIVER_MODE[srpm] = \"1\"")
+ res = bitbake("xcursor-transparent-theme", ignore_status=True)
+ self.assertEqual(res.status, 0, "\nCouldn't build xcursortransparenttheme.\nbitbake output %s" % res.output)
+ pkgs_path = g.glob(str(self.builddir) + "/tmp/deploy/sources/allarch*/xcurs*")
+ src_file_glob = str(pkgs_path[0]) + "/xcursor*.src.rpm"
+ tar_file_glob = str(pkgs_path[0]) + "/xcursor*.tar.gz"
+ self.assertTrue((g.glob(src_file_glob) and g.glob(tar_file_glob)), "Couldn't find .src.rpm and .tar.gz files under tmp/deploy/sources/allarch*/xcursor*")
diff --git a/meta/lib/oeqa/selftest/devtool.py b/meta/lib/oeqa/selftest/devtool.py
new file mode 100644
index 0000000..6e731d6
--- /dev/null
+++ b/meta/lib/oeqa/selftest/devtool.py
@@ -0,0 +1,971 @@
+import unittest
+import os
+import logging
+import re
+import shutil
+import tempfile
+import glob
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer, runqemu
+from oeqa.utils.decorators import testcase
+
+class DevtoolBase(oeSelfTest):
+
+ def _test_recipe_contents(self, recipefile, checkvars, checkinherits):
+ with open(recipefile, 'r') as f:
+ for line in f:
+ if '=' in line:
+ splitline = line.split('=', 1)
+ var = splitline[0].rstrip()
+ value = splitline[1].strip().strip('"')
+ if var in checkvars:
+ needvalue = checkvars.pop(var)
+ self.assertEqual(value, needvalue, 'values for %s do not match' % var)
+ if line.startswith('inherit '):
+ inherits = line.split()[1:]
+
+ self.assertEqual(checkvars, {}, 'Some variables not found: %s' % checkvars)
+
+ for inherit in checkinherits:
+ self.assertIn(inherit, inherits, 'Missing inherit of %s' % inherit)
+
+ def _check_bbappend(self, testrecipe, recipefile, appenddir):
+ result = runCmd('bitbake-layers show-appends', cwd=self.builddir)
+ resultlines = result.output.splitlines()
+ inrecipe = False
+ bbappends = []
+ bbappendfile = None
+ for line in resultlines:
+ if inrecipe:
+ if line.startswith(' '):
+ bbappends.append(line.strip())
+ else:
+ break
+ elif line == '%s:' % os.path.basename(recipefile):
+ inrecipe = True
+ self.assertLessEqual(len(bbappends), 2, '%s recipe is being bbappended by another layer - bbappends found:\n %s' % (testrecipe, '\n '.join(bbappends)))
+ for bbappend in bbappends:
+ if bbappend.startswith(appenddir):
+ bbappendfile = bbappend
+ break
+ else:
+ self.fail('bbappend for recipe %s does not seem to be created in test layer' % testrecipe)
+ return bbappendfile
+
+ def _create_temp_layer(self, templayerdir, addlayer, templayername, priority=999, recipepathspec='recipes-*/*'):
+ create_temp_layer(templayerdir, templayername, priority, recipepathspec)
+ if addlayer:
+ self.add_command_to_tearDown('bitbake-layers remove-layer %s || true' % templayerdir)
+ result = runCmd('bitbake-layers add-layer %s' % templayerdir, cwd=self.builddir)
+
+ def _process_ls_output(self, output):
+ """
+ Convert ls -l output to a format we can reasonably compare from one context
+ to another (e.g. from host to target)
+ """
+ filelist = []
+ for line in output.splitlines():
+ splitline = line.split()
+ # Remove trailing . on perms
+ splitline[0] = splitline[0].rstrip('.')
+ # Remove leading . on paths
+ splitline[-1] = splitline[-1].lstrip('.')
+ # Drop fields we don't want to compare
+ del splitline[7]
+ del splitline[6]
+ del splitline[5]
+ del splitline[4]
+ del splitline[1]
+ filelist.append(' '.join(splitline))
+ return filelist
+
+
+class DevtoolTests(DevtoolBase):
+
+ @testcase(1158)
+ def test_create_workspace(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ result = runCmd('bitbake-layers show-layers')
+ self.assertTrue('/workspace' not in result.output, 'This test cannot be run with a workspace layer in bblayers.conf')
+ # Try creating a workspace layer with a specific path
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ result = runCmd('devtool create-workspace %s' % tempdir)
+ self.assertTrue(os.path.isfile(os.path.join(tempdir, 'conf', 'layer.conf')), msg = "No workspace created. devtool output: %s " % result.output)
+ result = runCmd('bitbake-layers show-layers')
+ self.assertIn(tempdir, result.output)
+ # Try creating a workspace layer with the default path
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ result = runCmd('devtool create-workspace')
+ self.assertTrue(os.path.isfile(os.path.join(workspacedir, 'conf', 'layer.conf')), msg = "No workspace created. devtool output: %s " % result.output)
+ result = runCmd('bitbake-layers show-layers')
+ self.assertNotIn(tempdir, result.output)
+ self.assertIn(workspacedir, result.output)
+
+ @testcase(1159)
+ def test_devtool_add(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # Fetch source
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ url = 'http://www.ivarch.com/programs/sources/pv-1.5.3.tar.bz2'
+ result = runCmd('wget %s' % url, cwd=tempdir)
+ result = runCmd('tar xfv pv-1.5.3.tar.bz2', cwd=tempdir)
+ srcdir = os.path.join(tempdir, 'pv-1.5.3')
+ self.assertTrue(os.path.isfile(os.path.join(srcdir, 'configure')), 'Unable to find configure script in source directory')
+ # Test devtool add
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake -c cleansstate pv')
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ result = runCmd('devtool add pv %s' % srcdir)
+ self.assertTrue(os.path.exists(os.path.join(workspacedir, 'conf', 'layer.conf')), 'Workspace directory not created')
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn('pv', result.output)
+ self.assertIn(srcdir, result.output)
+ # Clean up anything in the workdir/sysroot/sstate cache (have to do this *after* devtool add since the recipe only exists then)
+ bitbake('pv -c cleansstate')
+ # Test devtool build
+ result = runCmd('devtool build pv')
+ installdir = get_bb_var('D', 'pv')
+ self.assertTrue(installdir, 'Could not query installdir variable')
+ bindir = get_bb_var('bindir', 'pv')
+ self.assertTrue(bindir, 'Could not query bindir variable')
+ if bindir[0] == '/':
+ bindir = bindir[1:]
+ self.assertTrue(os.path.isfile(os.path.join(installdir, bindir, 'pv')), 'pv binary not found in D')
+
+ @testcase(1162)
+ def test_devtool_add_library(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # We don't have the ability to pick up this dependency automatically yet...
+ bitbake('libusb1')
+ # Fetch source
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ url = 'http://www.intra2net.com/en/developer/libftdi/download/libftdi1-1.1.tar.bz2'
+ result = runCmd('wget %s' % url, cwd=tempdir)
+ result = runCmd('tar xfv libftdi1-1.1.tar.bz2', cwd=tempdir)
+ srcdir = os.path.join(tempdir, 'libftdi1-1.1')
+ self.assertTrue(os.path.isfile(os.path.join(srcdir, 'CMakeLists.txt')), 'Unable to find CMakeLists.txt in source directory')
+ # Test devtool add (and use -V so we test that too)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ result = runCmd('devtool add libftdi %s -V 1.1' % srcdir)
+ self.assertTrue(os.path.exists(os.path.join(workspacedir, 'conf', 'layer.conf')), 'Workspace directory not created')
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn('libftdi', result.output)
+ self.assertIn(srcdir, result.output)
+ # Clean up anything in the workdir/sysroot/sstate cache (have to do this *after* devtool add since the recipe only exists then)
+ bitbake('libftdi -c cleansstate')
+ # Test devtool build
+ result = runCmd('devtool build libftdi')
+ staging_libdir = get_bb_var('STAGING_LIBDIR', 'libftdi')
+ self.assertTrue(staging_libdir, 'Could not query STAGING_LIBDIR variable')
+ self.assertTrue(os.path.isfile(os.path.join(staging_libdir, 'libftdi1.so.2.1.0')), "libftdi binary not found in STAGING_LIBDIR. Output of devtool build libftdi %s" % result.output)
+ # Test devtool reset
+ stampprefix = get_bb_var('STAMP', 'libftdi')
+ result = runCmd('devtool reset libftdi')
+ result = runCmd('devtool status')
+ self.assertNotIn('libftdi', result.output)
+ self.assertTrue(stampprefix, 'Unable to get STAMP value for recipe libftdi')
+ matches = glob.glob(stampprefix + '*')
+ self.assertFalse(matches, 'Stamp files exist for recipe libftdi that should have been cleaned')
+ self.assertFalse(os.path.isfile(os.path.join(staging_libdir, 'libftdi1.so.2.1.0')), 'libftdi binary still found in STAGING_LIBDIR after cleaning')
+
+ @testcase(1160)
+ def test_devtool_add_fetch(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # Fetch source
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ testver = '0.23'
+ url = 'https://pypi.python.org/packages/source/M/MarkupSafe/MarkupSafe-%s.tar.gz' % testver
+ testrecipe = 'python-markupsafe'
+ srcdir = os.path.join(tempdir, testrecipe)
+ # Test devtool add
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake -c cleansstate %s' % testrecipe)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ result = runCmd('devtool add %s %s -f %s' % (testrecipe, srcdir, url))
+ self.assertTrue(os.path.exists(os.path.join(workspacedir, 'conf', 'layer.conf')), 'Workspace directory not created. %s' % result.output)
+ self.assertTrue(os.path.isfile(os.path.join(srcdir, 'setup.py')), 'Unable to find setup.py in source directory')
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn(testrecipe, result.output)
+ self.assertIn(srcdir, result.output)
+ # Check recipe
+ recipefile = get_bb_var('FILE', testrecipe)
+ self.assertIn('%s.bb' % testrecipe, recipefile, 'Recipe file incorrectly named')
+ checkvars = {}
+ checkvars['S'] = '${WORKDIR}/MarkupSafe-%s' % testver
+ checkvars['SRC_URI'] = url
+ self._test_recipe_contents(recipefile, checkvars, [])
+ # Try with version specified
+ result = runCmd('devtool reset -n %s' % testrecipe)
+ shutil.rmtree(srcdir)
+ result = runCmd('devtool add %s %s -f %s -V %s' % (testrecipe, srcdir, url, testver))
+ self.assertTrue(os.path.isfile(os.path.join(srcdir, 'setup.py')), 'Unable to find setup.py in source directory')
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn(testrecipe, result.output)
+ self.assertIn(srcdir, result.output)
+ # Check recipe
+ recipefile = get_bb_var('FILE', testrecipe)
+ self.assertIn('%s_%s.bb' % (testrecipe, testver), recipefile, 'Recipe file incorrectly named')
+ checkvars = {}
+ checkvars['S'] = '${WORKDIR}/MarkupSafe-${PV}'
+ checkvars['SRC_URI'] = url.replace(testver, '${PV}')
+ self._test_recipe_contents(recipefile, checkvars, [])
+
+ @testcase(1161)
+ def test_devtool_add_fetch_git(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # Fetch source
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ url = 'git://git.yoctoproject.org/libmatchbox'
+ checkrev = '462f0652055d89c648ddd54fd7b03f175c2c6973'
+ testrecipe = 'libmatchbox2'
+ srcdir = os.path.join(tempdir, testrecipe)
+ # Test devtool add
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake -c cleansstate %s' % testrecipe)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ result = runCmd('devtool add %s %s -f %s' % (testrecipe, srcdir, url))
+ self.assertTrue(os.path.exists(os.path.join(workspacedir, 'conf', 'layer.conf')), 'Workspace directory not created: %s' % result.output)
+ self.assertTrue(os.path.isfile(os.path.join(srcdir, 'configure.ac')), 'Unable to find configure.ac in source directory')
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn(testrecipe, result.output)
+ self.assertIn(srcdir, result.output)
+ # Check recipe
+ recipefile = get_bb_var('FILE', testrecipe)
+ self.assertIn('_git.bb', recipefile, 'Recipe file incorrectly named')
+ checkvars = {}
+ checkvars['S'] = '${WORKDIR}/git'
+ checkvars['PV'] = '1.0+git${SRCPV}'
+ checkvars['SRC_URI'] = url
+ checkvars['SRCREV'] = '${AUTOREV}'
+ self._test_recipe_contents(recipefile, checkvars, [])
+ # Try with revision and version specified
+ result = runCmd('devtool reset -n %s' % testrecipe)
+ shutil.rmtree(srcdir)
+ url_rev = '%s;rev=%s' % (url, checkrev)
+ result = runCmd('devtool add %s %s -f "%s" -V 1.5' % (testrecipe, srcdir, url_rev))
+ self.assertTrue(os.path.isfile(os.path.join(srcdir, 'configure.ac')), 'Unable to find configure.ac in source directory')
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn(testrecipe, result.output)
+ self.assertIn(srcdir, result.output)
+ # Check recipe
+ recipefile = get_bb_var('FILE', testrecipe)
+ self.assertIn('_git.bb', recipefile, 'Recipe file incorrectly named')
+ checkvars = {}
+ checkvars['S'] = '${WORKDIR}/git'
+ checkvars['PV'] = '1.5+git${SRCPV}'
+ checkvars['SRC_URI'] = url
+ checkvars['SRCREV'] = checkrev
+ self._test_recipe_contents(recipefile, checkvars, [])
+
+ @testcase(1164)
+ def test_devtool_modify(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # Clean up anything in the workdir/sysroot/sstate cache
+ bitbake('mdadm -c cleansstate')
+ # Try modifying a recipe
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ self.add_command_to_tearDown('bitbake -c clean mdadm')
+ result = runCmd('devtool modify mdadm -x %s' % tempdir)
+ self.assertTrue(os.path.exists(os.path.join(tempdir, 'Makefile')), 'Extracted source could not be found')
+ self.assertTrue(os.path.isdir(os.path.join(tempdir, '.git')), 'git repository for external source tree not found')
+ self.assertTrue(os.path.exists(os.path.join(workspacedir, 'conf', 'layer.conf')), 'Workspace directory not created')
+ matches = glob.glob(os.path.join(workspacedir, 'appends', 'mdadm_*.bbappend'))
+ self.assertTrue(matches, 'bbappend not created %s' % result.output)
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn('mdadm', result.output)
+ self.assertIn(tempdir, result.output)
+ # Check git repo
+ result = runCmd('git status --porcelain', cwd=tempdir)
+ self.assertEqual(result.output.strip(), "", 'Created git repo is not clean')
+ result = runCmd('git symbolic-ref HEAD', cwd=tempdir)
+ self.assertEqual(result.output.strip(), "refs/heads/devtool", 'Wrong branch in git repo')
+ # Try building
+ bitbake('mdadm')
+ # Try making (minor) modifications to the source
+ result = runCmd("sed -i 's!^\.TH.*!.TH MDADM 8 \"\" v9.999-custom!' %s" % os.path.join(tempdir, 'mdadm.8.in'))
+ bitbake('mdadm -c package')
+ pkgd = get_bb_var('PKGD', 'mdadm')
+ self.assertTrue(pkgd, 'Could not query PKGD variable')
+ mandir = get_bb_var('mandir', 'mdadm')
+ self.assertTrue(mandir, 'Could not query mandir variable')
+ if mandir[0] == '/':
+ mandir = mandir[1:]
+ with open(os.path.join(pkgd, mandir, 'man8', 'mdadm.8'), 'r') as f:
+ for line in f:
+ if line.startswith('.TH'):
+ self.assertEqual(line.rstrip(), '.TH MDADM 8 "" v9.999-custom', 'man file not modified. man searched file path: %s' % os.path.join(pkgd, mandir, 'man8', 'mdadm.8'))
+ # Test devtool reset
+ stampprefix = get_bb_var('STAMP', 'mdadm')
+ result = runCmd('devtool reset mdadm')
+ result = runCmd('devtool status')
+ self.assertNotIn('mdadm', result.output)
+ self.assertTrue(stampprefix, 'Unable to get STAMP value for recipe mdadm')
+ matches = glob.glob(stampprefix + '*')
+ self.assertFalse(matches, 'Stamp files exist for recipe mdadm that should have been cleaned')
+
+ @testcase(1166)
+ def test_devtool_modify_invalid(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # Try modifying some recipes
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+
+ testrecipes = 'perf kernel-devsrc package-index core-image-minimal meta-toolchain packagegroup-core-sdk meta-ide-support'.split()
+ # Find actual name of gcc-source since it now includes the version - crude, but good enough for this purpose
+ result = runCmd('bitbake-layers show-recipes gcc-source*')
+ reading = False
+ for line in result.output.splitlines():
+ if line.startswith('=='):
+ reading = True
+ elif reading and not line.startswith(' '):
+ testrecipes.append(line.split(':')[0])
+ for testrecipe in testrecipes:
+ # Check it's a valid recipe
+ bitbake('%s -e' % testrecipe)
+ # devtool extract should fail
+ result = runCmd('devtool extract %s %s' % (testrecipe, os.path.join(tempdir, testrecipe)), ignore_status=True)
+ self.assertNotEqual(result.status, 0, 'devtool extract on %s should have failed. devtool output: %s' % (testrecipe, result.output))
+ self.assertNotIn('Fetching ', result.output, 'devtool extract on %s should have errored out before trying to fetch' % testrecipe)
+ self.assertIn('ERROR: ', result.output, 'devtool extract on %s should have given an ERROR' % testrecipe)
+ # devtool modify should fail
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, os.path.join(tempdir, testrecipe)), ignore_status=True)
+ self.assertNotEqual(result.status, 0, 'devtool modify on %s should have failed. devtool output: %s' % (testrecipe, result.output))
+ self.assertIn('ERROR: ', result.output, 'devtool modify on %s should have given an ERROR' % testrecipe)
+
+ def test_devtool_modify_native(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # Try modifying some recipes
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+
+ bbclassextended = False
+ inheritnative = False
+ testrecipes = 'mtools-native apt-native desktop-file-utils-native'.split()
+ for testrecipe in testrecipes:
+ checkextend = 'native' in (get_bb_var('BBCLASSEXTEND', testrecipe) or '').split()
+ if not bbclassextended:
+ bbclassextended = checkextend
+ if not inheritnative:
+ inheritnative = not checkextend
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, os.path.join(tempdir, testrecipe)))
+ self.assertNotIn('ERROR: ', result.output, 'ERROR in devtool modify output: %s' % result.output)
+ result = runCmd('devtool build %s' % testrecipe)
+ self.assertNotIn('ERROR: ', result.output, 'ERROR in devtool build output: %s' % result.output)
+ result = runCmd('devtool reset %s' % testrecipe)
+ self.assertNotIn('ERROR: ', result.output, 'ERROR in devtool reset output: %s' % result.output)
+
+ self.assertTrue(bbclassextended, 'None of these recipes are BBCLASSEXTENDed to native - need to adjust testrecipes list: %s' % ', '.join(testrecipes))
+ self.assertTrue(inheritnative, 'None of these recipes do "inherit native" - need to adjust testrecipes list: %s' % ', '.join(testrecipes))
+
+
+ @testcase(1165)
+ def test_devtool_modify_git(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ testrecipe = 'mkelfimage'
+ src_uri = get_bb_var('SRC_URI', testrecipe)
+ self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
+ # Clean up anything in the workdir/sysroot/sstate cache
+ bitbake('%s -c cleansstate' % testrecipe)
+ # Try modifying a recipe
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
+ self.assertTrue(os.path.exists(os.path.join(tempdir, 'Makefile')), 'Extracted source could not be found')
+ self.assertTrue(os.path.isdir(os.path.join(tempdir, '.git')), 'git repository for external source tree not found')
+ self.assertTrue(os.path.exists(os.path.join(workspacedir, 'conf', 'layer.conf')), 'Workspace directory not created. devtool output: %s' % result.output)
+ matches = glob.glob(os.path.join(workspacedir, 'appends', 'mkelfimage_*.bbappend'))
+ self.assertTrue(matches, 'bbappend not created')
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn(testrecipe, result.output)
+ self.assertIn(tempdir, result.output)
+ # Check git repo
+ result = runCmd('git status --porcelain', cwd=tempdir)
+ self.assertEqual(result.output.strip(), "", 'Created git repo is not clean')
+ result = runCmd('git symbolic-ref HEAD', cwd=tempdir)
+ self.assertEqual(result.output.strip(), "refs/heads/devtool", 'Wrong branch in git repo')
+ # Try building
+ bitbake(testrecipe)
+
+ @testcase(1167)
+ def test_devtool_modify_localfiles(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ testrecipe = 'lighttpd'
+ src_uri = (get_bb_var('SRC_URI', testrecipe) or '').split()
+ foundlocal = False
+ for item in src_uri:
+ if item.startswith('file://') and '.patch' not in item:
+ foundlocal = True
+ break
+ self.assertTrue(foundlocal, 'This test expects the %s recipe to fetch local files and it seems that it no longer does' % testrecipe)
+ # Clean up anything in the workdir/sysroot/sstate cache
+ bitbake('%s -c cleansstate' % testrecipe)
+ # Try modifying a recipe
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
+ self.assertTrue(os.path.exists(os.path.join(tempdir, 'configure.ac')), 'Extracted source could not be found')
+ self.assertTrue(os.path.exists(os.path.join(workspacedir, 'conf', 'layer.conf')), 'Workspace directory not created')
+ matches = glob.glob(os.path.join(workspacedir, 'appends', '%s_*.bbappend' % testrecipe))
+ self.assertTrue(matches, 'bbappend not created')
+ # Test devtool status
+ result = runCmd('devtool status')
+ self.assertIn(testrecipe, result.output)
+ self.assertIn(tempdir, result.output)
+ # Try building
+ bitbake(testrecipe)
+
+ @testcase(1169)
+ def test_devtool_update_recipe(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ testrecipe = 'minicom'
+ recipefile = get_bb_var('FILE', testrecipe)
+ src_uri = get_bb_var('SRC_URI', testrecipe)
+ self.assertNotIn('git://', src_uri, 'This test expects the %s recipe to NOT be a git recipe' % testrecipe)
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe)
+ # First, modify a recipe
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ # (don't bother with cleaning the recipe on teardown, we won't be building it)
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
+ # Check git repo
+ self.assertTrue(os.path.isdir(os.path.join(tempdir, '.git')), 'git repository for external source tree not found')
+ result = runCmd('git status --porcelain', cwd=tempdir)
+ self.assertEqual(result.output.strip(), "", 'Created git repo is not clean')
+ result = runCmd('git symbolic-ref HEAD', cwd=tempdir)
+ self.assertEqual(result.output.strip(), "refs/heads/devtool", 'Wrong branch in git repo')
+ # Add a couple of commits
+ # FIXME: this only tests adding, need to also test update and remove
+ result = runCmd('echo "Additional line" >> README', cwd=tempdir)
+ result = runCmd('git commit -a -m "Change the README"', cwd=tempdir)
+ result = runCmd('echo "A new file" > devtool-new-file', cwd=tempdir)
+ result = runCmd('git add devtool-new-file', cwd=tempdir)
+ result = runCmd('git commit -m "Add a new file"', cwd=tempdir)
+ self.add_command_to_tearDown('cd %s; rm %s/*.patch; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
+ result = runCmd('devtool update-recipe %s' % testrecipe)
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ self.assertNotEqual(result.output.strip(), "", '%s recipe should be modified' % testrecipe)
+ status = result.output.splitlines()
+ self.assertEqual(len(status), 3, 'Less/more files modified than expected. Entire status:\n%s' % result.output)
+ for line in status:
+ if line.endswith('0001-Change-the-README.patch'):
+ self.assertEqual(line[:3], '?? ', 'Unexpected status in line: %s' % line)
+ elif line.endswith('0002-Add-a-new-file.patch'):
+ self.assertEqual(line[:3], '?? ', 'Unexpected status in line: %s' % line)
+ elif re.search('%s_[^_]*.bb$' % testrecipe, line):
+ self.assertEqual(line[:3], ' M ', 'Unexpected status in line: %s' % line)
+ else:
+ raise AssertionError('Unexpected modified file in status: %s' % line)
+
+ @testcase(1172)
+ def test_devtool_update_recipe_git(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ testrecipe = 'mtd-utils'
+ recipefile = get_bb_var('FILE', testrecipe)
+ src_uri = get_bb_var('SRC_URI', testrecipe)
+ self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
+ patches = []
+ for entry in src_uri.split():
+ if entry.startswith('file://') and entry.endswith('.patch'):
+ patches.append(entry[7:].split(';')[0])
+ self.assertGreater(len(patches), 0, 'The %s recipe does not appear to contain any patches, so this test will not be effective' % testrecipe)
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe)
+ # First, modify a recipe
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ # (don't bother with cleaning the recipe on teardown, we won't be building it)
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
+ # Check git repo
+ self.assertTrue(os.path.isdir(os.path.join(tempdir, '.git')), 'git repository for external source tree not found')
+ result = runCmd('git status --porcelain', cwd=tempdir)
+ self.assertEqual(result.output.strip(), "", 'Created git repo is not clean')
+ result = runCmd('git symbolic-ref HEAD', cwd=tempdir)
+ self.assertEqual(result.output.strip(), "refs/heads/devtool", 'Wrong branch in git repo')
+ # Add a couple of commits
+ # FIXME: this only tests adding, need to also test update and remove
+ result = runCmd('echo "# Additional line" >> Makefile', cwd=tempdir)
+ result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempdir)
+ result = runCmd('echo "A new file" > devtool-new-file', cwd=tempdir)
+ result = runCmd('git add devtool-new-file', cwd=tempdir)
+ result = runCmd('git commit -m "Add a new file"', cwd=tempdir)
+ self.add_command_to_tearDown('cd %s; rm -rf %s; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, testrecipe, os.path.basename(recipefile)))
+ result = runCmd('devtool update-recipe -m srcrev %s' % testrecipe)
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ self.assertNotEqual(result.output.strip(), "", '%s recipe should be modified' % testrecipe)
+ status = result.output.splitlines()
+ for line in status:
+ for patch in patches:
+ if line.endswith(patch):
+ self.assertEqual(line[:3], ' D ', 'Unexpected status in line: %s' % line)
+ break
+ else:
+ if re.search('%s_[^_]*.bb$' % testrecipe, line):
+ self.assertEqual(line[:3], ' M ', 'Unexpected status in line: %s' % line)
+ else:
+ raise AssertionError('Unexpected modified file in status: %s' % line)
+ result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile))
+ addlines = ['SRCREV = ".*"', 'SRC_URI = "git://git.infradead.org/mtd-utils.git"']
+ srcurilines = src_uri.split()
+ srcurilines[0] = 'SRC_URI = "' + srcurilines[0]
+ srcurilines.append('"')
+ removelines = ['SRCREV = ".*"'] + srcurilines
+ for line in result.output.splitlines():
+ if line.startswith('+++') or line.startswith('---'):
+ continue
+ elif line.startswith('+'):
+ matched = False
+ for item in addlines:
+ if re.match(item, line[1:].strip()):
+ matched = True
+ break
+ self.assertTrue(matched, 'Unexpected diff add line: %s' % line)
+ elif line.startswith('-'):
+ matched = False
+ for item in removelines:
+ if re.match(item, line[1:].strip()):
+ matched = True
+ break
+ self.assertTrue(matched, 'Unexpected diff remove line: %s' % line)
+ # Now try with auto mode
+ runCmd('cd %s; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, os.path.basename(recipefile)))
+ result = runCmd('devtool update-recipe %s' % testrecipe)
+ result = runCmd('git rev-parse --show-toplevel')
+ topleveldir = result.output.strip()
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ status = result.output.splitlines()
+ relpatchpath = os.path.join(os.path.relpath(os.path.dirname(recipefile), topleveldir), testrecipe)
+ expectedstatus = [('M', os.path.relpath(recipefile, topleveldir)),
+ ('??', '%s/0001-Change-the-Makefile.patch' % relpatchpath),
+ ('??', '%s/0002-Add-a-new-file.patch' % relpatchpath)]
+ for line in status:
+ statusline = line.split(None, 1)
+ for fstatus, fn in expectedstatus:
+ if fn == statusline[1]:
+ if fstatus != statusline[0]:
+ self.fail('Unexpected status in line: %s' % line)
+ break
+ else:
+ self.fail('Unexpected modified file in line: %s' % line)
+
+ @testcase(1170)
+ def test_devtool_update_recipe_append(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ testrecipe = 'mdadm'
+ recipefile = get_bb_var('FILE', testrecipe)
+ src_uri = get_bb_var('SRC_URI', testrecipe)
+ self.assertNotIn('git://', src_uri, 'This test expects the %s recipe to NOT be a git recipe' % testrecipe)
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe)
+ # First, modify a recipe
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ tempsrcdir = os.path.join(tempdir, 'source')
+ templayerdir = os.path.join(tempdir, 'layer')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ # (don't bother with cleaning the recipe on teardown, we won't be building it)
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, tempsrcdir))
+ # Check git repo
+ self.assertTrue(os.path.isdir(os.path.join(tempsrcdir, '.git')), 'git repository for external source tree not found')
+ result = runCmd('git status --porcelain', cwd=tempsrcdir)
+ self.assertEqual(result.output.strip(), "", 'Created git repo is not clean')
+ result = runCmd('git symbolic-ref HEAD', cwd=tempsrcdir)
+ self.assertEqual(result.output.strip(), "refs/heads/devtool", 'Wrong branch in git repo')
+ # Add a commit
+ result = runCmd("sed 's!\\(#define VERSION\\W*\"[^\"]*\\)\"!\\1-custom\"!' -i ReadMe.c", cwd=tempsrcdir)
+ result = runCmd('git commit -a -m "Add our custom version"', cwd=tempsrcdir)
+ self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (os.path.dirname(recipefile), testrecipe))
+ # Create a temporary layer and add it to bblayers.conf
+ self._create_temp_layer(templayerdir, True, 'selftestupdaterecipe')
+ # Create the bbappend
+ result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
+ self.assertNotIn('WARNING:', result.output)
+ # Check recipe is still clean
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe)
+ # Check bbappend was created
+ splitpath = os.path.dirname(recipefile).split(os.sep)
+ appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1])
+ bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir)
+ patchfile = os.path.join(appenddir, testrecipe, '0001-Add-our-custom-version.patch')
+ self.assertTrue(os.path.exists(patchfile), 'Patch file not created')
+
+ # Check bbappend contents
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://0001-Add-our-custom-version.patch"\n',
+ '\n']
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines, f.readlines())
+
+ # Check we can run it again and bbappend isn't modified
+ result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines, f.readlines())
+ # Drop new commit and check patch gets deleted
+ result = runCmd('git reset HEAD^', cwd=tempsrcdir)
+ result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
+ self.assertFalse(os.path.exists(patchfile), 'Patch file not deleted')
+ expectedlines2 = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines2, f.readlines())
+ # Put commit back and check we can run it if layer isn't in bblayers.conf
+ os.remove(bbappendfile)
+ result = runCmd('git commit -a -m "Add our custom version"', cwd=tempsrcdir)
+ result = runCmd('bitbake-layers remove-layer %s' % templayerdir, cwd=self.builddir)
+ result = runCmd('devtool update-recipe %s -a %s' % (testrecipe, templayerdir))
+ self.assertIn('WARNING: Specified layer is not currently enabled in bblayers.conf', result.output)
+ self.assertTrue(os.path.exists(patchfile), 'Patch file not created (with disabled layer)')
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines, f.readlines())
+ # Deleting isn't expected to work under these circumstances
+
+ @testcase(1171)
+ def test_devtool_update_recipe_append_git(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ testrecipe = 'mtd-utils'
+ recipefile = get_bb_var('FILE', testrecipe)
+ src_uri = get_bb_var('SRC_URI', testrecipe)
+ self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe)
+ for entry in src_uri.split():
+ if entry.startswith('git://'):
+ git_uri = entry
+ break
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe)
+ # First, modify a recipe
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ tempsrcdir = os.path.join(tempdir, 'source')
+ templayerdir = os.path.join(tempdir, 'layer')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ # (don't bother with cleaning the recipe on teardown, we won't be building it)
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, tempsrcdir))
+ # Check git repo
+ self.assertTrue(os.path.isdir(os.path.join(tempsrcdir, '.git')), 'git repository for external source tree not found')
+ result = runCmd('git status --porcelain', cwd=tempsrcdir)
+ self.assertEqual(result.output.strip(), "", 'Created git repo is not clean')
+ result = runCmd('git symbolic-ref HEAD', cwd=tempsrcdir)
+ self.assertEqual(result.output.strip(), "refs/heads/devtool", 'Wrong branch in git repo')
+ # Add a commit
+ result = runCmd('echo "# Additional line" >> Makefile', cwd=tempsrcdir)
+ result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempsrcdir)
+ self.add_command_to_tearDown('cd %s; rm -f %s/*.patch; git checkout .' % (os.path.dirname(recipefile), testrecipe))
+ # Create a temporary layer
+ os.makedirs(os.path.join(templayerdir, 'conf'))
+ with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
+ f.write('BBPATH .= ":${LAYERDIR}"\n')
+ f.write('BBFILES += "${LAYERDIR}/recipes-*/*/*.bbappend"\n')
+ f.write('BBFILE_COLLECTIONS += "oeselftesttemplayer"\n')
+ f.write('BBFILE_PATTERN_oeselftesttemplayer = "^${LAYERDIR}/"\n')
+ f.write('BBFILE_PRIORITY_oeselftesttemplayer = "999"\n')
+ f.write('BBFILE_PATTERN_IGNORE_EMPTY_oeselftesttemplayer = "1"\n')
+ self.add_command_to_tearDown('bitbake-layers remove-layer %s || true' % templayerdir)
+ result = runCmd('bitbake-layers add-layer %s' % templayerdir, cwd=self.builddir)
+ # Create the bbappend
+ result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
+ self.assertNotIn('WARNING:', result.output)
+ # Check recipe is still clean
+ result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile))
+ self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe)
+ # Check bbappend was created
+ splitpath = os.path.dirname(recipefile).split(os.sep)
+ appenddir = os.path.join(templayerdir, splitpath[-2], splitpath[-1])
+ bbappendfile = self._check_bbappend(testrecipe, recipefile, appenddir)
+ self.assertFalse(os.path.exists(os.path.join(appenddir, testrecipe)), 'Patch directory should not be created')
+
+ # Check bbappend contents
+ result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
+ expectedlines = ['SRCREV = "%s"\n' % result.output,
+ '\n',
+ 'SRC_URI = "%s"\n' % git_uri,
+ '\n']
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines, f.readlines())
+
+ # Check we can run it again and bbappend isn't modified
+ result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines, f.readlines())
+ # Drop new commit and check SRCREV changes
+ result = runCmd('git reset HEAD^', cwd=tempsrcdir)
+ result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
+ self.assertFalse(os.path.exists(os.path.join(appenddir, testrecipe)), 'Patch directory should not be created')
+ result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
+ expectedlines = ['SRCREV = "%s"\n' % result.output,
+ '\n',
+ 'SRC_URI = "%s"\n' % git_uri,
+ '\n']
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines, f.readlines())
+ # Put commit back and check we can run it if layer isn't in bblayers.conf
+ os.remove(bbappendfile)
+ result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempsrcdir)
+ result = runCmd('bitbake-layers remove-layer %s' % templayerdir, cwd=self.builddir)
+ result = runCmd('devtool update-recipe -m srcrev %s -a %s' % (testrecipe, templayerdir))
+ self.assertIn('WARNING: Specified layer is not currently enabled in bblayers.conf', result.output)
+ self.assertFalse(os.path.exists(os.path.join(appenddir, testrecipe)), 'Patch directory should not be created')
+ result = runCmd('git rev-parse HEAD', cwd=tempsrcdir)
+ expectedlines = ['SRCREV = "%s"\n' % result.output,
+ '\n',
+ 'SRC_URI = "%s"\n' % git_uri,
+ '\n']
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines, f.readlines())
+ # Deleting isn't expected to work under these circumstances
+
+ @testcase(1163)
+ def test_devtool_extract(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ # Try devtool extract
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ result = runCmd('devtool extract remake %s' % tempdir)
+ self.assertTrue(os.path.exists(os.path.join(tempdir, 'Makefile.am')), 'Extracted source could not be found')
+ self.assertTrue(os.path.isdir(os.path.join(tempdir, '.git')), 'git repository for external source tree not found')
+
+ @testcase(1168)
+ def test_devtool_reset_all(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ testrecipe1 = 'mdadm'
+ testrecipe2 = 'cronie'
+ result = runCmd('devtool modify -x %s %s' % (testrecipe1, os.path.join(tempdir, testrecipe1)))
+ result = runCmd('devtool modify -x %s %s' % (testrecipe2, os.path.join(tempdir, testrecipe2)))
+ result = runCmd('devtool build %s' % testrecipe1)
+ result = runCmd('devtool build %s' % testrecipe2)
+ stampprefix1 = get_bb_var('STAMP', testrecipe1)
+ self.assertTrue(stampprefix1, 'Unable to get STAMP value for recipe %s' % testrecipe1)
+ stampprefix2 = get_bb_var('STAMP', testrecipe2)
+ self.assertTrue(stampprefix2, 'Unable to get STAMP value for recipe %s' % testrecipe2)
+ result = runCmd('devtool reset -a')
+ self.assertIn(testrecipe1, result.output)
+ self.assertIn(testrecipe2, result.output)
+ result = runCmd('devtool status')
+ self.assertNotIn(testrecipe1, result.output)
+ self.assertNotIn(testrecipe2, result.output)
+ matches1 = glob.glob(stampprefix1 + '*')
+ self.assertFalse(matches1, 'Stamp files exist for recipe %s that should have been cleaned' % testrecipe1)
+ matches2 = glob.glob(stampprefix2 + '*')
+ self.assertFalse(matches2, 'Stamp files exist for recipe %s that should have been cleaned' % testrecipe2)
+
+ def test_devtool_deploy_target(self):
+ # NOTE: Whilst this test would seemingly be better placed as a runtime test,
+ # unfortunately the runtime tests run under bitbake and you can't run
+ # devtool within bitbake (since devtool needs to run bitbake itself).
+ # Additionally we are testing build-time functionality as well, so
+ # really this has to be done as an oe-selftest test.
+ #
+ # Check preconditions
+ machine = get_bb_var('MACHINE')
+ if not machine.startswith('qemu'):
+ self.skipTest('This test only works with qemu machines')
+ if not os.path.exists('/etc/runqemu-nosudo'):
+ self.skipTest('You must set up tap devices with scripts/runqemu-gen-tapdevs before running this test')
+ result = runCmd('PATH="$PATH:/sbin:/usr/sbin" ip tuntap show', ignore_status=True)
+ if result.status != 0:
+ result = runCmd('PATH="$PATH:/sbin:/usr/sbin" ifconfig -a', ignore_status=True)
+ if result.status != 0:
+ self.skipTest('Failed to determine if tap devices exist with ifconfig or ip: %s' % result.output)
+ for line in result.output.splitlines():
+ if line.startswith('tap'):
+ break
+ else:
+ self.skipTest('No tap devices found - you must set up tap devices with scripts/runqemu-gen-tapdevs before running this test')
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # Definitions
+ testrecipe = 'mdadm'
+ testfile = '/sbin/mdadm'
+ testimage = 'oe-selftest-image'
+ testcommand = '/sbin/mdadm --help'
+ # Build an image to run
+ bitbake("%s qemu-native qemu-helper-native" % testimage)
+ deploy_dir_image = get_bb_var('DEPLOY_DIR_IMAGE')
+ self.add_command_to_tearDown('bitbake -c clean %s' % testimage)
+ self.add_command_to_tearDown('rm -f %s/%s*' % (deploy_dir_image, testimage))
+ # Clean recipe so the first deploy will fail
+ bitbake("%s -c clean" % testrecipe)
+ # Try devtool modify
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ self.add_command_to_tearDown('bitbake -c clean %s' % testrecipe)
+ result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir))
+ # Test that deploy-target at this point fails (properly)
+ result = runCmd('devtool deploy-target -n %s root@localhost' % testrecipe, ignore_status=True)
+ self.assertNotEqual(result.output, 0, 'devtool deploy-target should have failed, output: %s' % result.output)
+ self.assertNotIn(result.output, 'Traceback', 'devtool deploy-target should have failed with a proper error not a traceback, output: %s' % result.output)
+ result = runCmd('devtool build %s' % testrecipe)
+ # First try a dry-run of deploy-target
+ result = runCmd('devtool deploy-target -n %s root@localhost' % testrecipe)
+ self.assertIn(' %s' % testfile, result.output)
+ # Boot the image
+ with runqemu(testimage, self) as qemu:
+ # Now really test deploy-target
+ result = runCmd('devtool deploy-target -c %s root@%s' % (testrecipe, qemu.ip))
+ # Run a test command to see if it was installed properly
+ sshargs = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
+ result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, testcommand))
+ # Check if it deployed all of the files with the right ownership/perms
+ # First look on the host - need to do this under pseudo to get the correct ownership/perms
+ installdir = get_bb_var('D', testrecipe)
+ fakerootenv = get_bb_var('FAKEROOTENV', testrecipe)
+ fakerootcmd = get_bb_var('FAKEROOTCMD', testrecipe)
+ result = runCmd('%s %s find . -type f -exec ls -l {} \;' % (fakerootenv, fakerootcmd), cwd=installdir)
+ filelist1 = self._process_ls_output(result.output)
+
+ # Now look on the target
+ tempdir2 = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir2)
+ tmpfilelist = os.path.join(tempdir2, 'files.txt')
+ with open(tmpfilelist, 'w') as f:
+ for line in filelist1:
+ splitline = line.split()
+ f.write(splitline[-1] + '\n')
+ result = runCmd('cat %s | ssh -q %s root@%s \'xargs ls -l\'' % (tmpfilelist, sshargs, qemu.ip))
+ filelist2 = self._process_ls_output(result.output)
+ filelist1.sort(key=lambda item: item.split()[-1])
+ filelist2.sort(key=lambda item: item.split()[-1])
+ self.assertEqual(filelist1, filelist2)
+ # Test undeploy-target
+ result = runCmd('devtool undeploy-target -c %s root@%s' % (testrecipe, qemu.ip))
+ result = runCmd('ssh %s root@%s %s' % (sshargs, qemu.ip, testcommand), ignore_status=True)
+ self.assertNotEqual(result, 0, 'undeploy-target did not remove command as it should have')
+
+ def test_devtool_build_image(self):
+ """Test devtool build-image plugin"""
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ image = 'core-image-minimal'
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
+ self.add_command_to_tearDown('bitbake -c clean %s' % image)
+ bitbake('%s -c clean' % image)
+ # Add target and native recipes to workspace
+ for recipe in ('mdadm', 'parted-native'):
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ self.track_for_cleanup(tempdir)
+ self.add_command_to_tearDown('bitbake -c clean %s' % recipe)
+ runCmd('devtool modify %s -x %s' % (recipe, tempdir))
+ # Try to build image
+ result = runCmd('devtool build-image %s' % image)
+ self.assertNotEqual(result, 0, 'devtool build-image failed')
+ # Check if image.bbappend has required content
+ bbappend = os.path.join(workspacedir, 'appends', image+'.bbappend')
+ self.assertTrue(os.path.isfile(bbappend), 'bbappend not created %s' % result.output)
+ # NOTE: native recipe parted-native should not be in IMAGE_INSTALL_append
+ self.assertTrue('IMAGE_INSTALL_append = " mdadm"\n' in open(bbappend).readlines(),
+ 'IMAGE_INSTALL_append = " mdadm" not found in %s' % bbappend)
+
+ def test_devtool_upgrade(self):
+ # Check preconditions
+ workspacedir = os.path.join(self.builddir, 'workspace')
+ self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory')
+ # Check parameters
+ result = runCmd('devtool upgrade -h')
+ for param in 'recipename srctree --version -V --branch -b --keep-temp --no-patch'.split():
+ self.assertIn(param, result.output)
+ # For the moment, we are using a real recipe.
+ recipe='devtool-upgrade'
+ version='0.2'
+ tempdir = tempfile.mkdtemp(prefix='devtoolqa')
+ # Check that recipe is not already under devtool control
+ result = runCmd('devtool status')
+ self.assertNotIn(recipe, result.output)
+ # Check upgrade. Code does not check if new PV is older or newer that current PV, so, it may be that
+ # we are downgrading instead of upgrading.
+ result = runCmd('devtool upgrade %s %s -V %s' % (recipe, tempdir, version))
+ # Check if srctree at least is populated
+ self.assertTrue(len(os.listdir(tempdir)) > 0, 'scrtree (%s) should be populated with new (%s) source code' % (tempdir, version))
+ # Check new recipe folder is present
+ self.assertTrue(os.path.exists(os.path.join(workspacedir,'recipes',recipe)), 'Recipe folder should exist')
+ # Check new recipe file is present
+ self.assertTrue(os.path.exists(os.path.join(workspacedir,'recipes',recipe,"%s_%s.bb" % (recipe,version))), 'Recipe folder should exist')
+ # Check devtool status and make sure recipe is present
+ result = runCmd('devtool status')
+ self.assertIn(recipe, result.output)
+ self.assertIn(tempdir, result.output)
+ # Check devtool reset recipe
+ result = runCmd('devtool reset %s -n' % recipe)
+ result = runCmd('devtool status')
+ self.assertNotIn(recipe, result.output)
+ self.track_for_cleanup(tempdir)
+ self.track_for_cleanup(workspacedir)
+ self.add_command_to_tearDown('bitbake-layers remove-layer */workspace')
diff --git a/meta/lib/oeqa/selftest/imagefeatures.py b/meta/lib/oeqa/selftest/imagefeatures.py
new file mode 100644
index 0000000..fcffc42
--- /dev/null
+++ b/meta/lib/oeqa/selftest/imagefeatures.py
@@ -0,0 +1,168 @@
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var, runqemu
+from oeqa.utils.decorators import testcase
+from oeqa.utils.sshcontrol import SSHControl
+import os
+import sys
+import logging
+
+class ImageFeatures(oeSelfTest):
+
+ test_user = 'tester'
+ root_user = 'root'
+
+ @testcase(1107)
+ def test_non_root_user_can_connect_via_ssh_without_password(self):
+ """
+ Summary: Check if non root user can connect via ssh without password
+ Expected: 1. Connection to the image via ssh using root user without providing a password should be allowed.
+ 2. Connection to the image via ssh using tester user without providing a password should be allowed.
+ Product: oe-core
+ Author: Ionut Chisanovici <ionutx.chisanovici@intel.com>
+ AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com>
+ """
+
+ features = 'EXTRA_IMAGE_FEATURES = "ssh-server-openssh empty-root-password allow-empty-password"\n'
+ features += 'INHERIT += "extrausers"\n'
+ features += 'EXTRA_USERS_PARAMS = "useradd -p \'\' {}; usermod -s /bin/sh {};"'.format(self.test_user, self.test_user)
+
+ # Append 'features' to local.conf
+ self.append_config(features)
+
+ # Build a core-image-minimal
+ bitbake('core-image-minimal')
+
+ with runqemu("core-image-minimal", self) as qemu:
+ # Attempt to ssh with each user into qemu with empty password
+ for user in [self.root_user, self.test_user]:
+ ssh = SSHControl(ip=qemu.ip, logfile=qemu.sshlog, user=user)
+ status, output = ssh.run("true")
+ self.assertEqual(status, 0, 'ssh to user %s failed with %s' % (user, output))
+
+ @testcase(1115)
+ def test_all_users_can_connect_via_ssh_without_password(self):
+ """
+ Summary: Check if all users can connect via ssh without password
+ Expected: 1. Connection to the image via ssh using root user without providing a password should NOT be allowed.
+ 2. Connection to the image via ssh using tester user without providing a password should be allowed.
+ Product: oe-core
+ Author: Ionut Chisanovici <ionutx.chisanovici@intel.com>
+ AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com>
+ """
+
+ features = 'EXTRA_IMAGE_FEATURES = "ssh-server-openssh allow-empty-password"\n'
+ features += 'INHERIT += "extrausers"\n'
+ features += 'EXTRA_USERS_PARAMS = "useradd -p \'\' {}; usermod -s /bin/sh {};"'.format(self.test_user, self.test_user)
+
+ # Append 'features' to local.conf
+ self.append_config(features)
+
+ # Build a core-image-minimal
+ bitbake('core-image-minimal')
+
+ with runqemu("core-image-minimal", self) as qemu:
+ # Attempt to ssh with each user into qemu with empty password
+ for user in [self.root_user, self.test_user]:
+ ssh = SSHControl(ip=qemu.ip, logfile=qemu.sshlog, user=user)
+ status, output = ssh.run("true")
+ if user == 'root':
+ self.assertNotEqual(status, 0, 'ssh to user root was allowed when it should not have been')
+ else:
+ self.assertEqual(status, 0, 'ssh to user tester failed with %s' % output)
+
+
+ @testcase(1114)
+ def test_rpm_version_4_support_on_image(self):
+ """
+ Summary: Check rpm version 4 support on image
+ Expected: Rpm version must be 4.x
+ Product: oe-core
+ Author: Ionut Chisanovici <ionutx.chisanovici@intel.com>
+ AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com>
+ """
+
+ features = 'PREFERRED_VERSION_rpm = "4.%"\n'
+ features += 'PREFERRED_VERSION_rpm-native = "4.%"\n'
+ # Use openssh in IMAGE_INSTALL instead of ssh-server-openssh in EXTRA_IMAGE_FEATURES as a workaround for bug 8047
+ features += 'IMAGE_INSTALL_append = " openssh"\n'
+ features += 'EXTRA_IMAGE_FEATURES = "empty-root-password allow-empty-password package-management"\n'
+ features += 'RPMROOTFSDEPENDS_remove = "rpmresolve-native:do_populate_sysroot"'
+
+ # Append 'features' to local.conf
+ self.append_config(features)
+
+ # Build a core-image-minimal
+ bitbake('core-image-minimal')
+
+ # Check the native version of rpm is correct
+ native_bindir = get_bb_var('STAGING_BINDIR_NATIVE')
+ result = runCmd(os.path.join(native_bindir, 'rpm') + ' --version')
+ self.assertIn('version 4.', result.output)
+
+ # Check manifest for the rpm package
+ deploydir = get_bb_var('DEPLOY_DIR_IMAGE')
+ imgname = get_bb_var('IMAGE_LINK_NAME', 'core-image-minimal')
+ with open(os.path.join(deploydir, imgname) + '.manifest', 'r') as f:
+ for line in f:
+ splitline = line.split()
+ if len(splitline) > 2:
+ rpm_version = splitline[2]
+ if splitline[0] == 'rpm':
+ if not rpm_version.startswith('4.'):
+ self.fail('rpm version %s found in image, expected 4.x' % rpm_version)
+ break
+ else:
+ self.fail('No rpm package found in image')
+
+ # Now do a couple of runtime tests
+ with runqemu("core-image-minimal", self) as qemu:
+ command = "rpm --version"
+ status, output = qemu.run(command)
+ self.assertEqual(0, status, 'Failed to run command "%s": %s' % (command, output))
+ found_rpm_version = output.strip()
+
+ # Make sure the retrieved rpm version is the expected one
+ if rpm_version not in found_rpm_version:
+ self.fail('RPM version is not {}, found instead {}.'.format(rpm_version, found_rpm_version))
+
+ # Test that the rpm database is there and working
+ command = "rpm -qa"
+ status, output = qemu.run(command)
+ self.assertEqual(0, status, 'Failed to run command "%s": %s' % (command, output))
+ self.assertIn('packagegroup-core-boot', output)
+ self.assertIn('busybox', output)
+
+
+ @testcase(1116)
+ def test_clutter_image_can_be_built(self):
+ """
+ Summary: Check if clutter image can be built
+ Expected: 1. core-image-clutter can be built
+ Product: oe-core
+ Author: Ionut Chisanovici <ionutx.chisanovici@intel.com>
+ AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com>
+ """
+
+ # Build a core-image-clutter
+ bitbake('core-image-clutter')
+
+ @testcase(1117)
+ def test_wayland_support_in_image(self):
+ """
+ Summary: Check Wayland support in image
+ Expected: 1. Wayland image can be build
+ 2. Wayland feature can be installed
+ Product: oe-core
+ Author: Ionut Chisanovici <ionutx.chisanovici@intel.com>
+ AutomatedBy: Daniel Istrate <daniel.alexandrux.istrate@intel.com>
+ """
+
+ features = 'DISTRO_FEATURES_append = " wayland"\n'
+ features += 'CORE_IMAGE_EXTRA_INSTALL += "wayland weston"'
+
+ # Append 'features' to local.conf
+ self.append_config(features)
+
+ # Build a core-image-weston
+ bitbake('core-image-weston')
+
diff --git a/meta/lib/oeqa/selftest/layerappend.py b/meta/lib/oeqa/selftest/layerappend.py
new file mode 100644
index 0000000..a82a6c8
--- /dev/null
+++ b/meta/lib/oeqa/selftest/layerappend.py
@@ -0,0 +1,96 @@
+import unittest
+import os
+import logging
+import re
+
+from oeqa.selftest.base import oeSelfTest
+from oeqa.selftest.buildhistory import BuildhistoryBase
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var
+import oeqa.utils.ftools as ftools
+from oeqa.utils.decorators import testcase
+
+class LayerAppendTests(oeSelfTest):
+ layerconf = """
+# We have a conf and classes directory, append to BBPATH
+BBPATH .= ":${LAYERDIR}"
+
+# We have a recipes directory, add to BBFILES
+BBFILES += "${LAYERDIR}/recipes*/*.bb ${LAYERDIR}/recipes*/*.bbappend"
+
+BBFILE_COLLECTIONS += "meta-layerINT"
+BBFILE_PATTERN_meta-layerINT := "^${LAYERDIR}/"
+BBFILE_PRIORITY_meta-layerINT = "6"
+"""
+ recipe = """
+LICENSE="CLOSED"
+INHIBIT_DEFAULT_DEPS = "1"
+
+python do_build() {
+ bb.plain('Building ...')
+}
+addtask build
+"""
+ append = """
+FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"
+
+SRC_URI_append = " file://appendtest.txt"
+
+sysroot_stage_all_append() {
+ install -m 644 ${WORKDIR}/appendtest.txt ${SYSROOT_DESTDIR}/
+}
+
+"""
+
+ append2 = """
+FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"
+
+SRC_URI_append += "file://appendtest.txt"
+"""
+ layerappend = "BBLAYERS += \"COREBASE/meta-layertest0 COREBASE/meta-layertest1 COREBASE/meta-layertest2\""
+
+ def tearDownLocal(self):
+ ftools.remove_from_file(self.builddir + "/conf/bblayers.conf", self.layerappend.replace("COREBASE", self.builddir + "/.."))
+
+ @testcase(1196)
+ def test_layer_appends(self):
+ corebase = get_bb_var("COREBASE")
+ stagingdir = get_bb_var("STAGING_DIR_TARGET")
+ for l in ["0", "1", "2"]:
+ layer = os.path.join(corebase, "meta-layertest" + l)
+ self.assertFalse(os.path.exists(layer))
+ os.mkdir(layer)
+ os.mkdir(layer + "/conf")
+ with open(layer + "/conf/layer.conf", "w") as f:
+ f.write(self.layerconf.replace("INT", l))
+ os.mkdir(layer + "/recipes-test")
+ if l == "0":
+ with open(layer + "/recipes-test/layerappendtest.bb", "w") as f:
+ f.write(self.recipe)
+ elif l == "1":
+ with open(layer + "/recipes-test/layerappendtest.bbappend", "w") as f:
+ f.write(self.append)
+ os.mkdir(layer + "/recipes-test/layerappendtest")
+ with open(layer + "/recipes-test/layerappendtest/appendtest.txt", "w") as f:
+ f.write("Layer 1 test")
+ elif l == "2":
+ with open(layer + "/recipes-test/layerappendtest.bbappend", "w") as f:
+ f.write(self.append2)
+ os.mkdir(layer + "/recipes-test/layerappendtest")
+ with open(layer + "/recipes-test/layerappendtest/appendtest.txt", "w") as f:
+ f.write("Layer 2 test")
+ self.track_for_cleanup(layer)
+ ftools.append_file(self.builddir + "/conf/bblayers.conf", self.layerappend.replace("COREBASE", self.builddir + "/.."))
+ bitbake("layerappendtest")
+ data = ftools.read_file(stagingdir + "/appendtest.txt")
+ self.assertEqual(data, "Layer 2 test")
+ os.remove(corebase + "/meta-layertest2/recipes-test/layerappendtest/appendtest.txt")
+ bitbake("layerappendtest")
+ data = ftools.read_file(stagingdir + "/appendtest.txt")
+ self.assertEqual(data, "Layer 1 test")
+ with open(corebase + "/meta-layertest2/recipes-test/layerappendtest/appendtest.txt", "w") as f:
+ f.write("Layer 2 test")
+ bitbake("layerappendtest")
+ data = ftools.read_file(stagingdir + "/appendtest.txt")
+ self.assertEqual(data, "Layer 2 test")
+
+
diff --git a/meta/lib/oeqa/selftest/lic-checksum.py b/meta/lib/oeqa/selftest/lic-checksum.py
new file mode 100644
index 0000000..bd3b9a1
--- /dev/null
+++ b/meta/lib/oeqa/selftest/lic-checksum.py
@@ -0,0 +1,31 @@
+import os
+import tempfile
+
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import bitbake
+from oeqa.utils import CommandError
+from oeqa.utils.decorators import testcase
+
+class LicenseTests(oeSelfTest):
+
+ # Verify that changing a license file that has an absolute path causes
+ # the license qa to fail due to a mismatched md5sum.
+ @testcase(1197)
+ def test_nonmatching_checksum(self):
+ bitbake_cmd = '-c configure emptytest'
+ error_msg = 'ERROR: emptytest: The new md5 checksum is 8d777f385d3dfec8815d20f7496026dc'
+
+ lic_file, lic_path = tempfile.mkstemp()
+ os.close(lic_file)
+ self.track_for_cleanup(lic_path)
+
+ self.write_recipeinc('emptytest', 'INHIBIT_DEFAULT_DEPS = "1"')
+ self.append_recipeinc('emptytest', 'LIC_FILES_CHKSUM = "file://%s;md5=d41d8cd98f00b204e9800998ecf8427e"' % lic_path)
+ result = bitbake(bitbake_cmd)
+
+ with open(lic_path, "w") as f:
+ f.write("data")
+
+ result = bitbake(bitbake_cmd, ignore_status=True)
+ if error_msg not in result.output:
+ raise AssertionError(result.output)
diff --git a/meta/lib/oeqa/selftest/oescripts.py b/meta/lib/oeqa/selftest/oescripts.py
new file mode 100644
index 0000000..31cd508
--- /dev/null
+++ b/meta/lib/oeqa/selftest/oescripts.py
@@ -0,0 +1,54 @@
+import datetime
+import unittest
+import os
+import re
+import shutil
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.selftest.buildhistory import BuildhistoryBase
+from oeqa.utils.commands import Command, runCmd, bitbake, get_bb_var, get_test_layer
+from oeqa.utils.decorators import testcase
+
+class TestScripts(oeSelfTest):
+
+ @testcase(300)
+ def test_cleanup_workdir(self):
+ path = os.path.dirname(get_bb_var('WORKDIR', 'gzip'))
+ old_version_recipe = os.path.join(get_bb_var('COREBASE'), 'meta/recipes-extended/gzip/gzip_1.3.12.bb')
+ old_version = '1.3.12'
+ bitbake("-ccleansstate gzip")
+ bitbake("-ccleansstate -b %s" % old_version_recipe)
+ if os.path.exists(get_bb_var('WORKDIR', "-b %s" % old_version_recipe)):
+ shutil.rmtree(get_bb_var('WORKDIR', "-b %s" % old_version_recipe))
+ if os.path.exists(get_bb_var('WORKDIR', 'gzip')):
+ shutil.rmtree(get_bb_var('WORKDIR', 'gzip'))
+
+ if os.path.exists(path):
+ initial_contents = os.listdir(path)
+ else:
+ initial_contents = []
+
+ bitbake('gzip')
+ intermediary_contents = os.listdir(path)
+ bitbake("-b %s" % old_version_recipe)
+ runCmd('cleanup-workdir')
+ remaining_contents = os.listdir(path)
+
+ expected_contents = [x for x in intermediary_contents if x not in initial_contents]
+ remaining_not_expected = [x for x in remaining_contents if x not in expected_contents]
+ self.assertFalse(remaining_not_expected, msg="Not all necessary content has been deleted from %s: %s" % (path, ', '.join(map(str, remaining_not_expected))))
+ expected_not_remaining = [x for x in expected_contents if x not in remaining_contents]
+ self.assertFalse(expected_not_remaining, msg="The script removed extra contents from %s: %s" % (path, ', '.join(map(str, expected_not_remaining))))
+
+class BuildhistoryDiffTests(BuildhistoryBase):
+
+ @testcase(295)
+ def test_buildhistory_diff(self):
+ self.add_command_to_tearDown('cleanup-workdir')
+ target = 'xcursor-transparent-theme'
+ self.run_buildhistory_operation(target, target_config="PR = \"r1\"", change_bh_location=True)
+ self.run_buildhistory_operation(target, target_config="PR = \"r0\"", change_bh_location=False, expect_error=True)
+ result = runCmd("buildhistory-diff -p %s" % get_bb_var('BUILDHISTORY_DIR'))
+ expected_output = 'PR changed from "r1" to "r0"'
+ self.assertTrue(expected_output in result.output, msg="Did not find expected output: %s" % result.output)
diff --git a/meta/lib/oeqa/selftest/pkgdata.py b/meta/lib/oeqa/selftest/pkgdata.py
new file mode 100644
index 0000000..138b03a
--- /dev/null
+++ b/meta/lib/oeqa/selftest/pkgdata.py
@@ -0,0 +1,226 @@
+import unittest
+import os
+import tempfile
+import logging
+import fnmatch
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var
+from oeqa.utils.decorators import testcase
+
+class OePkgdataUtilTests(oeSelfTest):
+
+ @classmethod
+ def setUpClass(cls):
+ # Ensure we have the right data in pkgdata
+ logger = logging.getLogger("selftest")
+ logger.info('Running bitbake to generate pkgdata')
+ bitbake('glibc busybox zlib bash')
+
+ @testcase(1203)
+ def test_lookup_pkg(self):
+ # Forward tests
+ result = runCmd('oe-pkgdata-util lookup-pkg "glibc busybox"')
+ self.assertEqual(result.output, 'libc6\nbusybox')
+ result = runCmd('oe-pkgdata-util lookup-pkg zlib-dev')
+ self.assertEqual(result.output, 'libz-dev')
+ result = runCmd('oe-pkgdata-util lookup-pkg nonexistentpkg', ignore_status=True)
+ self.assertEqual(result.status, 1, "Status different than 1. output: %s" % result.output)
+ self.assertEqual(result.output, 'ERROR: The following packages could not be found: nonexistentpkg')
+ # Reverse tests
+ result = runCmd('oe-pkgdata-util lookup-pkg -r "libc6 busybox"')
+ self.assertEqual(result.output, 'glibc\nbusybox')
+ result = runCmd('oe-pkgdata-util lookup-pkg -r libz-dev')
+ self.assertEqual(result.output, 'zlib-dev')
+ result = runCmd('oe-pkgdata-util lookup-pkg -r nonexistentpkg', ignore_status=True)
+ self.assertEqual(result.status, 1, "Status different than 1. output: %s" % result.output)
+ self.assertEqual(result.output, 'ERROR: The following packages could not be found: nonexistentpkg')
+
+ @testcase(1205)
+ def test_read_value(self):
+ result = runCmd('oe-pkgdata-util read-value PN libz1')
+ self.assertEqual(result.output, 'zlib')
+ result = runCmd('oe-pkgdata-util read-value PKGSIZE bash')
+ pkgsize = int(result.output.strip())
+ self.assertGreater(pkgsize, 1, "Size should be greater than 1. %s" % result.output)
+
+ @testcase(1198)
+ def test_find_path(self):
+ result = runCmd('oe-pkgdata-util find-path /lib/libc.so.6')
+ self.assertEqual(result.output, 'glibc: /lib/libc.so.6')
+ result = runCmd('oe-pkgdata-util find-path /bin/bash')
+ self.assertEqual(result.output, 'bash: /bin/bash')
+ result = runCmd('oe-pkgdata-util find-path /not/exist', ignore_status=True)
+ self.assertEqual(result.status, 1, "Status different than 1. output: %s" % result.output)
+ self.assertEqual(result.output, 'ERROR: Unable to find any package producing path /not/exist')
+
+ @testcase(1204)
+ def test_lookup_recipe(self):
+ result = runCmd('oe-pkgdata-util lookup-recipe "libc6-staticdev busybox"')
+ self.assertEqual(result.output, 'glibc\nbusybox')
+ result = runCmd('oe-pkgdata-util lookup-recipe libz-dbg')
+ self.assertEqual(result.output, 'zlib')
+ result = runCmd('oe-pkgdata-util lookup-recipe nonexistentpkg', ignore_status=True)
+ self.assertEqual(result.status, 1, "Status different than 1. output: %s" % result.output)
+ self.assertEqual(result.output, 'ERROR: The following packages could not be found: nonexistentpkg')
+
+ @testcase(1202)
+ def test_list_pkgs(self):
+ # No arguments
+ result = runCmd('oe-pkgdata-util list-pkgs')
+ pkglist = result.output.split()
+ self.assertIn('glibc-utils', pkglist, "Listed packages: %s" % result.output)
+ self.assertIn('zlib-dev', pkglist, "Listed packages: %s" % result.output)
+ # No pkgspec, runtime
+ result = runCmd('oe-pkgdata-util list-pkgs -r')
+ pkglist = result.output.split()
+ self.assertIn('libc6-utils', pkglist, "Listed packages: %s" % result.output)
+ self.assertIn('libz-dev', pkglist, "Listed packages: %s" % result.output)
+ # With recipe specified
+ result = runCmd('oe-pkgdata-util list-pkgs -p zlib')
+ pkglist = sorted(result.output.split())
+ try:
+ pkglist.remove('zlib-ptest') # in case ptest is disabled
+ except ValueError:
+ pass
+ self.assertEqual(pkglist, ['zlib', 'zlib-dbg', 'zlib-dev', 'zlib-doc', 'zlib-staticdev'], "Packages listed after remove: %s" % result.output)
+ # With recipe specified, runtime
+ result = runCmd('oe-pkgdata-util list-pkgs -p zlib -r')
+ pkglist = sorted(result.output.split())
+ try:
+ pkglist.remove('libz-ptest') # in case ptest is disabled
+ except ValueError:
+ pass
+ self.assertEqual(pkglist, ['libz-dbg', 'libz-dev', 'libz-doc', 'libz-staticdev', 'libz1'], "Packages listed after remove: %s" % result.output)
+ # With recipe specified and unpackaged
+ result = runCmd('oe-pkgdata-util list-pkgs -p zlib -u')
+ pkglist = sorted(result.output.split())
+ self.assertIn('zlib-locale', pkglist, "Listed packages: %s" % result.output)
+ # With recipe specified and unpackaged, runtime
+ result = runCmd('oe-pkgdata-util list-pkgs -p zlib -u -r')
+ pkglist = sorted(result.output.split())
+ self.assertIn('libz-locale', pkglist, "Listed packages: %s" % result.output)
+ # With recipe specified and pkgspec
+ result = runCmd('oe-pkgdata-util list-pkgs -p zlib "*-d*"')
+ pkglist = sorted(result.output.split())
+ self.assertEqual(pkglist, ['zlib-dbg', 'zlib-dev', 'zlib-doc'], "Packages listed: %s" % result.output)
+ # With recipe specified and pkgspec, runtime
+ result = runCmd('oe-pkgdata-util list-pkgs -p zlib -r "*-d*"')
+ pkglist = sorted(result.output.split())
+ self.assertEqual(pkglist, ['libz-dbg', 'libz-dev', 'libz-doc'], "Packages listed: %s" % result.output)
+
+ @testcase(1201)
+ def test_list_pkg_files(self):
+ def splitoutput(output):
+ files = {}
+ curpkg = None
+ for line in output.splitlines():
+ if line.startswith('\t'):
+ self.assertTrue(curpkg, 'Unexpected non-package line:\n%s' % line)
+ files[curpkg].append(line.strip())
+ else:
+ self.assertTrue(line.rstrip().endswith(':'), 'Invalid package line in output:\n%s' % line)
+ curpkg = line.split(':')[0]
+ files[curpkg] = []
+ return files
+ base_libdir = get_bb_var('base_libdir')
+ libdir = get_bb_var('libdir')
+ includedir = get_bb_var('includedir')
+ mandir = get_bb_var('mandir')
+ # Test recipe-space package name
+ result = runCmd('oe-pkgdata-util list-pkg-files zlib-dev zlib-doc')
+ files = splitoutput(result.output)
+ self.assertIn('zlib-dev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib-doc', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn(os.path.join(includedir, 'zlib.h'), files['zlib-dev'])
+ self.assertIn(os.path.join(mandir, 'man3/zlib.3'), files['zlib-doc'])
+ # Test runtime package name
+ result = runCmd('oe-pkgdata-util list-pkg-files -r libz1 libz-dev')
+ files = splitoutput(result.output)
+ self.assertIn('libz1', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz-dev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertGreater(len(files['libz1']), 1)
+ libspec = os.path.join(base_libdir, 'libz.so.1.*')
+ found = False
+ for fileitem in files['libz1']:
+ if fnmatch.fnmatchcase(fileitem, libspec):
+ found = True
+ break
+ self.assertTrue(found, 'Could not find zlib library file %s in libz1 package file list: %s' % (libspec, files['libz1']))
+ self.assertIn(os.path.join(includedir, 'zlib.h'), files['libz-dev'])
+ # Test recipe
+ result = runCmd('oe-pkgdata-util list-pkg-files -p zlib')
+ files = splitoutput(result.output)
+ self.assertIn('zlib-dbg', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib-doc', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib-dev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib-staticdev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertNotIn('zlib-locale', files.keys(), "listed pkgs. files: %s" %result.output)
+ # (ignore ptest, might not be there depending on config)
+ self.assertIn(os.path.join(includedir, 'zlib.h'), files['zlib-dev'])
+ self.assertIn(os.path.join(mandir, 'man3/zlib.3'), files['zlib-doc'])
+ self.assertIn(os.path.join(libdir, 'libz.a'), files['zlib-staticdev'])
+ # Test recipe, runtime
+ result = runCmd('oe-pkgdata-util list-pkg-files -p zlib -r')
+ files = splitoutput(result.output)
+ self.assertIn('libz-dbg', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz-doc', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz-dev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz-staticdev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz1', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertNotIn('libz-locale', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn(os.path.join(includedir, 'zlib.h'), files['libz-dev'])
+ self.assertIn(os.path.join(mandir, 'man3/zlib.3'), files['libz-doc'])
+ self.assertIn(os.path.join(libdir, 'libz.a'), files['libz-staticdev'])
+ # Test recipe, unpackaged
+ result = runCmd('oe-pkgdata-util list-pkg-files -p zlib -u')
+ files = splitoutput(result.output)
+ self.assertIn('zlib-dbg', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib-doc', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib-dev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib-staticdev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('zlib-locale', files.keys(), "listed pkgs. files: %s" %result.output) # this is the key one
+ self.assertIn(os.path.join(includedir, 'zlib.h'), files['zlib-dev'])
+ self.assertIn(os.path.join(mandir, 'man3/zlib.3'), files['zlib-doc'])
+ self.assertIn(os.path.join(libdir, 'libz.a'), files['zlib-staticdev'])
+ # Test recipe, runtime, unpackaged
+ result = runCmd('oe-pkgdata-util list-pkg-files -p zlib -r -u')
+ files = splitoutput(result.output)
+ self.assertIn('libz-dbg', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz-doc', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz-dev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz-staticdev', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz1', files.keys(), "listed pkgs. files: %s" %result.output)
+ self.assertIn('libz-locale', files.keys(), "listed pkgs. files: %s" %result.output) # this is the key one
+ self.assertIn(os.path.join(includedir, 'zlib.h'), files['libz-dev'])
+ self.assertIn(os.path.join(mandir, 'man3/zlib.3'), files['libz-doc'])
+ self.assertIn(os.path.join(libdir, 'libz.a'), files['libz-staticdev'])
+
+ @testcase(1200)
+ def test_glob(self):
+ tempdir = tempfile.mkdtemp(prefix='pkgdataqa')
+ self.track_for_cleanup(tempdir)
+ pkglistfile = os.path.join(tempdir, 'pkglist')
+ with open(pkglistfile, 'w') as f:
+ f.write('libc6\n')
+ f.write('libz1\n')
+ f.write('busybox\n')
+ result = runCmd('oe-pkgdata-util glob %s "*-dev"' % pkglistfile)
+ desiredresult = ['libc6-dev', 'libz-dev', 'busybox-dev']
+ self.assertEqual(sorted(result.output.split()), sorted(desiredresult))
+ # The following should not error (because when we use this during rootfs construction, sometimes the complementary package won't exist)
+ result = runCmd('oe-pkgdata-util glob %s "*-nonexistent"' % pkglistfile)
+ self.assertEqual(result.output, '')
+ # Test exclude option
+ result = runCmd('oe-pkgdata-util glob %s "*-dev *-dbg" -x "^libz"' % pkglistfile)
+ resultlist = result.output.split()
+ self.assertNotIn('libz-dev', resultlist)
+ self.assertNotIn('libz-dbg', resultlist)
+
+ @testcase(1206)
+ def test_specify_pkgdatadir(self):
+ result = runCmd('oe-pkgdata-util -p %s lookup-pkg glibc' % get_bb_var('PKGDATA_DIR'))
+ self.assertEqual(result.output, 'libc6')
diff --git a/meta/lib/oeqa/selftest/prservice.py b/meta/lib/oeqa/selftest/prservice.py
new file mode 100644
index 0000000..4187fbf
--- /dev/null
+++ b/meta/lib/oeqa/selftest/prservice.py
@@ -0,0 +1,121 @@
+import unittest
+import os
+import logging
+import re
+import shutil
+import datetime
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var
+from oeqa.utils.decorators import testcase
+
+class BitbakePrTests(oeSelfTest):
+
+ def get_pr_version(self, package_name):
+ pkgdata_dir = get_bb_var('PKGDATA_DIR')
+ package_data_file = os.path.join(pkgdata_dir, 'runtime', package_name)
+ package_data = ftools.read_file(package_data_file)
+ find_pr = re.search("PKGR: r[0-9]+\.([0-9]+)", package_data)
+ self.assertTrue(find_pr, "No PKG revision found in %s" % package_data_file)
+ return int(find_pr.group(1))
+
+ def get_task_stamp(self, package_name, recipe_task):
+ stampdata = get_bb_var('STAMP', target=package_name).split('/')
+ prefix = stampdata[-1]
+ package_stamps_path = "/".join(stampdata[:-1])
+ stamps = []
+ for stamp in os.listdir(package_stamps_path):
+ find_stamp = re.match("%s\.%s\.([a-z0-9]{32})" % (prefix, recipe_task), stamp)
+ if find_stamp:
+ stamps.append(find_stamp.group(1))
+ self.assertFalse(len(stamps) == 0, msg="Cound not find stamp for task %s for recipe %s" % (recipe_task, package_name))
+ self.assertFalse(len(stamps) > 1, msg="Found multiple %s stamps for the %s recipe in the %s directory." % (recipe_task, package_name, package_stamps_path))
+ return str(stamps[0])
+
+ def increment_package_pr(self, package_name):
+ inc_data = "do_package_append() {\nbb.build.exec_func('do_test_prserv', d)\n}\ndo_test_prserv() {\necho \"The current date is: %s\"\n}" % datetime.datetime.now()
+ self.write_recipeinc(package_name, inc_data)
+ bitbake("-ccleansstate %s" % package_name)
+ res = bitbake(package_name, ignore_status=True)
+ self.delete_recipeinc(package_name)
+ self.assertEqual(res.status, 0, msg=res.output)
+ self.assertTrue("NOTE: Started PRServer with DBfile" in res.output, msg=res.output)
+
+ def config_pr_tests(self, package_name, package_type='rpm', pr_socket='localhost:0'):
+ config_package_data = 'PACKAGE_CLASSES = "package_%s"' % package_type
+ self.write_config(config_package_data)
+ config_server_data = 'PRSERV_HOST = "%s"' % pr_socket
+ self.append_config(config_server_data)
+
+ def run_test_pr_service(self, package_name, package_type='rpm', track_task='do_package', pr_socket='localhost:0'):
+ self.config_pr_tests(package_name, package_type, pr_socket)
+
+ self.increment_package_pr(package_name)
+ pr_1 = self.get_pr_version(package_name)
+ stamp_1 = self.get_task_stamp(package_name, track_task)
+
+ self.increment_package_pr(package_name)
+ pr_2 = self.get_pr_version(package_name)
+ stamp_2 = self.get_task_stamp(package_name, track_task)
+
+ bitbake("-ccleansstate %s" % package_name)
+ self.assertTrue(pr_2 - pr_1 == 1, "Step between same pkg. revision is greater than 1")
+ self.assertTrue(stamp_1 != stamp_2, "Different pkg rev. but same stamp: %s" % stamp_1)
+
+ def run_test_pr_export_import(self, package_name, replace_current_db=True):
+ self.config_pr_tests(package_name)
+
+ self.increment_package_pr(package_name)
+ pr_1 = self.get_pr_version(package_name)
+
+ exported_db_path = os.path.join(self.builddir, 'export.inc')
+ export_result = runCmd("bitbake-prserv-tool export %s" % exported_db_path, ignore_status=True)
+ self.assertEqual(export_result.status, 0, msg="PR Service database export failed: %s" % export_result.output)
+
+ if replace_current_db:
+ current_db_path = os.path.join(get_bb_var('PERSISTENT_DIR'), 'prserv.sqlite3')
+ self.assertTrue(os.path.exists(current_db_path), msg="Path to current PR Service database is invalid: %s" % current_db_path)
+ os.remove(current_db_path)
+
+ import_result = runCmd("bitbake-prserv-tool import %s" % exported_db_path, ignore_status=True)
+ os.remove(exported_db_path)
+ self.assertEqual(import_result.status, 0, msg="PR Service database import failed: %s" % import_result.output)
+
+ self.increment_package_pr(package_name)
+ pr_2 = self.get_pr_version(package_name)
+
+ bitbake("-ccleansstate %s" % package_name)
+ self.assertTrue(pr_2 - pr_1 == 1, "Step between same pkg. revision is greater than 1")
+
+ @testcase(930)
+ def test_import_export_replace_db(self):
+ self.run_test_pr_export_import('m4')
+
+ @testcase(931)
+ def test_import_export_override_db(self):
+ self.run_test_pr_export_import('m4', replace_current_db=False)
+
+ @testcase(932)
+ def test_pr_service_rpm_arch_dep(self):
+ self.run_test_pr_service('m4', 'rpm', 'do_package')
+
+ @testcase(934)
+ def test_pr_service_deb_arch_dep(self):
+ self.run_test_pr_service('m4', 'deb', 'do_package')
+
+ @testcase(933)
+ def test_pr_service_ipk_arch_dep(self):
+ self.run_test_pr_service('m4', 'ipk', 'do_package')
+
+ @testcase(935)
+ def test_pr_service_rpm_arch_indep(self):
+ self.run_test_pr_service('xcursor-transparent-theme', 'rpm', 'do_package')
+
+ @testcase(937)
+ def test_pr_service_deb_arch_indep(self):
+ self.run_test_pr_service('xcursor-transparent-theme', 'deb', 'do_package')
+
+ @testcase(936)
+ def test_pr_service_ipk_arch_indep(self):
+ self.run_test_pr_service('xcursor-transparent-theme', 'ipk', 'do_package')
diff --git a/meta/lib/oeqa/selftest/recipetool.py b/meta/lib/oeqa/selftest/recipetool.py
new file mode 100644
index 0000000..c34ad68
--- /dev/null
+++ b/meta/lib/oeqa/selftest/recipetool.py
@@ -0,0 +1,554 @@
+import os
+import logging
+import tempfile
+import urlparse
+
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var, create_temp_layer
+from oeqa.utils.decorators import testcase
+from oeqa.selftest import devtool
+
+
+templayerdir = None
+
+
+def setUpModule():
+ global templayerdir
+ templayerdir = tempfile.mkdtemp(prefix='recipetoolqa')
+ create_temp_layer(templayerdir, 'selftestrecipetool')
+ runCmd('bitbake-layers add-layer %s' % templayerdir)
+
+
+def tearDownModule():
+ runCmd('bitbake-layers remove-layer %s' % templayerdir, ignore_status=True)
+ runCmd('rm -rf %s' % templayerdir)
+
+
+class RecipetoolBase(devtool.DevtoolBase):
+ def setUpLocal(self):
+ self.templayerdir = templayerdir
+ self.tempdir = tempfile.mkdtemp(prefix='recipetoolqa')
+ self.track_for_cleanup(self.tempdir)
+ self.testfile = os.path.join(self.tempdir, 'testfile')
+ with open(self.testfile, 'w') as f:
+ f.write('Test file\n')
+
+ def tearDownLocal(self):
+ runCmd('rm -rf %s/recipes-*' % self.templayerdir)
+
+ def _try_recipetool_appendcmd(self, cmd, testrecipe, expectedfiles, expectedlines=None):
+ result = runCmd(cmd)
+ self.assertNotIn('Traceback', result.output)
+
+ # Check the bbappend was created and applies properly
+ recipefile = get_bb_var('FILE', testrecipe)
+ bbappendfile = self._check_bbappend(testrecipe, recipefile, self.templayerdir)
+
+ # Check the bbappend contents
+ if expectedlines is not None:
+ with open(bbappendfile, 'r') as f:
+ self.assertEqual(expectedlines, f.readlines(), "Expected lines are not present in %s" % bbappendfile)
+
+ # Check file was copied
+ filesdir = os.path.join(os.path.dirname(bbappendfile), testrecipe)
+ for expectedfile in expectedfiles:
+ self.assertTrue(os.path.isfile(os.path.join(filesdir, expectedfile)), 'Expected file %s to be copied next to bbappend, but it wasn\'t' % expectedfile)
+
+ # Check no other files created
+ createdfiles = []
+ for root, _, files in os.walk(filesdir):
+ for f in files:
+ createdfiles.append(os.path.relpath(os.path.join(root, f), filesdir))
+ self.assertTrue(sorted(createdfiles), sorted(expectedfiles))
+
+ return bbappendfile, result.output
+
+
+class RecipetoolTests(RecipetoolBase):
+ @classmethod
+ def setUpClass(cls):
+ # Ensure we have the right data in shlibs/pkgdata
+ logger = logging.getLogger("selftest")
+ logger.info('Running bitbake to generate pkgdata')
+ bitbake('-c packagedata base-files coreutils busybox selftest-recipetool-appendfile')
+
+ @classmethod
+ def tearDownClass(cls):
+ # Shouldn't leave any traces of this artificial recipe behind
+ bitbake('-c cleansstate selftest-recipetool-appendfile')
+
+ def _try_recipetool_appendfile(self, testrecipe, destfile, newfile, options, expectedlines, expectedfiles):
+ cmd = 'recipetool appendfile %s %s %s %s' % (self.templayerdir, destfile, newfile, options)
+ return self._try_recipetool_appendcmd(cmd, testrecipe, expectedfiles, expectedlines)
+
+ def _try_recipetool_appendfile_fail(self, destfile, newfile, checkerror):
+ cmd = 'recipetool appendfile %s %s %s' % (self.templayerdir, destfile, newfile)
+ result = runCmd(cmd, ignore_status=True)
+ self.assertNotEqual(result.status, 0, 'Command "%s" should have failed but didn\'t' % cmd)
+ self.assertNotIn('Traceback', result.output)
+ for errorstr in checkerror:
+ self.assertIn(errorstr, result.output)
+
+ @testcase(1177)
+ def test_recipetool_appendfile_basic(self):
+ # Basic test
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ _, output = self._try_recipetool_appendfile('base-files', '/etc/motd', self.testfile, '', expectedlines, ['motd'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1183)
+ def test_recipetool_appendfile_invalid(self):
+ # Test some commands that should error
+ self._try_recipetool_appendfile_fail('/etc/passwd', self.testfile, ['ERROR: /etc/passwd cannot be handled by this tool', 'useradd', 'extrausers'])
+ self._try_recipetool_appendfile_fail('/etc/timestamp', self.testfile, ['ERROR: /etc/timestamp cannot be handled by this tool'])
+ self._try_recipetool_appendfile_fail('/dev/console', self.testfile, ['ERROR: /dev/console cannot be handled by this tool'])
+
+ @testcase(1176)
+ def test_recipetool_appendfile_alternatives(self):
+ # Now try with a file we know should be an alternative
+ # (this is very much a fake example, but one we know is reliably an alternative)
+ self._try_recipetool_appendfile_fail('/bin/ls', self.testfile, ['ERROR: File /bin/ls is an alternative possibly provided by the following recipes:', 'coreutils', 'busybox'])
+ corebase = get_bb_var('COREBASE')
+ # Need a test file - should be executable
+ testfile2 = os.path.join(corebase, 'oe-init-build-env')
+ testfile2name = os.path.basename(testfile2)
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://%s"\n' % testfile2name,
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${base_bindir}\n',
+ ' install -m 0755 ${WORKDIR}/%s ${D}${base_bindir}/ls\n' % testfile2name,
+ '}\n']
+ self._try_recipetool_appendfile('coreutils', '/bin/ls', testfile2, '-r coreutils', expectedlines, [testfile2name])
+ # Now try bbappending the same file again, contents should not change
+ bbappendfile, _ = self._try_recipetool_appendfile('coreutils', '/bin/ls', self.testfile, '-r coreutils', expectedlines, [testfile2name])
+ # But file should have
+ copiedfile = os.path.join(os.path.dirname(bbappendfile), 'coreutils', testfile2name)
+ result = runCmd('diff -q %s %s' % (testfile2, copiedfile), ignore_status=True)
+ self.assertNotEqual(result.status, 0, 'New file should have been copied but was not %s' % result.output)
+
+ @testcase(1178)
+ def test_recipetool_appendfile_binary(self):
+ # Try appending a binary file
+ # /bin/ls can be a symlink to /usr/bin/ls
+ ls = os.path.realpath("/bin/ls")
+ result = runCmd('recipetool appendfile %s /bin/ls %s -r coreutils' % (self.templayerdir, ls))
+ self.assertIn('WARNING: ', result.output)
+ self.assertIn('is a binary', result.output)
+
+ @testcase(1173)
+ def test_recipetool_appendfile_add(self):
+ corebase = get_bb_var('COREBASE')
+ # Try arbitrary file add to a recipe
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://testfile"\n',
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${datadir}\n',
+ ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/something\n',
+ '}\n']
+ self._try_recipetool_appendfile('netbase', '/usr/share/something', self.testfile, '-r netbase', expectedlines, ['testfile'])
+ # Try adding another file, this time where the source file is executable
+ # (so we're testing that, plus modifying an existing bbappend)
+ testfile2 = os.path.join(corebase, 'oe-init-build-env')
+ testfile2name = os.path.basename(testfile2)
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://testfile \\\n',
+ ' file://%s \\\n' % testfile2name,
+ ' "\n',
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${datadir}\n',
+ ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/something\n',
+ ' install -m 0755 ${WORKDIR}/%s ${D}${datadir}/scriptname\n' % testfile2name,
+ '}\n']
+ self._try_recipetool_appendfile('netbase', '/usr/share/scriptname', testfile2, '-r netbase', expectedlines, ['testfile', testfile2name])
+
+ @testcase(1174)
+ def test_recipetool_appendfile_add_bindir(self):
+ # Try arbitrary file add to a recipe, this time to a location such that should be installed as executable
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://testfile"\n',
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${bindir}\n',
+ ' install -m 0755 ${WORKDIR}/testfile ${D}${bindir}/selftest-recipetool-testbin\n',
+ '}\n']
+ _, output = self._try_recipetool_appendfile('netbase', '/usr/bin/selftest-recipetool-testbin', self.testfile, '-r netbase', expectedlines, ['testfile'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1175)
+ def test_recipetool_appendfile_add_machine(self):
+ # Try arbitrary file add to a recipe, this time to a location such that should be installed as executable
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'PACKAGE_ARCH = "${MACHINE_ARCH}"\n',
+ '\n',
+ 'SRC_URI_append_mymachine = " file://testfile"\n',
+ '\n',
+ 'do_install_append_mymachine() {\n',
+ ' install -d ${D}${datadir}\n',
+ ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/something\n',
+ '}\n']
+ _, output = self._try_recipetool_appendfile('netbase', '/usr/share/something', self.testfile, '-r netbase -m mymachine', expectedlines, ['mymachine/testfile'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1184)
+ def test_recipetool_appendfile_orig(self):
+ # A file that's in SRC_URI and in do_install with the same name
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-orig', self.testfile, '', expectedlines, ['selftest-replaceme-orig'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1191)
+ def test_recipetool_appendfile_todir(self):
+ # A file that's in SRC_URI and in do_install with destination directory rather than file
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-todir', self.testfile, '', expectedlines, ['selftest-replaceme-todir'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1187)
+ def test_recipetool_appendfile_renamed(self):
+ # A file that's in SRC_URI with a different name to the destination file
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-renamed', self.testfile, '', expectedlines, ['file1'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1190)
+ def test_recipetool_appendfile_subdir(self):
+ # A file that's in SRC_URI in a subdir
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://testfile"\n',
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${datadir}\n',
+ ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/selftest-replaceme-subdir\n',
+ '}\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-subdir', self.testfile, '', expectedlines, ['testfile'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1189)
+ def test_recipetool_appendfile_src_glob(self):
+ # A file that's in SRC_URI as a glob
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://testfile"\n',
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${datadir}\n',
+ ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/selftest-replaceme-src-globfile\n',
+ '}\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-src-globfile', self.testfile, '', expectedlines, ['testfile'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1181)
+ def test_recipetool_appendfile_inst_glob(self):
+ # A file that's in do_install as a glob
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-inst-globfile', self.testfile, '', expectedlines, ['selftest-replaceme-inst-globfile'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1182)
+ def test_recipetool_appendfile_inst_todir_glob(self):
+ # A file that's in do_install as a glob with destination as a directory
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-inst-todir-globfile', self.testfile, '', expectedlines, ['selftest-replaceme-inst-todir-globfile'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1185)
+ def test_recipetool_appendfile_patch(self):
+ # A file that's added by a patch in SRC_URI
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://testfile"\n',
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${sysconfdir}\n',
+ ' install -m 0644 ${WORKDIR}/testfile ${D}${sysconfdir}/selftest-replaceme-patched\n',
+ '}\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/etc/selftest-replaceme-patched', self.testfile, '', expectedlines, ['testfile'])
+ for line in output.splitlines():
+ if line.startswith('WARNING: '):
+ self.assertIn('add-file.patch', line, 'Unexpected warning found in output:\n%s' % line)
+ break
+ else:
+ self.fail('Patch warning not found in output:\n%s' % output)
+
+ @testcase(1188)
+ def test_recipetool_appendfile_script(self):
+ # Now, a file that's in SRC_URI but installed by a script (so no mention in do_install)
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://testfile"\n',
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${datadir}\n',
+ ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/selftest-replaceme-scripted\n',
+ '}\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-scripted', self.testfile, '', expectedlines, ['testfile'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1180)
+ def test_recipetool_appendfile_inst_func(self):
+ # A file that's installed from a function called by do_install
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-inst-func', self.testfile, '', expectedlines, ['selftest-replaceme-inst-func'])
+ self.assertNotIn('WARNING: ', output)
+
+ @testcase(1186)
+ def test_recipetool_appendfile_postinstall(self):
+ # A file that's created by a postinstall script (and explicitly mentioned in it)
+ # First try without specifying recipe
+ self._try_recipetool_appendfile_fail('/usr/share/selftest-replaceme-postinst', self.testfile, ['File /usr/share/selftest-replaceme-postinst may be written out in a pre/postinstall script of the following recipes:', 'selftest-recipetool-appendfile'])
+ # Now specify recipe
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n',
+ 'SRC_URI += "file://testfile"\n',
+ '\n',
+ 'do_install_append() {\n',
+ ' install -d ${D}${datadir}\n',
+ ' install -m 0644 ${WORKDIR}/testfile ${D}${datadir}/selftest-replaceme-postinst\n',
+ '}\n']
+ _, output = self._try_recipetool_appendfile('selftest-recipetool-appendfile', '/usr/share/selftest-replaceme-postinst', self.testfile, '-r selftest-recipetool-appendfile', expectedlines, ['testfile'])
+
+ @testcase(1179)
+ def test_recipetool_appendfile_extlayer(self):
+ # Try creating a bbappend in a layer that's not in bblayers.conf and has a different structure
+ exttemplayerdir = os.path.join(self.tempdir, 'extlayer')
+ self._create_temp_layer(exttemplayerdir, False, 'oeselftestextlayer', recipepathspec='metadata/recipes/recipes-*/*')
+ result = runCmd('recipetool appendfile %s /usr/share/selftest-replaceme-orig %s' % (exttemplayerdir, self.testfile))
+ self.assertNotIn('Traceback', result.output)
+ createdfiles = []
+ for root, _, files in os.walk(exttemplayerdir):
+ for f in files:
+ createdfiles.append(os.path.relpath(os.path.join(root, f), exttemplayerdir))
+ createdfiles.remove('conf/layer.conf')
+ expectedfiles = ['metadata/recipes/recipes-test/selftest-recipetool-appendfile/selftest-recipetool-appendfile.bbappend',
+ 'metadata/recipes/recipes-test/selftest-recipetool-appendfile/selftest-recipetool-appendfile/selftest-replaceme-orig']
+ self.assertEqual(sorted(createdfiles), sorted(expectedfiles))
+
+ @testcase(1192)
+ def test_recipetool_appendfile_wildcard(self):
+
+ def try_appendfile_wc(options):
+ result = runCmd('recipetool appendfile %s /etc/profile %s %s' % (self.templayerdir, self.testfile, options))
+ self.assertNotIn('Traceback', result.output)
+ bbappendfile = None
+ for root, _, files in os.walk(self.templayerdir):
+ for f in files:
+ if f.endswith('.bbappend'):
+ bbappendfile = f
+ break
+ if not bbappendfile:
+ self.fail('No bbappend file created')
+ runCmd('rm -rf %s/recipes-*' % self.templayerdir)
+ return bbappendfile
+
+ # Check without wildcard option
+ recipefn = os.path.basename(get_bb_var('FILE', 'base-files'))
+ filename = try_appendfile_wc('')
+ self.assertEqual(filename, recipefn.replace('.bb', '.bbappend'))
+ # Now check with wildcard option
+ filename = try_appendfile_wc('-w')
+ self.assertEqual(filename, recipefn.split('_')[0] + '_%.bbappend')
+
+ @testcase(1193)
+ def test_recipetool_create(self):
+ # Try adding a recipe
+ tempsrc = os.path.join(self.tempdir, 'srctree')
+ os.makedirs(tempsrc)
+ recipefile = os.path.join(self.tempdir, 'logrotate_3.8.7.bb')
+ srcuri = 'https://fedorahosted.org/releases/l/o/logrotate/logrotate-3.8.7.tar.gz'
+ result = runCmd('recipetool create -o %s %s -x %s' % (recipefile, srcuri, tempsrc))
+ self.assertTrue(os.path.isfile(recipefile))
+ checkvars = {}
+ checkvars['LICENSE'] = 'GPLv2'
+ checkvars['LIC_FILES_CHKSUM'] = 'file://COPYING;md5=18810669f13b87348459e611d31ab760'
+ checkvars['SRC_URI'] = 'https://fedorahosted.org/releases/l/o/logrotate/logrotate-${PV}.tar.gz'
+ checkvars['SRC_URI[md5sum]'] = '99e08503ef24c3e2e3ff74cc5f3be213'
+ checkvars['SRC_URI[sha256sum]'] = 'f6ba691f40e30e640efa2752c1f9499a3f9738257660994de70a45fe00d12b64'
+ self._test_recipe_contents(recipefile, checkvars, [])
+
+ @testcase(1194)
+ def test_recipetool_create_git(self):
+ # Ensure we have the right data in shlibs/pkgdata
+ bitbake('libpng pango libx11 libxext jpeg')
+ # Try adding a recipe
+ tempsrc = os.path.join(self.tempdir, 'srctree')
+ os.makedirs(tempsrc)
+ recipefile = os.path.join(self.tempdir, 'libmatchbox.bb')
+ srcuri = 'git://git.yoctoproject.org/libmatchbox'
+ result = runCmd('recipetool create -o %s %s -x %s' % (recipefile, srcuri, tempsrc))
+ self.assertTrue(os.path.isfile(recipefile), 'recipetool did not create recipe file; output:\n%s' % result.output)
+ checkvars = {}
+ checkvars['LICENSE'] = 'LGPLv2.1'
+ checkvars['LIC_FILES_CHKSUM'] = 'file://COPYING;md5=7fbc338309ac38fefcd64b04bb903e34'
+ checkvars['S'] = '${WORKDIR}/git'
+ checkvars['PV'] = '1.0+git${SRCPV}'
+ checkvars['SRC_URI'] = srcuri
+ checkvars['DEPENDS'] = 'libpng pango libx11 libxext jpeg'
+ inherits = ['autotools', 'pkgconfig']
+ self._test_recipe_contents(recipefile, checkvars, inherits)
+
+
+class RecipetoolAppendsrcBase(RecipetoolBase):
+ def _try_recipetool_appendsrcfile(self, testrecipe, newfile, destfile, options, expectedlines, expectedfiles):
+ cmd = 'recipetool appendsrcfile %s %s %s %s %s' % (options, self.templayerdir, testrecipe, newfile, destfile)
+ return self._try_recipetool_appendcmd(cmd, testrecipe, expectedfiles, expectedlines)
+
+ def _try_recipetool_appendsrcfiles(self, testrecipe, newfiles, expectedlines=None, expectedfiles=None, destdir=None, options=''):
+
+ if destdir:
+ options += ' -D %s' % destdir
+
+ if expectedfiles is None:
+ expectedfiles = [os.path.basename(f) for f in newfiles]
+
+ cmd = 'recipetool appendsrcfiles %s %s %s %s' % (options, self.templayerdir, testrecipe, ' '.join(newfiles))
+ return self._try_recipetool_appendcmd(cmd, testrecipe, expectedfiles, expectedlines)
+
+ def _try_recipetool_appendsrcfile_fail(self, testrecipe, newfile, destfile, checkerror):
+ cmd = 'recipetool appendsrcfile %s %s %s %s' % (self.templayerdir, testrecipe, newfile, destfile or '')
+ result = runCmd(cmd, ignore_status=True)
+ self.assertNotEqual(result.status, 0, 'Command "%s" should have failed but didn\'t' % cmd)
+ self.assertNotIn('Traceback', result.output)
+ for errorstr in checkerror:
+ self.assertIn(errorstr, result.output)
+
+ @staticmethod
+ def _get_first_file_uri(recipe):
+ '''Return the first file:// in SRC_URI for the specified recipe.'''
+ src_uri = get_bb_var('SRC_URI', recipe).split()
+ for uri in src_uri:
+ p = urlparse.urlparse(uri)
+ if p.scheme == 'file':
+ return p.netloc + p.path
+
+ def _test_appendsrcfile(self, testrecipe, filename=None, destdir=None, has_src_uri=True, srcdir=None, newfile=None, options=''):
+ if newfile is None:
+ newfile = self.testfile
+
+ if srcdir:
+ if destdir:
+ expected_subdir = os.path.join(srcdir, destdir)
+ else:
+ expected_subdir = srcdir
+ else:
+ options += " -W"
+ expected_subdir = destdir
+
+ if filename:
+ if destdir:
+ destpath = os.path.join(destdir, filename)
+ else:
+ destpath = filename
+ else:
+ filename = os.path.basename(newfile)
+ if destdir:
+ destpath = destdir + os.sep
+ else:
+ destpath = '.' + os.sep
+
+ expectedlines = ['FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n',
+ '\n']
+ if has_src_uri:
+ uri = 'file://%s' % filename
+ if expected_subdir:
+ uri += ';subdir=%s' % expected_subdir
+ expectedlines[0:0] = ['SRC_URI += "%s"\n' % uri,
+ '\n']
+
+ return self._try_recipetool_appendsrcfile(testrecipe, newfile, destpath, options, expectedlines, [filename])
+
+ def _test_appendsrcfiles(self, testrecipe, newfiles, expectedfiles=None, destdir=None, options=''):
+ if expectedfiles is None:
+ expectedfiles = [os.path.basename(n) for n in newfiles]
+
+ self._try_recipetool_appendsrcfiles(testrecipe, newfiles, expectedfiles=expectedfiles, destdir=destdir, options=options)
+
+ src_uri = get_bb_var('SRC_URI', testrecipe).split()
+ for f in expectedfiles:
+ if destdir:
+ self.assertIn('file://%s;subdir=%s' % (f, destdir), src_uri)
+ else:
+ self.assertIn('file://%s' % f, src_uri)
+
+ recipefile = get_bb_var('FILE', testrecipe)
+ bbappendfile = self._check_bbappend(testrecipe, recipefile, self.templayerdir)
+ filesdir = os.path.join(os.path.dirname(bbappendfile), testrecipe)
+ filesextrapaths = get_bb_var('FILESEXTRAPATHS', testrecipe).split(':')
+ self.assertIn(filesdir, filesextrapaths)
+
+
+class RecipetoolAppendsrcTests(RecipetoolAppendsrcBase):
+ def test_recipetool_appendsrcfile_basic(self):
+ self._test_appendsrcfile('base-files', 'a-file')
+
+ def test_recipetool_appendsrcfile_basic_wildcard(self):
+ testrecipe = 'base-files'
+ self._test_appendsrcfile(testrecipe, 'a-file', options='-w')
+ recipefile = get_bb_var('FILE', testrecipe)
+ bbappendfile = self._check_bbappend(testrecipe, recipefile, self.templayerdir)
+ self.assertEqual(os.path.basename(bbappendfile), '%s_%%.bbappend' % testrecipe)
+
+ def test_recipetool_appendsrcfile_subdir_basic(self):
+ self._test_appendsrcfile('base-files', 'a-file', 'tmp')
+
+ def test_recipetool_appendsrcfile_subdir_basic_dirdest(self):
+ self._test_appendsrcfile('base-files', destdir='tmp')
+
+ def test_recipetool_appendsrcfile_srcdir_basic(self):
+ testrecipe = 'bash'
+ srcdir = get_bb_var('S', testrecipe)
+ workdir = get_bb_var('WORKDIR', testrecipe)
+ subdir = os.path.relpath(srcdir, workdir)
+ self._test_appendsrcfile(testrecipe, 'a-file', srcdir=subdir)
+
+ def test_recipetool_appendsrcfile_existing_in_src_uri(self):
+ testrecipe = 'base-files'
+ filepath = self._get_first_file_uri(testrecipe)
+ self.assertTrue(filepath, 'Unable to test, no file:// uri found in SRC_URI for %s' % testrecipe)
+ self._test_appendsrcfile(testrecipe, filepath, has_src_uri=False)
+
+ def test_recipetool_appendsrcfile_existing_in_src_uri_diff_params(self):
+ testrecipe = 'base-files'
+ subdir = 'tmp'
+ filepath = self._get_first_file_uri(testrecipe)
+ self.assertTrue(filepath, 'Unable to test, no file:// uri found in SRC_URI for %s' % testrecipe)
+
+ output = self._test_appendsrcfile(testrecipe, filepath, subdir, has_src_uri=False)
+ self.assertTrue(any('with different parameters' in l for l in output))
+
+ def test_recipetool_appendsrcfile_replace_file_srcdir(self):
+ testrecipe = 'bash'
+ filepath = 'Makefile.in'
+ srcdir = get_bb_var('S', testrecipe)
+ workdir = get_bb_var('WORKDIR', testrecipe)
+ subdir = os.path.relpath(srcdir, workdir)
+
+ self._test_appendsrcfile(testrecipe, filepath, srcdir=subdir)
+ bitbake('%s:do_unpack' % testrecipe)
+ self.assertEqual(open(self.testfile, 'r').read(), open(os.path.join(srcdir, filepath), 'r').read())
+
+ def test_recipetool_appendsrcfiles_basic(self, destdir=None):
+ newfiles = [self.testfile]
+ for i in range(1, 5):
+ testfile = os.path.join(self.tempdir, 'testfile%d' % i)
+ with open(testfile, 'w') as f:
+ f.write('Test file %d\n' % i)
+ newfiles.append(testfile)
+ self._test_appendsrcfiles('gcc', newfiles, destdir=destdir, options='-W')
+
+ def test_recipetool_appendsrcfiles_basic_subdir(self):
+ self.test_recipetool_appendsrcfiles_basic(destdir='testdir')
diff --git a/meta/lib/oeqa/selftest/sstate.py b/meta/lib/oeqa/selftest/sstate.py
new file mode 100644
index 0000000..5989724
--- /dev/null
+++ b/meta/lib/oeqa/selftest/sstate.py
@@ -0,0 +1,53 @@
+import datetime
+import unittest
+import os
+import re
+import shutil
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_test_layer
+
+
+class SStateBase(oeSelfTest):
+
+ def setUpLocal(self):
+ self.temp_sstate_location = None
+ self.sstate_path = get_bb_var('SSTATE_DIR')
+ self.distro = get_bb_var('NATIVELSBSTRING')
+ self.distro_specific_sstate = os.path.join(self.sstate_path, self.distro)
+
+ # Creates a special sstate configuration with the option to add sstate mirrors
+ def config_sstate(self, temp_sstate_location=False, add_local_mirrors=[]):
+ self.temp_sstate_location = temp_sstate_location
+
+ if self.temp_sstate_location:
+ temp_sstate_path = os.path.join(self.builddir, "temp_sstate_%s" % datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
+ config_temp_sstate = "SSTATE_DIR = \"%s\"" % temp_sstate_path
+ self.append_config(config_temp_sstate)
+ self.track_for_cleanup(temp_sstate_path)
+ self.sstate_path = get_bb_var('SSTATE_DIR')
+ self.distro = get_bb_var('NATIVELSBSTRING')
+ self.distro_specific_sstate = os.path.join(self.sstate_path, self.distro)
+
+ if add_local_mirrors:
+ config_set_sstate_if_not_set = 'SSTATE_MIRRORS ?= ""'
+ self.append_config(config_set_sstate_if_not_set)
+ for local_mirror in add_local_mirrors:
+ self.assertFalse(os.path.join(local_mirror) == os.path.join(self.sstate_path), msg='Cannot add the current sstate path as a sstate mirror')
+ config_sstate_mirror = "SSTATE_MIRRORS += \"file://.* file:///%s/PATH\"" % local_mirror
+ self.append_config(config_sstate_mirror)
+
+ # Returns a list containing sstate files
+ def search_sstate(self, filename_regex, distro_specific=True, distro_nonspecific=True):
+ result = []
+ for root, dirs, files in os.walk(self.sstate_path):
+ if distro_specific and re.search("%s/[a-z0-9]{2}$" % self.distro, root):
+ for f in files:
+ if re.search(filename_regex, f):
+ result.append(f)
+ if distro_nonspecific and re.search("%s/[a-z0-9]{2}$" % self.sstate_path, root):
+ for f in files:
+ if re.search(filename_regex, f):
+ result.append(f)
+ return result
diff --git a/meta/lib/oeqa/selftest/sstatetests.py b/meta/lib/oeqa/selftest/sstatetests.py
new file mode 100644
index 0000000..6906b21
--- /dev/null
+++ b/meta/lib/oeqa/selftest/sstatetests.py
@@ -0,0 +1,308 @@
+import datetime
+import unittest
+import os
+import re
+import shutil
+
+import oeqa.utils.ftools as ftools
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_test_layer
+from oeqa.selftest.sstate import SStateBase
+from oeqa.utils.decorators import testcase
+
+class SStateTests(SStateBase):
+
+ # Test sstate files creation and their location
+ def run_test_sstate_creation(self, targets, distro_specific=True, distro_nonspecific=True, temp_sstate_location=True, should_pass=True):
+ self.config_sstate(temp_sstate_location)
+
+ if self.temp_sstate_location:
+ bitbake(['-cclean'] + targets)
+ else:
+ bitbake(['-ccleansstate'] + targets)
+
+ bitbake(targets)
+ file_tracker = self.search_sstate('|'.join(map(str, targets)), distro_specific, distro_nonspecific)
+ if should_pass:
+ self.assertTrue(file_tracker , msg="Could not find sstate files for: %s" % ', '.join(map(str, targets)))
+ else:
+ self.assertTrue(not file_tracker , msg="Found sstate files in the wrong place for: %s" % ', '.join(map(str, targets)))
+
+ @testcase(975)
+ def test_sstate_creation_distro_specific_pass(self):
+ targetarch = get_bb_var('TUNE_ARCH')
+ self.run_test_sstate_creation(['binutils-cross-'+ targetarch, 'binutils-native'], distro_specific=True, distro_nonspecific=False, temp_sstate_location=True)
+
+ @testcase(975)
+ def test_sstate_creation_distro_specific_fail(self):
+ targetarch = get_bb_var('TUNE_ARCH')
+ self.run_test_sstate_creation(['binutils-cross-'+ targetarch, 'binutils-native'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True, should_pass=False)
+
+ @testcase(976)
+ def test_sstate_creation_distro_nonspecific_pass(self):
+ self.run_test_sstate_creation(['glibc-initial'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True)
+
+ @testcase(976)
+ def test_sstate_creation_distro_nonspecific_fail(self):
+ self.run_test_sstate_creation(['glibc-initial'], distro_specific=True, distro_nonspecific=False, temp_sstate_location=True, should_pass=False)
+
+
+ # Test the sstate files deletion part of the do_cleansstate task
+ def run_test_cleansstate_task(self, targets, distro_specific=True, distro_nonspecific=True, temp_sstate_location=True):
+ self.config_sstate(temp_sstate_location)
+
+ bitbake(['-ccleansstate'] + targets)
+
+ bitbake(targets)
+ tgz_created = self.search_sstate('|'.join(map(str, [s + '.*?\.tgz$' for s in targets])), distro_specific, distro_nonspecific)
+ self.assertTrue(tgz_created, msg="Could not find sstate .tgz files for: %s" % ', '.join(map(str, targets)))
+
+ siginfo_created = self.search_sstate('|'.join(map(str, [s + '.*?\.siginfo$' for s in targets])), distro_specific, distro_nonspecific)
+ self.assertTrue(siginfo_created, msg="Could not find sstate .siginfo files for: %s" % ', '.join(map(str, targets)))
+
+ bitbake(['-ccleansstate'] + targets)
+ tgz_removed = self.search_sstate('|'.join(map(str, [s + '.*?\.tgz$' for s in targets])), distro_specific, distro_nonspecific)
+ self.assertTrue(not tgz_removed, msg="do_cleansstate didn't remove .tgz sstate files for: %s" % ', '.join(map(str, targets)))
+
+ @testcase(977)
+ def test_cleansstate_task_distro_specific_nonspecific(self):
+ targetarch = get_bb_var('TUNE_ARCH')
+ self.run_test_cleansstate_task(['binutils-cross-' + targetarch, 'binutils-native', 'glibc-initial'], distro_specific=True, distro_nonspecific=True, temp_sstate_location=True)
+
+ @testcase(977)
+ def test_cleansstate_task_distro_nonspecific(self):
+ self.run_test_cleansstate_task(['glibc-initial'], distro_specific=False, distro_nonspecific=True, temp_sstate_location=True)
+
+ @testcase(977)
+ def test_cleansstate_task_distro_specific(self):
+ targetarch = get_bb_var('TUNE_ARCH')
+ self.run_test_cleansstate_task(['binutils-cross-'+ targetarch, 'binutils-native', 'glibc-initial'], distro_specific=True, distro_nonspecific=False, temp_sstate_location=True)
+
+
+ # Test rebuilding of distro-specific sstate files
+ def run_test_rebuild_distro_specific_sstate(self, targets, temp_sstate_location=True):
+ self.config_sstate(temp_sstate_location)
+
+ bitbake(['-ccleansstate'] + targets)
+
+ bitbake(targets)
+ self.assertTrue(self.search_sstate('|'.join(map(str, [s + '.*?\.tgz$' for s in targets])), distro_specific=False, distro_nonspecific=True) == [], msg="Found distro non-specific sstate for: %s" % ', '.join(map(str, targets)))
+ file_tracker_1 = self.search_sstate('|'.join(map(str, [s + '.*?\.tgz$' for s in targets])), distro_specific=True, distro_nonspecific=False)
+ self.assertTrue(len(file_tracker_1) >= len(targets), msg = "Not all sstate files ware created for: %s" % ', '.join(map(str, targets)))
+
+ self.track_for_cleanup(self.distro_specific_sstate + "_old")
+ shutil.copytree(self.distro_specific_sstate, self.distro_specific_sstate + "_old")
+ shutil.rmtree(self.distro_specific_sstate)
+
+ bitbake(['-cclean'] + targets)
+ bitbake(targets)
+ file_tracker_2 = self.search_sstate('|'.join(map(str, [s + '.*?\.tgz$' for s in targets])), distro_specific=True, distro_nonspecific=False)
+ self.assertTrue(len(file_tracker_2) >= len(targets), msg = "Not all sstate files ware created for: %s" % ', '.join(map(str, targets)))
+
+ not_recreated = [x for x in file_tracker_1 if x not in file_tracker_2]
+ self.assertTrue(not_recreated == [], msg="The following sstate files ware not recreated: %s" % ', '.join(map(str, not_recreated)))
+
+ created_once = [x for x in file_tracker_2 if x not in file_tracker_1]
+ self.assertTrue(created_once == [], msg="The following sstate files ware created only in the second run: %s" % ', '.join(map(str, created_once)))
+
+ @testcase(175)
+ def test_rebuild_distro_specific_sstate_cross_native_targets(self):
+ targetarch = get_bb_var('TUNE_ARCH')
+ self.run_test_rebuild_distro_specific_sstate(['binutils-cross-' + targetarch, 'binutils-native'], temp_sstate_location=True)
+
+ @testcase(175)
+ def test_rebuild_distro_specific_sstate_cross_target(self):
+ targetarch = get_bb_var('TUNE_ARCH')
+ self.run_test_rebuild_distro_specific_sstate(['binutils-cross-' + targetarch], temp_sstate_location=True)
+
+ @testcase(175)
+ def test_rebuild_distro_specific_sstate_native_target(self):
+ self.run_test_rebuild_distro_specific_sstate(['binutils-native'], temp_sstate_location=True)
+
+
+ # Test the sstate-cache-management script. Each element in the global_config list is used with the corresponding element in the target_config list
+ # global_config elements are expected to not generate any sstate files that would be removed by sstate-cache-management.sh (such as changing the value of MACHINE)
+ def run_test_sstate_cache_management_script(self, target, global_config=[''], target_config=[''], ignore_patterns=[]):
+ self.assertTrue(global_config)
+ self.assertTrue(target_config)
+ self.assertTrue(len(global_config) == len(target_config), msg='Lists global_config and target_config should have the same number of elements')
+ self.config_sstate(temp_sstate_location=True, add_local_mirrors=[self.sstate_path])
+
+ # If buildhistory is enabled, we need to disable version-going-backwards QA checks for this test. It may report errors otherwise.
+ if ('buildhistory' in get_bb_var('USER_CLASSES')) or ('buildhistory' in get_bb_var('INHERIT')):
+ remove_errors_config = 'ERROR_QA_remove = "version-going-backwards"'
+ self.append_config(remove_errors_config)
+
+ # For not this only checks if random sstate tasks are handled correctly as a group.
+ # In the future we should add control over what tasks we check for.
+
+ sstate_archs_list = []
+ expected_remaining_sstate = []
+ for idx in range(len(target_config)):
+ self.append_config(global_config[idx])
+ self.append_recipeinc(target, target_config[idx])
+ sstate_arch = get_bb_var('SSTATE_PKGARCH', target)
+ if not sstate_arch in sstate_archs_list:
+ sstate_archs_list.append(sstate_arch)
+ if target_config[idx] == target_config[-1]:
+ target_sstate_before_build = self.search_sstate(target + '.*?\.tgz$')
+ bitbake("-cclean %s" % target)
+ result = bitbake(target, ignore_status=True)
+ if target_config[idx] == target_config[-1]:
+ target_sstate_after_build = self.search_sstate(target + '.*?\.tgz$')
+ expected_remaining_sstate += [x for x in target_sstate_after_build if x not in target_sstate_before_build if not any(pattern in x for pattern in ignore_patterns)]
+ self.remove_config(global_config[idx])
+ self.remove_recipeinc(target, target_config[idx])
+ self.assertEqual(result.status, 0, msg = "build of %s failed with %s" % (target, result.output))
+
+ runCmd("sstate-cache-management.sh -y --cache-dir=%s --remove-duplicated --extra-archs=%s" % (self.sstate_path, ','.join(map(str, sstate_archs_list))))
+ actual_remaining_sstate = [x for x in self.search_sstate(target + '.*?\.tgz$') if not any(pattern in x for pattern in ignore_patterns)]
+
+ actual_not_expected = [x for x in actual_remaining_sstate if x not in expected_remaining_sstate]
+ self.assertFalse(actual_not_expected, msg="Files should have been removed but ware not: %s" % ', '.join(map(str, actual_not_expected)))
+ expected_not_actual = [x for x in expected_remaining_sstate if x not in actual_remaining_sstate]
+ self.assertFalse(expected_not_actual, msg="Extra files ware removed: %s" ', '.join(map(str, expected_not_actual)))
+
+ @testcase(973)
+ def test_sstate_cache_management_script_using_pr_1(self):
+ global_config = []
+ target_config = []
+ global_config.append('')
+ target_config.append('PR = "0"')
+ self.run_test_sstate_cache_management_script('m4', global_config, target_config, ignore_patterns=['populate_lic'])
+
+ @testcase(978)
+ def test_sstate_cache_management_script_using_pr_2(self):
+ global_config = []
+ target_config = []
+ global_config.append('')
+ target_config.append('PR = "0"')
+ global_config.append('')
+ target_config.append('PR = "1"')
+ self.run_test_sstate_cache_management_script('m4', global_config, target_config, ignore_patterns=['populate_lic'])
+
+ @testcase(979)
+ def test_sstate_cache_management_script_using_pr_3(self):
+ global_config = []
+ target_config = []
+ global_config.append('MACHINE = "qemux86-64"')
+ target_config.append('PR = "0"')
+ global_config.append(global_config[0])
+ target_config.append('PR = "1"')
+ global_config.append('MACHINE = "qemux86"')
+ target_config.append('PR = "1"')
+ self.run_test_sstate_cache_management_script('m4', global_config, target_config, ignore_patterns=['populate_lic'])
+
+ @testcase(974)
+ def test_sstate_cache_management_script_using_machine(self):
+ global_config = []
+ target_config = []
+ global_config.append('MACHINE = "qemux86-64"')
+ target_config.append('')
+ global_config.append('MACHINE = "qemux86"')
+ target_config.append('')
+ self.run_test_sstate_cache_management_script('m4', global_config, target_config, ignore_patterns=['populate_lic'])
+
+ @testcase(1270)
+ def test_sstate_32_64_same_hash(self):
+ """
+ The sstate checksums for both native and target should not vary whether
+ they're built on a 32 or 64 bit system. Rather than requiring two different
+ build machines and running a builds, override the variables calling uname()
+ manually and check using bitbake -S.
+ """
+
+ topdir = get_bb_var('TOPDIR')
+ targetvendor = get_bb_var('TARGET_VENDOR')
+ self.write_config("""
+TMPDIR = \"${TOPDIR}/tmp-sstatesamehash\"
+BUILD_ARCH = \"x86_64\"
+BUILD_OS = \"linux\"
+""")
+ self.track_for_cleanup(topdir + "/tmp-sstatesamehash")
+ bitbake("core-image-sato -S none")
+ self.write_config("""
+TMPDIR = \"${TOPDIR}/tmp-sstatesamehash2\"
+BUILD_ARCH = \"i686\"
+BUILD_OS = \"linux\"
+""")
+ self.track_for_cleanup(topdir + "/tmp-sstatesamehash2")
+ bitbake("core-image-sato -S none")
+
+ def get_files(d):
+ f = []
+ for root, dirs, files in os.walk(d):
+ f.extend(os.path.join(root, name) for name in files)
+ return f
+ files1 = get_files(topdir + "/tmp-sstatesamehash/stamps/")
+ files2 = get_files(topdir + "/tmp-sstatesamehash2/stamps/")
+ files2 = [x.replace("tmp-sstatesamehash2", "tmp-sstatesamehash").replace("i686-linux", "x86_64-linux").replace("i686" + targetvendor + "-linux", "x86_64" + targetvendor + "-linux", ) for x in files2]
+ self.assertItemsEqual(files1, files2)
+
+
+ @testcase(1271)
+ def test_sstate_nativelsbstring_same_hash(self):
+ """
+ The sstate checksums should be independent of whichever NATIVELSBSTRING is
+ detected. Rather than requiring two different build machines and running
+ builds, override the variables manually and check using bitbake -S.
+ """
+
+ topdir = get_bb_var('TOPDIR')
+ self.write_config("""
+TMPDIR = \"${TOPDIR}/tmp-sstatesamehash\"
+NATIVELSBSTRING = \"DistroA\"
+""")
+ self.track_for_cleanup(topdir + "/tmp-sstatesamehash")
+ bitbake("core-image-sato -S none")
+ self.write_config("""
+TMPDIR = \"${TOPDIR}/tmp-sstatesamehash2\"
+NATIVELSBSTRING = \"DistroB\"
+""")
+ self.track_for_cleanup(topdir + "/tmp-sstatesamehash2")
+ bitbake("core-image-sato -S none")
+
+ def get_files(d):
+ f = []
+ for root, dirs, files in os.walk(d):
+ f.extend(os.path.join(root, name) for name in files)
+ return f
+ files1 = get_files(topdir + "/tmp-sstatesamehash/stamps/")
+ files2 = get_files(topdir + "/tmp-sstatesamehash2/stamps/")
+ files2 = [x.replace("tmp-sstatesamehash2", "tmp-sstatesamehash") for x in files2]
+ self.assertItemsEqual(files1, files2)
+
+ def test_sstate_allarch_samesigs(self):
+ """
+ The sstate checksums off allarch packages should be independent of whichever
+ MACHINE is set. Check this using bitbake -S.
+ """
+
+ topdir = get_bb_var('TOPDIR')
+ targetos = get_bb_var('TARGET_OS')
+ targetvendor = get_bb_var('TARGET_VENDOR')
+ self.write_config("""
+TMPDIR = \"${TOPDIR}/tmp-sstatesamehash\"
+MACHINE = \"qemux86\"
+""")
+ self.track_for_cleanup(topdir + "/tmp-sstatesamehash")
+ bitbake("world -S none")
+ self.write_config("""
+TMPDIR = \"${TOPDIR}/tmp-sstatesamehash2\"
+MACHINE = \"qemuarm\"
+""")
+ self.track_for_cleanup(topdir + "/tmp-sstatesamehash2")
+ bitbake("world -S none")
+
+ def get_files(d):
+ f = []
+ for root, dirs, files in os.walk(d):
+ for name in files:
+ if "do_build" not in name:
+ f.append(os.path.join(root, name))
+ return f
+ files1 = get_files(topdir + "/tmp-sstatesamehash/stamps/all" + targetvendor + "-" + targetos)
+ files2 = get_files(topdir + "/tmp-sstatesamehash2/stamps/all" + targetvendor + "-" + targetos)
+ files2 = [x.replace("tmp-sstatesamehash2", "tmp-sstatesamehash") for x in files2]
+ self.maxDiff = None
+ self.assertItemsEqual(files1, files2)
diff --git a/meta/lib/oeqa/selftest/wic.py b/meta/lib/oeqa/selftest/wic.py
new file mode 100644
index 0000000..3dc54a4
--- /dev/null
+++ b/meta/lib/oeqa/selftest/wic.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# Copyright (c) 2015, Intel Corporation.
+# All rights reserved.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# AUTHORS
+# Ed Bartosh <ed.bartosh@linux.intel.com>
+
+"""Test cases for wic."""
+
+import os
+import sys
+
+from glob import glob
+from shutil import rmtree
+
+from oeqa.selftest.base import oeSelfTest
+from oeqa.utils.commands import runCmd, bitbake, get_bb_var
+
+class Wic(oeSelfTest):
+ """Wic test class."""
+
+ resultdir = "/var/tmp/wic/build/"
+
+ @classmethod
+ def setUpClass(cls):
+ """Build wic runtime dependencies."""
+ bitbake('syslinux syslinux-native parted-native gptfdisk-native '
+ 'dosfstools-native mtools-native')
+ Wic.image_is_ready = False
+
+ def setUp(self):
+ """This code is executed before each test method."""
+ if not Wic.image_is_ready:
+ # build core-image-minimal with required features
+ features = 'IMAGE_FSTYPES += " hddimg"\nMACHINE_FEATURES_append = " efi"\n'
+ self.append_config(features)
+ bitbake('core-image-minimal')
+ # set this class variable to avoid buiding image many times
+ Wic.image_is_ready = True
+
+ rmtree(self.resultdir, ignore_errors=True)
+
+ def test01_help(self):
+ """Test wic --help"""
+ self.assertEqual(0, runCmd('wic --help').status)
+
+ def test02_createhelp(self):
+ """Test wic create --help"""
+ self.assertEqual(0, runCmd('wic create --help').status)
+
+ def test03_listhelp(self):
+ """Test wic list --help"""
+ self.assertEqual(0, runCmd('wic list --help').status)
+
+ def test04_build_image_name(self):
+ """Test wic create directdisk --image-name core-image-minimal"""
+ self.assertEqual(0, runCmd("wic create directdisk "
+ "--image-name core-image-minimal").status)
+ self.assertEqual(1, len(glob(self.resultdir + "directdisk-*.direct")))
+
+ def test05_build_artifacts(self):
+ """Test wic create directdisk providing all artifacts."""
+ vars = dict((var.lower(), get_bb_var(var, 'core-image-minimal')) \
+ for var in ('STAGING_DATADIR', 'DEPLOY_DIR_IMAGE',
+ 'STAGING_DIR_NATIVE', 'IMAGE_ROOTFS'))
+ status = runCmd("wic create directdisk "
+ "-b %(staging_datadir)s "
+ "-k %(deploy_dir_image)s "
+ "-n %(staging_dir_native)s "
+ "-r %(image_rootfs)s" % vars).status
+ self.assertEqual(0, status)
+ self.assertEqual(1, len(glob(self.resultdir + "directdisk-*.direct")))
+
+ def test06_gpt_image(self):
+ """Test creation of core-image-minimal with gpt table and UUID boot"""
+ self.assertEqual(0, runCmd("wic create directdisk-gpt "
+ "--image-name core-image-minimal").status)
+ self.assertEqual(1, len(glob(self.resultdir + "directdisk-*.direct")))
+
+ def test07_unsupported_subcommand(self):
+ """Test unsupported subcommand"""
+ self.assertEqual(1, runCmd('wic unsupported',
+ ignore_status=True).status)
+
+ def test08_no_command(self):
+ """Test wic without command"""
+ self.assertEqual(1, runCmd('wic', ignore_status=True).status)
+
+ def test09_help_kickstart(self):
+ """Test wic help overview"""
+ self.assertEqual(0, runCmd('wic help overview').status)
+
+ def test10_help_plugins(self):
+ """Test wic help plugins"""
+ self.assertEqual(0, runCmd('wic help plugins').status)
+
+ def test11_help_kickstart(self):
+ """Test wic help kickstart"""
+ self.assertEqual(0, runCmd('wic help kickstart').status)
+
+ def test12_compress_gzip(self):
+ """Test compressing an image with gzip"""
+ self.assertEqual(0, runCmd("wic create directdisk "
+ "--image-name core-image-minimal "
+ "-c gzip").status)
+ self.assertEqual(1, len(glob(self.resultdir + \
+ "directdisk-*.direct.gz")))
+
+ def test13_compress_gzip(self):
+ """Test compressing an image with bzip2"""
+ self.assertEqual(0, runCmd("wic create directdisk "
+ "--image-name core-image-minimal "
+ "-c bzip2").status)
+ self.assertEqual(1, len(glob(self.resultdir + \
+ "directdisk-*.direct.bz2")))
+
+ def test14_compress_gzip(self):
+ """Test compressing an image with xz"""
+ self.assertEqual(0, runCmd("wic create directdisk "
+ "--image-name core-image-minimal "
+ "-c xz").status)
+ self.assertEqual(1, len(glob(self.resultdir + \
+ "directdisk-*.direct.xz")))
+
+ def test15_wrong_compressor(self):
+ """Test how wic breaks if wrong compressor is provided"""
+ self.assertEqual(2, runCmd("wic create directdisk "
+ "--image-name core-image-minimal "
+ "-c wrong", ignore_status=True).status)
+
+ def test16_rootfs_indirect_recipes(self):
+ """Test usage of rootfs plugin with rootfs recipes"""
+ wks = "directdisk-multi-rootfs"
+ self.assertEqual(0, runCmd("wic create %s "
+ "--image-name core-image-minimal "
+ "--rootfs rootfs1=core-image-minimal "
+ "--rootfs rootfs2=core-image-minimal" \
+ % wks).status)
+ self.assertEqual(1, len(glob(self.resultdir + "%s*.direct" % wks)))
+
+ def test17_rootfs_artifacts(self):
+ """Test usage of rootfs plugin with rootfs paths"""
+ vars = dict((var.lower(), get_bb_var(var, 'core-image-minimal')) \
+ for var in ('STAGING_DATADIR', 'DEPLOY_DIR_IMAGE',
+ 'STAGING_DIR_NATIVE', 'IMAGE_ROOTFS'))
+ vars['wks'] = "directdisk-multi-rootfs"
+ status = runCmd("wic create %(wks)s "
+ "-b %(staging_datadir)s "
+ "-k %(deploy_dir_image)s "
+ "-n %(staging_dir_native)s "
+ "--rootfs-dir rootfs1=%(image_rootfs)s "
+ "--rootfs-dir rootfs2=%(image_rootfs)s" \
+ % vars).status
+ self.assertEqual(0, status)
+ self.assertEqual(1, len(glob(self.resultdir + \
+ "%(wks)s-*.direct" % vars)))
+
+ def test18_iso_image(self):
+ """Test creation of hybrid iso imagewith legacy and EFI boot"""
+ self.assertEqual(0, runCmd("wic create mkhybridiso "
+ "--image-name core-image-minimal").status)
+ self.assertEqual(1, len(glob(self.resultdir + "HYBRID_ISO_IMG-*.direct")))
+ self.assertEqual(1, len(glob(self.resultdir + "HYBRID_ISO_IMG-*.iso")))
+
+ def test19_image_env(self):
+ """Test generation of <image>.env files."""
+ image = 'core-image-minimal'
+ stdir = get_bb_var('STAGING_DIR_TARGET', image)
+ imgdatadir = os.path.join(stdir, 'imgdata')
+
+ basename = get_bb_var('IMAGE_BASENAME', image)
+ self.assertEqual(basename, image)
+ path = os.path.join(imgdatadir, basename) + '.env'
+ self.assertTrue(os.path.isfile(path))
+
+ wicvars = set(get_bb_var('WICVARS', image).split())
+ # filter out optional variables
+ wicvars = wicvars.difference(('HDDDIR', 'IMAGE_BOOT_FILES',
+ 'INITRD', 'ISODIR'))
+ with open(path) as envfile:
+ content = dict(line.split("=", 1) for line in envfile)
+ # test if variables used by wic present in the .env file
+ for var in wicvars:
+ self.assertTrue(var in content, "%s is not in .env file" % var)
+ self.assertTrue(content[var])
+
+ def test20_wic_image_type(self):
+ """Test building wic images by bitbake"""
+ self.assertEqual(0, bitbake('wic-image-minimal').status)
+
+ deploy_dir = get_bb_var('DEPLOY_DIR_IMAGE')
+ machine = get_bb_var('MACHINE')
+ prefix = os.path.join(deploy_dir, 'wic-image-minimal-%s.' % machine)
+ # check if we have result image and manifests symlinks
+ # pointing to existing files
+ for suffix in ('wic.bz2', 'manifest'):
+ path = prefix + suffix
+ self.assertTrue(os.path.islink(path))
+ self.assertTrue(os.path.isfile(os.path.realpath(path)))
+
+ def test21_qemux86_directdisk(self):
+ """Test creation of qemux-86-directdisk image"""
+ image = "qemux86-directdisk"
+ self.assertEqual(0, runCmd("wic create %s -e core-image-minimal" \
+ % image).status)
+ self.assertEqual(1, len(glob(self.resultdir + "%s-*direct" % image)))
+
+ def test22_mkgummidisk(self):
+ """Test creation of mkgummidisk image"""
+ image = "mkgummidisk"
+ self.assertEqual(0, runCmd("wic create %s -e core-image-minimal" \
+ % image).status)
+ self.assertEqual(1, len(glob(self.resultdir + "%s-*direct" % image)))
+
+ def test23_mkefidisk(self):
+ """Test creation of mkefidisk image"""
+ image = "mkefidisk"
+ self.assertEqual(0, runCmd("wic create %s -e core-image-minimal" \
+ % image).status)
+ self.assertEqual(1, len(glob(self.resultdir + "%s-*direct" % image)))
diff --git a/meta/lib/oeqa/targetcontrol.py b/meta/lib/oeqa/targetcontrol.py
new file mode 100644
index 0000000..edc0d01
--- /dev/null
+++ b/meta/lib/oeqa/targetcontrol.py
@@ -0,0 +1,240 @@
+# Copyright (C) 2013 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# This module is used by testimage.bbclass for setting up and controlling a target machine.
+
+import os
+import shutil
+import subprocess
+import bb
+import traceback
+import sys
+import logging
+from oeqa.utils.sshcontrol import SSHControl
+from oeqa.utils.qemurunner import QemuRunner
+from oeqa.utils.qemutinyrunner import QemuTinyRunner
+from oeqa.utils.dump import TargetDumper
+from oeqa.controllers.testtargetloader import TestTargetLoader
+from abc import ABCMeta, abstractmethod
+
+def get_target_controller(d):
+ testtarget = d.getVar("TEST_TARGET", True)
+ # old, simple names
+ if testtarget == "qemu":
+ return QemuTarget(d)
+ elif testtarget == "simpleremote":
+ return SimpleRemoteTarget(d)
+ else:
+ # use the class name
+ try:
+ # is it a core class defined here?
+ controller = getattr(sys.modules[__name__], testtarget)
+ except AttributeError:
+ # nope, perhaps a layer defined one
+ try:
+ bbpath = d.getVar("BBPATH", True).split(':')
+ testtargetloader = TestTargetLoader()
+ controller = testtargetloader.get_controller_module(testtarget, bbpath)
+ except ImportError as e:
+ bb.fatal("Failed to import {0} from available controller modules:\n{1}".format(testtarget,traceback.format_exc()))
+ except AttributeError as e:
+ bb.fatal("Invalid TEST_TARGET - " + str(e))
+ return controller(d)
+
+
+class BaseTarget(object):
+
+ __metaclass__ = ABCMeta
+
+ supported_image_fstypes = []
+
+ def __init__(self, d):
+ self.connection = None
+ self.ip = None
+ self.server_ip = None
+ self.datetime = d.getVar('DATETIME', True)
+ self.testdir = d.getVar("TEST_LOG_DIR", True)
+ self.pn = d.getVar("PN", True)
+
+ @abstractmethod
+ def deploy(self):
+
+ self.sshlog = os.path.join(self.testdir, "ssh_target_log.%s" % self.datetime)
+ sshloglink = os.path.join(self.testdir, "ssh_target_log")
+ if os.path.islink(sshloglink):
+ os.unlink(sshloglink)
+ os.symlink(self.sshlog, sshloglink)
+ bb.note("SSH log file: %s" % self.sshlog)
+
+ @abstractmethod
+ def start(self, params=None):
+ pass
+
+ @abstractmethod
+ def stop(self):
+ pass
+
+ @classmethod
+ def get_extra_files(self):
+ return None
+
+ @classmethod
+ def match_image_fstype(self, d, image_fstypes=None):
+ if not image_fstypes:
+ image_fstypes = d.getVar('IMAGE_FSTYPES', True).split(' ')
+ possible_image_fstypes = [fstype for fstype in self.supported_image_fstypes if fstype in image_fstypes]
+ if possible_image_fstypes:
+ return possible_image_fstypes[0]
+ else:
+ return None
+
+ def get_image_fstype(self, d):
+ image_fstype = self.match_image_fstype(d)
+ if image_fstype:
+ return image_fstype
+ else:
+ bb.fatal("IMAGE_FSTYPES should contain a Target Controller supported image fstype: %s " % ', '.join(map(str, self.supported_image_fstypes)))
+
+ def restart(self, params=None):
+ self.stop()
+ self.start(params)
+
+ def run(self, cmd, timeout=None):
+ return self.connection.run(cmd, timeout)
+
+ def copy_to(self, localpath, remotepath):
+ return self.connection.copy_to(localpath, remotepath)
+
+ def copy_from(self, remotepath, localpath):
+ return self.connection.copy_from(remotepath, localpath)
+
+
+
+class QemuTarget(BaseTarget):
+
+ supported_image_fstypes = ['ext3', 'ext4', 'cpio.gz']
+
+ def __init__(self, d):
+
+ super(QemuTarget, self).__init__(d)
+
+ self.image_fstype = self.get_image_fstype(d)
+ self.qemulog = os.path.join(self.testdir, "qemu_boot_log.%s" % self.datetime)
+ self.origrootfs = os.path.join(d.getVar("DEPLOY_DIR_IMAGE", True), d.getVar("IMAGE_LINK_NAME", True) + '.' + self.image_fstype)
+ self.rootfs = os.path.join(self.testdir, d.getVar("IMAGE_LINK_NAME", True) + '-testimage.' + self.image_fstype)
+ self.kernel = os.path.join(d.getVar("DEPLOY_DIR_IMAGE", True), d.getVar("KERNEL_IMAGETYPE", False) + '-' + d.getVar('MACHINE', False) + '.bin')
+ dump_target_cmds = d.getVar("testimage_dump_target", True)
+ dump_host_cmds = d.getVar("testimage_dump_host", True)
+ dump_dir = d.getVar("TESTIMAGE_DUMP_DIR", True)
+
+ # Log QemuRunner log output to a file
+ import oe.path
+ bb.utils.mkdirhier(self.testdir)
+ self.qemurunnerlog = os.path.join(self.testdir, 'qemurunner_log.%s' % self.datetime)
+ logger = logging.getLogger('BitBake.QemuRunner')
+ loggerhandler = logging.FileHandler(self.qemurunnerlog)
+ loggerhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
+ logger.addHandler(loggerhandler)
+ oe.path.symlink(os.path.basename(self.qemurunnerlog), os.path.join(self.testdir, 'qemurunner_log'), force=True)
+
+ if d.getVar("DISTRO", True) == "poky-tiny":
+ self.runner = QemuTinyRunner(machine=d.getVar("MACHINE", True),
+ rootfs=self.rootfs,
+ tmpdir = d.getVar("TMPDIR", True),
+ deploy_dir_image = d.getVar("DEPLOY_DIR_IMAGE", True),
+ display = d.getVar("BB_ORIGENV", False).getVar("DISPLAY", True),
+ logfile = self.qemulog,
+ kernel = self.kernel,
+ boottime = int(d.getVar("TEST_QEMUBOOT_TIMEOUT", True)))
+ else:
+ self.runner = QemuRunner(machine=d.getVar("MACHINE", True),
+ rootfs=self.rootfs,
+ tmpdir = d.getVar("TMPDIR", True),
+ deploy_dir_image = d.getVar("DEPLOY_DIR_IMAGE", True),
+ display = d.getVar("BB_ORIGENV", False).getVar("DISPLAY", True),
+ logfile = self.qemulog,
+ boottime = int(d.getVar("TEST_QEMUBOOT_TIMEOUT", True)),
+ dump_dir = dump_dir,
+ dump_host_cmds = d.getVar("testimage_dump_host", True))
+
+ self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner)
+
+ def deploy(self):
+ try:
+ bb.utils.mkdirhier(self.testdir)
+ shutil.copyfile(self.origrootfs, self.rootfs)
+ except Exception as e:
+ bb.fatal("Error copying rootfs: %s" % e)
+
+ qemuloglink = os.path.join(self.testdir, "qemu_boot_log")
+ if os.path.islink(qemuloglink):
+ os.unlink(qemuloglink)
+ os.symlink(self.qemulog, qemuloglink)
+
+ bb.note("rootfs file: %s" % self.rootfs)
+ bb.note("Qemu log file: %s" % self.qemulog)
+ super(QemuTarget, self).deploy()
+
+ def start(self, params=None):
+ if self.runner.start(params):
+ self.ip = self.runner.ip
+ self.server_ip = self.runner.server_ip
+ self.connection = SSHControl(ip=self.ip, logfile=self.sshlog)
+ else:
+ self.stop()
+ if os.path.exists(self.qemulog):
+ with open(self.qemulog, 'r') as f:
+ bb.error("Qemu log output from %s:\n%s" % (self.qemulog, f.read()))
+ raise bb.build.FuncFailed("%s - FAILED to start qemu - check the task log and the boot log" % self.pn)
+
+ def check(self):
+ return self.runner.is_alive()
+
+ def stop(self):
+ self.runner.stop()
+ self.connection = None
+ self.ip = None
+ self.server_ip = None
+
+ def restart(self, params=None):
+ if self.runner.restart(params):
+ self.ip = self.runner.ip
+ self.server_ip = self.runner.server_ip
+ self.connection = SSHControl(ip=self.ip, logfile=self.sshlog)
+ else:
+ raise bb.build.FuncFailed("%s - FAILED to re-start qemu - check the task log and the boot log" % self.pn)
+
+ def run_serial(self, command):
+ return self.runner.run_serial(command)
+
+
+class SimpleRemoteTarget(BaseTarget):
+
+ def __init__(self, d):
+ super(SimpleRemoteTarget, self).__init__(d)
+ addr = d.getVar("TEST_TARGET_IP", True) or bb.fatal('Please set TEST_TARGET_IP with the IP address of the machine you want to run the tests on.')
+ self.ip = addr.split(":")[0]
+ try:
+ self.port = addr.split(":")[1]
+ except IndexError:
+ self.port = None
+ bb.note("Target IP: %s" % self.ip)
+ self.server_ip = d.getVar("TEST_SERVER_IP", True)
+ if not self.server_ip:
+ try:
+ self.server_ip = subprocess.check_output(['ip', 'route', 'get', self.ip ]).split("\n")[0].split()[-1]
+ except Exception as e:
+ bb.fatal("Failed to determine the host IP address (alternatively you can set TEST_SERVER_IP with the IP address of this machine): %s" % e)
+ bb.note("Server IP: %s" % self.server_ip)
+
+ def deploy(self):
+ super(SimpleRemoteTarget, self).deploy()
+
+ def start(self, params=None):
+ self.connection = SSHControl(self.ip, logfile=self.sshlog, port=self.port)
+
+ def stop(self):
+ self.connection = None
+ self.ip = None
+ self.server_ip = None
diff --git a/meta/lib/oeqa/utils/__init__.py b/meta/lib/oeqa/utils/__init__.py
new file mode 100644
index 0000000..2260046
--- /dev/null
+++ b/meta/lib/oeqa/utils/__init__.py
@@ -0,0 +1,15 @@
+# Enable other layers to have modules in the same named directory
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
+
+
+# Borrowed from CalledProcessError
+
+class CommandError(Exception):
+ def __init__(self, retcode, cmd, output = None):
+ self.retcode = retcode
+ self.cmd = cmd
+ self.output = output
+ def __str__(self):
+ return "Command '%s' returned non-zero exit status %d with output: %s" % (self.cmd, self.retcode, self.output)
+
diff --git a/meta/lib/oeqa/utils/commands.py b/meta/lib/oeqa/utils/commands.py
new file mode 100644
index 0000000..08e2cbb
--- /dev/null
+++ b/meta/lib/oeqa/utils/commands.py
@@ -0,0 +1,224 @@
+# Copyright (c) 2013-2014 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# DESCRIPTION
+# This module is mainly used by scripts/oe-selftest and modules under meta/oeqa/selftest
+# It provides a class and methods for running commands on the host in a convienent way for tests.
+
+
+
+import os
+import sys
+import signal
+import subprocess
+import threading
+import logging
+from oeqa.utils import CommandError
+from oeqa.utils import ftools
+import re
+import contextlib
+
+class Command(object):
+ def __init__(self, command, bg=False, timeout=None, data=None, **options):
+
+ self.defaultopts = {
+ "stdout": subprocess.PIPE,
+ "stderr": subprocess.STDOUT,
+ "stdin": None,
+ "shell": False,
+ "bufsize": -1,
+ }
+
+ self.cmd = command
+ self.bg = bg
+ self.timeout = timeout
+ self.data = data
+
+ self.options = dict(self.defaultopts)
+ if isinstance(self.cmd, basestring):
+ self.options["shell"] = True
+ if self.data:
+ self.options['stdin'] = subprocess.PIPE
+ self.options.update(options)
+
+ self.status = None
+ self.output = None
+ self.error = None
+ self.thread = None
+
+ self.log = logging.getLogger("utils.commands")
+
+ def run(self):
+ self.process = subprocess.Popen(self.cmd, **self.options)
+
+ def commThread():
+ self.output, self.error = self.process.communicate(self.data)
+
+ self.thread = threading.Thread(target=commThread)
+ self.thread.start()
+
+ self.log.debug("Running command '%s'" % self.cmd)
+
+ if not self.bg:
+ self.thread.join(self.timeout)
+ self.stop()
+
+ def stop(self):
+ if self.thread.isAlive():
+ self.process.terminate()
+ # let's give it more time to terminate gracefully before killing it
+ self.thread.join(5)
+ if self.thread.isAlive():
+ self.process.kill()
+ self.thread.join()
+
+ self.output = self.output.rstrip()
+ self.status = self.process.poll()
+
+ self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status))
+ # logging the complete output is insane
+ # bitbake -e output is really big
+ # and makes the log file useless
+ if self.status:
+ lout = "\n".join(self.output.splitlines()[-20:])
+ self.log.debug("Last 20 lines:\n%s" % lout)
+
+
+class Result(object):
+ pass
+
+
+def runCmd(command, ignore_status=False, timeout=None, assert_error=True, **options):
+ result = Result()
+
+ cmd = Command(command, timeout=timeout, **options)
+ cmd.run()
+
+ result.command = command
+ result.status = cmd.status
+ result.output = cmd.output
+ result.pid = cmd.process.pid
+
+ if result.status and not ignore_status:
+ if assert_error:
+ raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, result.output))
+ else:
+ raise CommandError(result.status, command, result.output)
+
+ return result
+
+
+def bitbake(command, ignore_status=False, timeout=None, postconfig=None, **options):
+
+ if postconfig:
+ postconfig_file = os.path.join(os.environ.get('BUILDDIR'), 'oeqa-post.conf')
+ ftools.write_file(postconfig_file, postconfig)
+ extra_args = "-R %s" % postconfig_file
+ else:
+ extra_args = ""
+
+ if isinstance(command, basestring):
+ cmd = "bitbake " + extra_args + " " + command
+ else:
+ cmd = [ "bitbake" ] + [a for a in (command + extra_args.split(" ")) if a not in [""]]
+
+ try:
+ return runCmd(cmd, ignore_status, timeout, **options)
+ finally:
+ if postconfig:
+ os.remove(postconfig_file)
+
+
+def get_bb_env(target=None, postconfig=None):
+ if target:
+ return bitbake("-e %s" % target, postconfig=postconfig).output
+ else:
+ return bitbake("-e", postconfig=postconfig).output
+
+def get_bb_var(var, target=None, postconfig=None):
+ val = None
+ bbenv = get_bb_env(target, postconfig=postconfig)
+ lastline = None
+ for line in bbenv.splitlines():
+ if re.search("^(export )?%s=" % var, line):
+ val = line.split('=', 1)[1]
+ val = val.strip('\"')
+ break
+ elif re.match("unset %s$" % var, line):
+ # Handle [unexport] variables
+ if lastline.startswith('# "'):
+ val = lastline.split('\"')[1]
+ break
+ lastline = line
+ return val
+
+def get_test_layer():
+ layers = get_bb_var("BBLAYERS").split()
+ testlayer = None
+ for l in layers:
+ if '~' in l:
+ l = os.path.expanduser(l)
+ if "/meta-selftest" in l and os.path.isdir(l):
+ testlayer = l
+ break
+ return testlayer
+
+def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'):
+ os.makedirs(os.path.join(templayerdir, 'conf'))
+ with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
+ f.write('BBPATH .= ":${LAYERDIR}"\n')
+ f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec)
+ f.write(' ${LAYERDIR}/%s/*.bbappend"\n' % recipepathspec)
+ f.write('BBFILE_COLLECTIONS += "%s"\n' % templayername)
+ f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername)
+ f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority))
+ f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername)
+
+
+@contextlib.contextmanager
+def runqemu(pn, test):
+
+ import bb.tinfoil
+ import bb.build
+
+ tinfoil = bb.tinfoil.Tinfoil()
+ tinfoil.prepare(False)
+ try:
+ tinfoil.logger.setLevel(logging.WARNING)
+ import oeqa.targetcontrol
+ tinfoil.config_data.setVar("TEST_LOG_DIR", "${WORKDIR}/testimage")
+ tinfoil.config_data.setVar("TEST_QEMUBOOT_TIMEOUT", "1000")
+ import oe.recipeutils
+ recipefile = oe.recipeutils.pn_to_recipe(tinfoil.cooker, pn)
+ recipedata = oe.recipeutils.parse_recipe(recipefile, [], tinfoil.config_data)
+
+ # The QemuRunner log is saved out, but we need to ensure it is at the right
+ # log level (and then ensure that since it's a child of the BitBake logger,
+ # we disable propagation so we don't then see the log events on the console)
+ logger = logging.getLogger('BitBake.QemuRunner')
+ logger.setLevel(logging.DEBUG)
+ logger.propagate = False
+ logdir = recipedata.getVar("TEST_LOG_DIR", True)
+
+ qemu = oeqa.targetcontrol.QemuTarget(recipedata)
+ finally:
+ # We need to shut down tinfoil early here in case we actually want
+ # to run tinfoil-using utilities with the running QEMU instance.
+ # Luckily QemuTarget doesn't need it after the constructor.
+ tinfoil.shutdown()
+
+ try:
+ qemu.deploy()
+ try:
+ qemu.start()
+ except bb.build.FuncFailed:
+ raise Exception('Failed to start QEMU - see the logs in %s' % logdir)
+
+ yield qemu
+
+ finally:
+ try:
+ qemu.stop()
+ except:
+ pass
diff --git a/meta/lib/oeqa/utils/decorators.py b/meta/lib/oeqa/utils/decorators.py
new file mode 100644
index 0000000..162a88f
--- /dev/null
+++ b/meta/lib/oeqa/utils/decorators.py
@@ -0,0 +1,222 @@
+# Copyright (C) 2013 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# Some custom decorators that can be used by unittests
+# Most useful is skipUnlessPassed which can be used for
+# creating dependecies between two test methods.
+
+import os
+import logging
+import sys
+import unittest
+import threading
+import signal
+from functools import wraps
+
+#get the "result" object from one of the upper frames provided that one of these upper frames is a unittest.case frame
+class getResults(object):
+ def __init__(self):
+ #dynamically determine the unittest.case frame and use it to get the name of the test method
+ ident = threading.current_thread().ident
+ upperf = sys._current_frames()[ident]
+ while (upperf.f_globals['__name__'] != 'unittest.case'):
+ upperf = upperf.f_back
+
+ def handleList(items):
+ ret = []
+ # items is a list of tuples, (test, failure) or (_ErrorHandler(), Exception())
+ for i in items:
+ s = i[0].id()
+ #Handle the _ErrorHolder objects from skipModule failures
+ if "setUpModule (" in s:
+ ret.append(s.replace("setUpModule (", "").replace(")",""))
+ else:
+ ret.append(s)
+ return ret
+ self.faillist = handleList(upperf.f_locals['result'].failures)
+ self.errorlist = handleList(upperf.f_locals['result'].errors)
+ self.skiplist = handleList(upperf.f_locals['result'].skipped)
+
+ def getFailList(self):
+ return self.faillist
+
+ def getErrorList(self):
+ return self.errorlist
+
+ def getSkipList(self):
+ return self.skiplist
+
+class skipIfFailure(object):
+
+ def __init__(self,testcase):
+ self.testcase = testcase
+
+ def __call__(self,f):
+ def wrapped_f(*args):
+ res = getResults()
+ if self.testcase in (res.getFailList() or res.getErrorList()):
+ raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase)
+ return f(*args)
+ wrapped_f.__name__ = f.__name__
+ return wrapped_f
+
+class skipIfSkipped(object):
+
+ def __init__(self,testcase):
+ self.testcase = testcase
+
+ def __call__(self,f):
+ def wrapped_f(*args):
+ res = getResults()
+ if self.testcase in res.getSkipList():
+ raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase)
+ return f(*args)
+ wrapped_f.__name__ = f.__name__
+ return wrapped_f
+
+class skipUnlessPassed(object):
+
+ def __init__(self,testcase):
+ self.testcase = testcase
+
+ def __call__(self,f):
+ def wrapped_f(*args):
+ res = getResults()
+ if self.testcase in res.getSkipList() or \
+ self.testcase in res.getFailList() or \
+ self.testcase in res.getErrorList():
+ raise unittest.SkipTest("Testcase dependency not met: %s" % self.testcase)
+ return f(*args)
+ wrapped_f.__name__ = f.__name__
+ wrapped_f._depends_on = self.testcase
+ return wrapped_f
+
+class testcase(object):
+
+ def __init__(self, test_case):
+ self.test_case = test_case
+
+ def __call__(self, func):
+ def wrapped_f(*args):
+ return func(*args)
+ wrapped_f.test_case = self.test_case
+ wrapped_f.__name__ = func.__name__
+ return wrapped_f
+
+class NoParsingFilter(logging.Filter):
+ def filter(self, record):
+ return record.levelno == 100
+
+def LogResults(original_class):
+ orig_method = original_class.run
+
+ #rewrite the run method of unittest.TestCase to add testcase logging
+ def run(self, result, *args, **kws):
+ orig_method(self, result, *args, **kws)
+ passed = True
+ testMethod = getattr(self, self._testMethodName)
+ #if test case is decorated then use it's number, else use it's name
+ try:
+ test_case = testMethod.test_case
+ except AttributeError:
+ test_case = self._testMethodName
+
+ class_name = str(testMethod.im_class).split("'")[1]
+
+ #create custom logging level for filtering.
+ custom_log_level = 100
+ logging.addLevelName(custom_log_level, 'RESULTS')
+ caller = os.path.basename(sys.argv[0])
+
+ def results(self, message, *args, **kws):
+ if self.isEnabledFor(custom_log_level):
+ self.log(custom_log_level, message, *args, **kws)
+ logging.Logger.results = results
+
+ logging.basicConfig(filename=os.path.join(os.getcwd(),'results-'+caller+'.log'),
+ filemode='w',
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ datefmt='%H:%M:%S',
+ level=custom_log_level)
+ for handler in logging.root.handlers:
+ handler.addFilter(NoParsingFilter())
+ local_log = logging.getLogger(caller)
+
+ #check status of tests and record it
+
+ for (name, msg) in result.errors:
+ if (self._testMethodName == str(name).split(' ')[0]) and (class_name in str(name).split(' ')[1]):
+ local_log.results("Testcase "+str(test_case)+": ERROR")
+ local_log.results("Testcase "+str(test_case)+":\n"+msg)
+ passed = False
+ for (name, msg) in result.failures:
+ if (self._testMethodName == str(name).split(' ')[0]) and (class_name in str(name).split(' ')[1]):
+ local_log.results("Testcase "+str(test_case)+": FAILED")
+ local_log.results("Testcase "+str(test_case)+":\n"+msg)
+ passed = False
+ for (name, msg) in result.skipped:
+ if (self._testMethodName == str(name).split(' ')[0]) and (class_name in str(name).split(' ')[1]):
+ local_log.results("Testcase "+str(test_case)+": SKIPPED")
+ passed = False
+ if passed:
+ local_log.results("Testcase "+str(test_case)+": PASSED")
+
+ original_class.run = run
+ return original_class
+
+class TimeOut(BaseException):
+ pass
+
+def timeout(seconds):
+ def decorator(fn):
+ if hasattr(signal, 'alarm'):
+ @wraps(fn)
+ def wrapped_f(*args, **kw):
+ current_frame = sys._getframe()
+ def raiseTimeOut(signal, frame):
+ if frame is not current_frame:
+ raise TimeOut('%s seconds' % seconds)
+ prev_handler = signal.signal(signal.SIGALRM, raiseTimeOut)
+ try:
+ signal.alarm(seconds)
+ return fn(*args, **kw)
+ finally:
+ signal.alarm(0)
+ signal.signal(signal.SIGALRM, prev_handler)
+ return wrapped_f
+ else:
+ return fn
+ return decorator
+
+__tag_prefix = "tag__"
+def tag(*args, **kwargs):
+ """Decorator that adds attributes to classes or functions
+ for use with the Attribute (-a) plugin.
+ """
+ def wrap_ob(ob):
+ for name in args:
+ setattr(ob, __tag_prefix + name, True)
+ for name, value in kwargs.iteritems():
+ setattr(ob, __tag_prefix + name, value)
+ return ob
+ return wrap_ob
+
+def gettag(obj, key, default=None):
+ key = __tag_prefix + key
+ if not isinstance(obj, unittest.TestCase):
+ return getattr(obj, key, default)
+ tc_method = getattr(obj, obj._testMethodName)
+ ret = getattr(tc_method, key, getattr(obj, key, default))
+ return ret
+
+def getAllTags(obj):
+ def __gettags(o):
+ r = {k[len(__tag_prefix):]:getattr(o,k) for k in dir(o) if k.startswith(__tag_prefix)}
+ return r
+ if not isinstance(obj, unittest.TestCase):
+ return __gettags(obj)
+ tc_method = getattr(obj, obj._testMethodName)
+ ret = __gettags(obj)
+ ret.update(__gettags(tc_method))
+ return ret
diff --git a/meta/lib/oeqa/utils/dump.py b/meta/lib/oeqa/utils/dump.py
new file mode 100644
index 0000000..4ae871c
--- /dev/null
+++ b/meta/lib/oeqa/utils/dump.py
@@ -0,0 +1,87 @@
+import os
+import sys
+import errno
+import datetime
+import itertools
+from commands import runCmd
+
+def get_host_dumper(d):
+ cmds = d.getVar("testimage_dump_host", True)
+ parent_dir = d.getVar("TESTIMAGE_DUMP_DIR", True)
+ return HostDumper(cmds, parent_dir)
+
+
+class BaseDumper(object):
+ """ Base class to dump commands from host/target """
+
+ def __init__(self, cmds, parent_dir):
+ self.cmds = []
+ self.parent_dir = parent_dir
+ if not cmds:
+ return
+ for cmd in cmds.split('\n'):
+ cmd = cmd.lstrip()
+ if not cmd or cmd[0] == '#':
+ continue
+ self.cmds.append(cmd)
+
+ def create_dir(self, dir_suffix):
+ dump_subdir = ("%s_%s" % (
+ datetime.datetime.now().strftime('%Y%m%d%H%M'),
+ dir_suffix))
+ dump_dir = os.path.join(self.parent_dir, dump_subdir)
+ try:
+ os.makedirs(dump_dir)
+ except OSError as err:
+ if err.errno != errno.EEXIST:
+ raise err
+ self.dump_dir = dump_dir
+
+ def _write_dump(self, command, output):
+ if isinstance(self, HostDumper):
+ prefix = "host"
+ elif isinstance(self, TargetDumper):
+ prefix = "target"
+ else:
+ prefix = "unknown"
+ for i in itertools.count():
+ filename = "%s_%02d_%s" % (prefix, i, command)
+ fullname = os.path.join(self.dump_dir, filename)
+ if not os.path.exists(fullname):
+ break
+ with open(fullname, 'w') as dump_file:
+ dump_file.write(output)
+
+
+class HostDumper(BaseDumper):
+ """ Class to get dumps from the host running the tests """
+
+ def __init__(self, cmds, parent_dir):
+ super(HostDumper, self).__init__(cmds, parent_dir)
+
+ def dump_host(self, dump_dir=""):
+ if dump_dir:
+ self.dump_dir = dump_dir
+ for cmd in self.cmds:
+ result = runCmd(cmd, ignore_status=True)
+ self._write_dump(cmd.split()[0], result.output)
+
+
+class TargetDumper(BaseDumper):
+ """ Class to get dumps from target, it only works with QemuRunner """
+
+ def __init__(self, cmds, parent_dir, qemurunner):
+ super(TargetDumper, self).__init__(cmds, parent_dir)
+ self.runner = qemurunner
+
+ def dump_target(self, dump_dir=""):
+ if dump_dir:
+ self.dump_dir = dump_dir
+ for cmd in self.cmds:
+ # We can continue with the testing if serial commands fail
+ try:
+ (status, output) = self.runner.run_serial(cmd)
+ self._write_dump(cmd.split()[0], output)
+ except:
+ print("Tried to dump info from target but "
+ "serial console failed")
diff --git a/meta/lib/oeqa/utils/ftools.py b/meta/lib/oeqa/utils/ftools.py
new file mode 100644
index 0000000..64ebe3d
--- /dev/null
+++ b/meta/lib/oeqa/utils/ftools.py
@@ -0,0 +1,27 @@
+import os
+import re
+
+def write_file(path, data):
+ wdata = data.rstrip() + "\n"
+ with open(path, "w") as f:
+ f.write(wdata)
+
+def append_file(path, data):
+ wdata = data.rstrip() + "\n"
+ with open(path, "a") as f:
+ f.write(wdata)
+
+def read_file(path):
+ data = None
+ with open(path) as f:
+ data = f.read()
+ return data
+
+def remove_from_file(path, data):
+ lines = read_file(path).splitlines()
+ rmdata = data.strip().splitlines()
+ for l in rmdata:
+ for c in range(0, lines.count(l)):
+ i = lines.index(l)
+ del(lines[i])
+ write_file(path, "\n".join(lines))
diff --git a/meta/lib/oeqa/utils/httpserver.py b/meta/lib/oeqa/utils/httpserver.py
new file mode 100644
index 0000000..76518d8
--- /dev/null
+++ b/meta/lib/oeqa/utils/httpserver.py
@@ -0,0 +1,35 @@
+import SimpleHTTPServer
+import multiprocessing
+import os
+
+class HTTPServer(SimpleHTTPServer.BaseHTTPServer.HTTPServer):
+
+ def server_start(self, root_dir):
+ import signal
+ signal.signal(signal.SIGTERM, signal.SIG_DFL)
+ os.chdir(root_dir)
+ self.serve_forever()
+
+class HTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+
+ def log_message(self, format_str, *args):
+ pass
+
+class HTTPService(object):
+
+ def __init__(self, root_dir, host=''):
+ self.root_dir = root_dir
+ self.host = host
+ self.port = 0
+
+ def start(self):
+ self.server = HTTPServer((self.host, self.port), HTTPRequestHandler)
+ if self.port == 0:
+ self.port = self.server.server_port
+ self.process = multiprocessing.Process(target=self.server.server_start, args=[self.root_dir])
+ self.process.start()
+
+ def stop(self):
+ self.server.server_close()
+ self.process.terminate()
+ self.process.join()
diff --git a/meta/lib/oeqa/utils/logparser.py b/meta/lib/oeqa/utils/logparser.py
new file mode 100644
index 0000000..87b5035
--- /dev/null
+++ b/meta/lib/oeqa/utils/logparser.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+
+import sys
+import os
+import re
+import ftools
+
+
+# A parser that can be used to identify weather a line is a test result or a section statement.
+class Lparser(object):
+
+ def __init__(self, test_0_pass_regex, test_0_fail_regex, section_0_begin_regex=None, section_0_end_regex=None, **kwargs):
+ # Initialize the arguments dictionary
+ if kwargs:
+ self.args = kwargs
+ else:
+ self.args = {}
+
+ # Add the default args to the dictionary
+ self.args['test_0_pass_regex'] = test_0_pass_regex
+ self.args['test_0_fail_regex'] = test_0_fail_regex
+ if section_0_begin_regex:
+ self.args['section_0_begin_regex'] = section_0_begin_regex
+ if section_0_end_regex:
+ self.args['section_0_end_regex'] = section_0_end_regex
+
+ self.test_possible_status = ['pass', 'fail', 'error']
+ self.section_possible_status = ['begin', 'end']
+
+ self.initialized = False
+
+
+ # Initialize the parser with the current configuration
+ def init(self):
+
+ # extra arguments can be added by the user to define new test and section categories. They must follow a pre-defined pattern: <type>_<category_name>_<status>_regex
+ self.test_argument_pattern = "^test_(.+?)_(%s)_regex" % '|'.join(map(str, self.test_possible_status))
+ self.section_argument_pattern = "^section_(.+?)_(%s)_regex" % '|'.join(map(str, self.section_possible_status))
+
+ # Initialize the test and section regex dictionaries
+ self.test_regex = {}
+ self.section_regex ={}
+
+ for arg, value in self.args.items():
+ if not value:
+ raise Exception('The value of provided argument %s is %s. Should have a valid value.' % (key, value))
+ is_test = re.search(self.test_argument_pattern, arg)
+ is_section = re.search(self.section_argument_pattern, arg)
+ if is_test:
+ if not is_test.group(1) in self.test_regex:
+ self.test_regex[is_test.group(1)] = {}
+ self.test_regex[is_test.group(1)][is_test.group(2)] = re.compile(value)
+ elif is_section:
+ if not is_section.group(1) in self.section_regex:
+ self.section_regex[is_section.group(1)] = {}
+ self.section_regex[is_section.group(1)][is_section.group(2)] = re.compile(value)
+ else:
+ # TODO: Make these call a traceback instead of a simple exception..
+ raise Exception("The provided argument name does not correspond to any valid type. Please give one of the following types:\nfor tests: %s\nfor sections: %s" % (self.test_argument_pattern, self.section_argument_pattern))
+
+ self.initialized = True
+
+ # Parse a line and return a tuple containing the type of result (test/section) and its category, status and name
+ def parse_line(self, line):
+ if not self.initialized:
+ raise Exception("The parser is not initialized..")
+
+ for test_category, test_status_list in self.test_regex.items():
+ for test_status, status_regex in test_status_list.items():
+ test_name = status_regex.search(line)
+ if test_name:
+ return ['test', test_category, test_status, test_name.group(1)]
+
+ for section_category, section_status_list in self.section_regex.items():
+ for section_status, status_regex in section_status_list.items():
+ section_name = status_regex.search(line)
+ if section_name:
+ return ['section', section_category, section_status, section_name.group(1)]
+ return None
+
+
+class Result(object):
+
+ def __init__(self):
+ self.result_dict = {}
+
+ def store(self, section, test, status):
+ if not section in self.result_dict:
+ self.result_dict[section] = []
+
+ self.result_dict[section].append((test, status))
+
+ # sort tests by the test name(the first element of the tuple), for each section. This can be helpful when using git to diff for changes by making sure they are always in the same order.
+ def sort_tests(self):
+ for package in self.result_dict:
+ sorted_results = sorted(self.result_dict[package], key=lambda tup: tup[0])
+ self.result_dict[package] = sorted_results
+
+ # Log the results as files. The file name is the section name and the contents are the tests in that section.
+ def log_as_files(self, target_dir, test_status):
+ status_regex = re.compile('|'.join(map(str, test_status)))
+ if not type(test_status) == type([]):
+ raise Exception("test_status should be a list. Got " + str(test_status) + " instead.")
+ if not os.path.exists(target_dir):
+ raise Exception("Target directory does not exist: %s" % target_dir)
+
+ for section, test_results in self.result_dict.items():
+ prefix = ''
+ for x in test_status:
+ prefix +=x+'.'
+ if (section != ''):
+ prefix += section
+ section_file = os.path.join(target_dir, prefix)
+ # purge the file contents if it exists
+ open(section_file, 'w').close()
+ for test_result in test_results:
+ (test_name, status) = test_result
+ # we log only the tests with status in the test_status list
+ match_status = status_regex.search(status)
+ if match_status:
+ ftools.append_file(section_file, status + ": " + test_name)
+
+ # Not yet implemented!
+ def log_to_lava(self):
+ pass
diff --git a/meta/lib/oeqa/utils/qemurunner.py b/meta/lib/oeqa/utils/qemurunner.py
new file mode 100644
index 0000000..d32c9db
--- /dev/null
+++ b/meta/lib/oeqa/utils/qemurunner.py
@@ -0,0 +1,519 @@
+# Copyright (C) 2013 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# This module provides a class for starting qemu images using runqemu.
+# It's used by testimage.bbclass.
+
+import subprocess
+import os
+import time
+import signal
+import re
+import socket
+import select
+import errno
+import threading
+from oeqa.utils.dump import HostDumper
+
+import logging
+logger = logging.getLogger("BitBake.QemuRunner")
+
+class QemuRunner:
+
+ def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, boottime, dump_dir, dump_host_cmds):
+
+ # Popen object for runqemu
+ self.runqemu = None
+ # pid of the qemu process that runqemu will start
+ self.qemupid = None
+ # target ip - from the command line
+ self.ip = None
+ # host ip - where qemu is running
+ self.server_ip = None
+
+ self.machine = machine
+ self.rootfs = rootfs
+ self.display = display
+ self.tmpdir = tmpdir
+ self.deploy_dir_image = deploy_dir_image
+ self.logfile = logfile
+ self.boottime = boottime
+ self.logged = False
+ self.thread = None
+
+ self.runqemutime = 60
+ self.host_dumper = HostDumper(dump_host_cmds, dump_dir)
+
+ def create_socket(self):
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setblocking(0)
+ sock.bind(("127.0.0.1",0))
+ sock.listen(2)
+ port = sock.getsockname()[1]
+ logger.info("Created listening socket for qemu serial console on: 127.0.0.1:%s" % port)
+ return (sock, port)
+
+ except socket.error:
+ sock.close()
+ raise
+
+ def log(self, msg):
+ if self.logfile:
+ with open(self.logfile, "a") as f:
+ f.write("%s" % msg)
+
+ def getOutput(self, o):
+ import fcntl
+ fl = fcntl.fcntl(o, fcntl.F_GETFL)
+ fcntl.fcntl(o, fcntl.F_SETFL, fl | os.O_NONBLOCK)
+ return os.read(o.fileno(), 1000000)
+
+
+ def handleSIGCHLD(self, signum, frame):
+ if self.runqemu and self.runqemu.poll():
+ if self.runqemu.returncode:
+ logger.info('runqemu exited with code %d' % self.runqemu.returncode)
+ logger.info("Output from runqemu:\n%s" % self.getOutput(self.runqemu.stdout))
+ self.stop()
+ self._dump_host()
+ raise SystemExit
+
+ def start(self, qemuparams = None):
+ if self.display:
+ os.environ["DISPLAY"] = self.display
+ else:
+ logger.error("To start qemu I need a X desktop, please set DISPLAY correctly (e.g. DISPLAY=:1)")
+ return False
+ if not os.path.exists(self.rootfs):
+ logger.error("Invalid rootfs %s" % self.rootfs)
+ return False
+ if not os.path.exists(self.tmpdir):
+ logger.error("Invalid TMPDIR path %s" % self.tmpdir)
+ return False
+ else:
+ os.environ["OE_TMPDIR"] = self.tmpdir
+ if not os.path.exists(self.deploy_dir_image):
+ logger.error("Invalid DEPLOY_DIR_IMAGE path %s" % self.deploy_dir_image)
+ return False
+ else:
+ os.environ["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
+
+ try:
+ threadsock, threadport = self.create_socket()
+ self.server_socket, self.serverport = self.create_socket()
+ except socket.error, msg:
+ logger.error("Failed to create listening socket: %s" % msg[1])
+ return False
+
+ # Set this flag so that Qemu doesn't do any grabs as SDL grabs interact
+ # badly with screensavers.
+ os.environ["QEMU_DONT_GRAB"] = "1"
+ self.qemuparams = 'bootparams="console=tty1 console=ttyS0,115200n8" qemuparams="-serial tcp:127.0.0.1:{}"'.format(threadport)
+ if qemuparams:
+ self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
+
+ self.origchldhandler = signal.getsignal(signal.SIGCHLD)
+ signal.signal(signal.SIGCHLD, self.handleSIGCHLD)
+
+ launch_cmd = 'runqemu tcpserial=%s %s %s %s' % (self.serverport, self.machine, self.rootfs, self.qemuparams)
+ # FIXME: We pass in stdin=subprocess.PIPE here to work around stty
+ # blocking at the end of the runqemu script when using this within
+ # oe-selftest (this makes stty error out immediately). There ought
+ # to be a proper fix but this will suffice for now.
+ self.runqemu = subprocess.Popen(launch_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, preexec_fn=os.setpgrp)
+ output = self.runqemu.stdout
+
+ #
+ # We need the preexec_fn above so that all runqemu processes can easily be killed
+ # (by killing their process group). This presents a problem if this controlling
+ # process itself is killed however since those processes don't notice the death
+ # of the parent and merrily continue on.
+ #
+ # Rather than hack runqemu to deal with this, we add something here instead.
+ # Basically we fork off another process which holds an open pipe to the parent
+ # and also is setpgrp. If/when the pipe sees EOF from the parent dieing, it kills
+ # the process group. This is like pctrl's PDEATHSIG but for a process group
+ # rather than a single process.
+ #
+ r, w = os.pipe()
+ self.monitorpid = os.fork()
+ if self.monitorpid:
+ os.close(r)
+ self.monitorpipe = os.fdopen(w, "w")
+ else:
+ # child process
+ os.setpgrp()
+ os.close(w)
+ r = os.fdopen(r)
+ x = r.read()
+ os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
+ sys.exit(0)
+
+ logger.info("runqemu started, pid is %s" % self.runqemu.pid)
+ logger.info("waiting at most %s seconds for qemu pid" % self.runqemutime)
+ endtime = time.time() + self.runqemutime
+ while not self.is_alive() and time.time() < endtime:
+ if self.runqemu.poll():
+ if self.runqemu.returncode:
+ # No point waiting any longer
+ logger.info('runqemu exited with code %d' % self.runqemu.returncode)
+ self._dump_host()
+ self.stop()
+ logger.info("Output from runqemu:\n%s" % self.getOutput(output))
+ return False
+ time.sleep(1)
+
+ if self.is_alive():
+ logger.info("qemu started - qemu procces pid is %s" % self.qemupid)
+ cmdline = ''
+ with open('/proc/%s/cmdline' % self.qemupid) as p:
+ cmdline = p.read()
+ try:
+ ips = re.findall("((?:[0-9]{1,3}\.){3}[0-9]{1,3})", cmdline.split("ip=")[1])
+ if not ips or len(ips) != 3:
+ raise ValueError
+ else:
+ self.ip = ips[0]
+ self.server_ip = ips[1]
+ except IndexError, ValueError:
+ logger.info("Couldn't get ip from qemu process arguments! Here is the qemu command line used:\n%s\nand output from runqemu:\n%s" % (cmdline, self.getOutput(output)))
+ self._dump_host()
+ self.stop()
+ return False
+ logger.info("qemu cmdline used:\n{}".format(cmdline))
+ logger.info("Target IP: %s" % self.ip)
+ logger.info("Server IP: %s" % self.server_ip)
+
+ logger.info("Starting logging thread")
+ self.thread = LoggingThread(self.log, threadsock, logger)
+ self.thread.start()
+ if not self.thread.connection_established.wait(self.boottime):
+ logger.error("Didn't receive a console connection from qemu. "
+ "Here is the qemu command line used:\n%s\nand "
+ "output from runqemu:\n%s" % (cmdline,
+ self.getOutput(output)))
+ self.stop_thread()
+ return False
+
+ logger.info("Waiting at most %d seconds for login banner" % self.boottime)
+ endtime = time.time() + self.boottime
+ socklist = [self.server_socket]
+ reachedlogin = False
+ stopread = False
+ qemusock = None
+ bootlog = ''
+ while time.time() < endtime and not stopread:
+ sread, swrite, serror = select.select(socklist, [], [], 5)
+ for sock in sread:
+ if sock is self.server_socket:
+ qemusock, addr = self.server_socket.accept()
+ qemusock.setblocking(0)
+ socklist.append(qemusock)
+ socklist.remove(self.server_socket)
+ logger.info("Connection from %s:%s" % addr)
+ else:
+ data = sock.recv(1024)
+ if data:
+ bootlog += data
+ if re.search(".* login:", bootlog):
+ self.server_socket = qemusock
+ stopread = True
+ reachedlogin = True
+ logger.info("Reached login banner")
+ else:
+ socklist.remove(sock)
+ sock.close()
+ stopread = True
+
+ if not reachedlogin:
+ logger.info("Target didn't reached login boot in %d seconds" % self.boottime)
+ lines = "\n".join(bootlog.splitlines()[-25:])
+ logger.info("Last 25 lines of text:\n%s" % lines)
+ logger.info("Check full boot log: %s" % self.logfile)
+ self._dump_host()
+ self.stop()
+ return False
+
+ # If we are not able to login the tests can continue
+ try:
+ (status, output) = self.run_serial("root\n", raw=True)
+ if re.search("root@[a-zA-Z0-9\-]+:~#", output):
+ self.logged = True
+ logger.info("Logged as root in serial console")
+ else:
+ logger.info("Couldn't login into serial console"
+ " as root using blank password")
+ except:
+ logger.info("Serial console failed while trying to login")
+
+ else:
+ logger.info("Qemu pid didn't appeared in %s seconds" % self.runqemutime)
+ self._dump_host()
+ self.stop()
+ logger.info("Output from runqemu:\n%s" % self.getOutput(output))
+ return False
+
+ return self.is_alive()
+
+ def stop(self):
+ self.stop_thread()
+ if self.runqemu:
+ signal.signal(signal.SIGCHLD, self.origchldhandler)
+ os.kill(self.monitorpid, signal.SIGKILL)
+ logger.info("Sending SIGTERM to runqemu")
+ try:
+ os.killpg(os.getpgid(self.runqemu.pid), signal.SIGTERM)
+ except OSError as e:
+ if e.errno != errno.ESRCH:
+ raise
+ endtime = time.time() + self.runqemutime
+ while self.runqemu.poll() is None and time.time() < endtime:
+ time.sleep(1)
+ if self.runqemu.poll() is None:
+ logger.info("Sending SIGKILL to runqemu")
+ os.killpg(os.getpgid(self.runqemu.pid), signal.SIGKILL)
+ self.runqemu = None
+ if hasattr(self, 'server_socket') and self.server_socket:
+ self.server_socket.close()
+ self.server_socket = None
+ self.qemupid = None
+ self.ip = None
+ signal.signal(signal.SIGCHLD, self.origchldhandler)
+
+ def stop_thread(self):
+ if self.thread and self.thread.is_alive():
+ self.thread.stop()
+ self.thread.join()
+
+ def restart(self, qemuparams = None):
+ logger.info("Restarting qemu process")
+ if self.runqemu.poll() is None:
+ self.stop()
+ if self.start(qemuparams):
+ return True
+ return False
+
+ def is_alive(self):
+ if not self.runqemu:
+ return False
+ qemu_child = self.find_child(str(self.runqemu.pid))
+ if qemu_child:
+ self.qemupid = qemu_child[0]
+ if os.path.exists("/proc/" + str(self.qemupid)):
+ return True
+ return False
+
+ def find_child(self,parent_pid):
+ #
+ # Walk the process tree from the process specified looking for a qemu-system. Return its [pid'cmd]
+ #
+ ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,command'], stdout=subprocess.PIPE).communicate()[0]
+ processes = ps.split('\n')
+ nfields = len(processes[0].split()) - 1
+ pids = {}
+ commands = {}
+ for row in processes[1:]:
+ data = row.split(None, nfields)
+ if len(data) != 3:
+ continue
+ if data[1] not in pids:
+ pids[data[1]] = []
+
+ pids[data[1]].append(data[0])
+ commands[data[0]] = data[2]
+
+ if parent_pid not in pids:
+ return []
+
+ parents = []
+ newparents = pids[parent_pid]
+ while newparents:
+ next = []
+ for p in newparents:
+ if p in pids:
+ for n in pids[p]:
+ if n not in parents and n not in next:
+ next.append(n)
+ if p not in parents:
+ parents.append(p)
+ newparents = next
+ #print "Children matching %s:" % str(parents)
+ for p in parents:
+ # Need to be careful here since runqemu-internal runs "ldd qemu-system-xxxx"
+ # Also, old versions of ldd (2.11) run "LD_XXXX qemu-system-xxxx"
+ basecmd = commands[p].split()[0]
+ basecmd = os.path.basename(basecmd)
+ if "qemu-system" in basecmd and "-serial tcp" in commands[p]:
+ return [int(p),commands[p]]
+
+ def run_serial(self, command, raw=False):
+ # We assume target system have echo to get command status
+ if not raw:
+ command = "%s; echo $?\n" % command
+ self.server_socket.sendall(command)
+ data = ''
+ status = 0
+ stopread = False
+ endtime = time.time()+5
+ while time.time()<endtime and not stopread:
+ sread, _, _ = select.select([self.server_socket],[],[],5)
+ for sock in sread:
+ answer = sock.recv(1024)
+ if answer:
+ data += answer
+ # Search the prompt to stop
+ if re.search("[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#", data):
+ stopread = True
+ break
+ else:
+ raise Exception("No data on serial console socket")
+ if data:
+ if raw:
+ status = 1
+ else:
+ # Remove first line (command line) and last line (prompt)
+ data = data[data.find('$?\r\n')+4:data.rfind('\r\n')]
+ index = data.rfind('\r\n')
+ if index == -1:
+ status_cmd = data
+ data = ""
+ else:
+ status_cmd = data[index+2:]
+ data = data[:index]
+ if (status_cmd == "0"):
+ status = 1
+ return (status, str(data))
+
+
+ def _dump_host(self):
+ self.host_dumper.create_dir("qemu")
+ logger.warn("Qemu ended unexpectedly, dump data from host"
+ " is in %s" % self.host_dumper.dump_dir)
+ self.host_dumper.dump_host()
+
+# This class is for reading data from a socket and passing it to logfunc
+# to be processed. It's completely event driven and has a straightforward
+# event loop. The mechanism for stopping the thread is a simple pipe which
+# will wake up the poll and allow for tearing everything down.
+class LoggingThread(threading.Thread):
+ def __init__(self, logfunc, sock, logger):
+ self.connection_established = threading.Event()
+ self.serversock = sock
+ self.logfunc = logfunc
+ self.logger = logger
+ self.readsock = None
+ self.running = False
+
+ self.errorevents = select.POLLERR | select.POLLHUP | select.POLLNVAL
+ self.readevents = select.POLLIN | select.POLLPRI
+
+ threading.Thread.__init__(self, target=self.threadtarget)
+
+ def threadtarget(self):
+ try:
+ self.eventloop()
+ finally:
+ self.teardown()
+
+ def run(self):
+ self.logger.info("Starting logging thread")
+ self.readpipe, self.writepipe = os.pipe()
+ threading.Thread.run(self)
+
+ def stop(self):
+ self.logger.info("Stopping logging thread")
+ if self.running:
+ os.write(self.writepipe, "stop")
+
+ def teardown(self):
+ self.logger.info("Tearing down logging thread")
+ self.close_socket(self.serversock)
+
+ if self.readsock is not None:
+ self.close_socket(self.readsock)
+
+ self.close_ignore_error(self.readpipe)
+ self.close_ignore_error(self.writepipe)
+ self.running = False
+
+ def eventloop(self):
+ poll = select.poll()
+ eventmask = self.errorevents | self.readevents
+ poll.register(self.serversock.fileno())
+ poll.register(self.readpipe, eventmask)
+
+ breakout = False
+ self.running = True
+ self.logger.info("Starting thread event loop")
+ while not breakout:
+ events = poll.poll()
+ for event in events:
+ # An error occurred, bail out
+ if event[1] & self.errorevents:
+ raise Exception(self.stringify_event(event[1]))
+
+ # Event to stop the thread
+ if self.readpipe == event[0]:
+ self.logger.info("Stop event received")
+ breakout = True
+ break
+
+ # A connection request was received
+ elif self.serversock.fileno() == event[0]:
+ self.logger.info("Connection request received")
+ self.readsock, _ = self.serversock.accept()
+ self.readsock.setblocking(0)
+ poll.unregister(self.serversock.fileno())
+ poll.register(self.readsock.fileno())
+
+ self.logger.info("Setting connection established event")
+ self.connection_established.set()
+
+ # Actual data to be logged
+ elif self.readsock.fileno() == event[0]:
+ data = self.recv(1024)
+ self.logfunc(data)
+
+ # Since the socket is non-blocking make sure to honor EAGAIN
+ # and EWOULDBLOCK.
+ def recv(self, count):
+ try:
+ data = self.readsock.recv(count)
+ except socket.error as e:
+ if e.errno == errno.EAGAIN or e.errno == errno.EWOULDBLOCK:
+ return ''
+ else:
+ raise
+
+ if data is None:
+ raise Exception("No data on read ready socket")
+ elif not data:
+ # This actually means an orderly shutdown
+ # happened. But for this code it counts as an
+ # error since the connection shouldn't go away
+ # until qemu exits.
+ raise Exception("Console connection closed unexpectedly")
+
+ return data
+
+ def stringify_event(self, event):
+ val = ''
+ if select.POLLERR == event:
+ val = 'POLLER'
+ elif select.POLLHUP == event:
+ val = 'POLLHUP'
+ elif select.POLLNVAL == event:
+ val = 'POLLNVAL'
+ return val
+
+ def close_socket(self, sock):
+ sock.shutdown(socket.SHUT_RDWR)
+ sock.close()
+
+ def close_ignore_error(self, fd):
+ try:
+ os.close(fd)
+ except OSError:
+ pass
diff --git a/meta/lib/oeqa/utils/qemutinyrunner.py b/meta/lib/oeqa/utils/qemutinyrunner.py
new file mode 100644
index 0000000..4f95101
--- /dev/null
+++ b/meta/lib/oeqa/utils/qemutinyrunner.py
@@ -0,0 +1,170 @@
+# Copyright (C) 2015 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# This module provides a class for starting qemu images of poky tiny.
+# It's used by testimage.bbclass.
+
+import subprocess
+import os
+import time
+import signal
+import re
+import socket
+import select
+import bb
+from qemurunner import QemuRunner
+
+class QemuTinyRunner(QemuRunner):
+
+ def __init__(self, machine, rootfs, display, tmpdir, deploy_dir_image, logfile, kernel, boottime):
+
+ # Popen object for runqemu
+ self.runqemu = None
+ # pid of the qemu process that runqemu will start
+ self.qemupid = None
+ # target ip - from the command line
+ self.ip = None
+ # host ip - where qemu is running
+ self.server_ip = None
+
+ self.machine = machine
+ self.rootfs = rootfs
+ self.display = display
+ self.tmpdir = tmpdir
+ self.deploy_dir_image = deploy_dir_image
+ self.logfile = logfile
+ self.boottime = boottime
+
+ self.runqemutime = 60
+ self.socketfile = "console.sock"
+ self.server_socket = None
+ self.kernel = kernel
+
+
+ def create_socket(self):
+ tries = 3
+ while tries > 0:
+ try:
+ self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.server_socket.connect(self.socketfile)
+ bb.note("Created listening socket for qemu serial console.")
+ tries = 0
+ except socket.error, msg:
+ self.server_socket.close()
+ bb.fatal("Failed to create listening socket.")
+ tries -= 1
+
+ def log(self, msg):
+ if self.logfile:
+ with open(self.logfile, "a") as f:
+ f.write("%s" % msg)
+
+ def start(self, qemuparams = None):
+
+ if self.display:
+ os.environ["DISPLAY"] = self.display
+ else:
+ bb.error("To start qemu I need a X desktop, please set DISPLAY correctly (e.g. DISPLAY=:1)")
+ return False
+ if not os.path.exists(self.rootfs):
+ bb.error("Invalid rootfs %s" % self.rootfs)
+ return False
+ if not os.path.exists(self.tmpdir):
+ bb.error("Invalid TMPDIR path %s" % self.tmpdir)
+ return False
+ else:
+ os.environ["OE_TMPDIR"] = self.tmpdir
+ if not os.path.exists(self.deploy_dir_image):
+ bb.error("Invalid DEPLOY_DIR_IMAGE path %s" % self.deploy_dir_image)
+ return False
+ else:
+ os.environ["DEPLOY_DIR_IMAGE"] = self.deploy_dir_image
+
+ # Set this flag so that Qemu doesn't do any grabs as SDL grabs interact
+ # badly with screensavers.
+ os.environ["QEMU_DONT_GRAB"] = "1"
+ self.qemuparams = '--append "root=/dev/ram0 console=ttyS0" -nographic -serial unix:%s,server,nowait' % self.socketfile
+
+ launch_cmd = 'qemu-system-i386 -kernel %s -initrd %s %s' % (self.kernel, self.rootfs, self.qemuparams)
+ self.runqemu = subprocess.Popen(launch_cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,preexec_fn=os.setpgrp)
+
+ bb.note("runqemu started, pid is %s" % self.runqemu.pid)
+ bb.note("waiting at most %s seconds for qemu pid" % self.runqemutime)
+ endtime = time.time() + self.runqemutime
+ while not self.is_alive() and time.time() < endtime:
+ time.sleep(1)
+
+ if self.is_alive():
+ bb.note("qemu started - qemu procces pid is %s" % self.qemupid)
+ self.create_socket()
+ else:
+ bb.note("Qemu pid didn't appeared in %s seconds" % self.runqemutime)
+ output = self.runqemu.stdout
+ self.stop()
+ bb.note("Output from runqemu:\n%s" % output.read())
+ return False
+
+ return self.is_alive()
+
+ def run_serial(self, command):
+ self.server_socket.sendall(command+'\n')
+ data = ''
+ status = 0
+ stopread = False
+ endtime = time.time()+5
+ while time.time()<endtime and not stopread:
+ sread, _, _ = select.select([self.server_socket],[],[],5)
+ for sock in sread:
+ answer = sock.recv(1024)
+ if answer:
+ data += answer
+ else:
+ sock.close()
+ stopread = True
+ if not data:
+ status = 1
+ return (status, str(data))
+
+ def find_child(self,parent_pid):
+ #
+ # Walk the process tree from the process specified looking for a qemu-system. Return its [pid'cmd]
+ #
+ ps = subprocess.Popen(['ps', 'axww', '-o', 'pid,ppid,command'], stdout=subprocess.PIPE).communicate()[0]
+ processes = ps.split('\n')
+ nfields = len(processes[0].split()) - 1
+ pids = {}
+ commands = {}
+ for row in processes[1:]:
+ data = row.split(None, nfields)
+ if len(data) != 3:
+ continue
+ if data[1] not in pids:
+ pids[data[1]] = []
+
+ pids[data[1]].append(data[0])
+ commands[data[0]] = data[2]
+
+ if parent_pid not in pids:
+ return []
+
+ parents = []
+ newparents = pids[parent_pid]
+ while newparents:
+ next = []
+ for p in newparents:
+ if p in pids:
+ for n in pids[p]:
+ if n not in parents and n not in next:
+ next.append(n)
+ if p not in parents:
+ parents.append(p)
+ newparents = next
+ #print "Children matching %s:" % str(parents)
+ for p in parents:
+ # Need to be careful here since runqemu-internal runs "ldd qemu-system-xxxx"
+ # Also, old versions of ldd (2.11) run "LD_XXXX qemu-system-xxxx"
+ basecmd = commands[p].split()[0]
+ basecmd = os.path.basename(basecmd)
+ if "qemu-system" in basecmd and "-serial unix" in commands[p]:
+ return [int(p),commands[p]]
\ No newline at end of file
diff --git a/meta/lib/oeqa/utils/sshcontrol.py b/meta/lib/oeqa/utils/sshcontrol.py
new file mode 100644
index 0000000..00f5051
--- /dev/null
+++ b/meta/lib/oeqa/utils/sshcontrol.py
@@ -0,0 +1,154 @@
+# Copyright (C) 2013 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# Provides a class for setting up ssh connections,
+# running commands and copying files to/from a target.
+# It's used by testimage.bbclass and tests in lib/oeqa/runtime.
+
+import subprocess
+import time
+import os
+import select
+
+
+class SSHProcess(object):
+ def __init__(self, **options):
+
+ self.defaultopts = {
+ "stdout": subprocess.PIPE,
+ "stderr": subprocess.STDOUT,
+ "stdin": None,
+ "shell": False,
+ "bufsize": -1,
+ "preexec_fn": os.setsid,
+ }
+ self.options = dict(self.defaultopts)
+ self.options.update(options)
+ self.status = None
+ self.output = None
+ self.process = None
+ self.starttime = None
+ self.logfile = None
+
+ # Unset DISPLAY which means we won't trigger SSH_ASKPASS
+ env = os.environ.copy()
+ if "DISPLAY" in env:
+ del env['DISPLAY']
+ self.options['env'] = env
+
+ def log(self, msg):
+ if self.logfile:
+ with open(self.logfile, "a") as f:
+ f.write("%s" % msg)
+
+ def _run(self, command, timeout=None, logfile=None):
+ self.logfile = logfile
+ self.starttime = time.time()
+ output = ''
+ self.process = subprocess.Popen(command, **self.options)
+ if timeout:
+ endtime = self.starttime + timeout
+ eof = False
+ while time.time() < endtime and not eof:
+ if select.select([self.process.stdout], [], [], 5)[0] != []:
+ data = os.read(self.process.stdout.fileno(), 1024)
+ if not data:
+ self.process.stdout.close()
+ eof = True
+ else:
+ output += data
+ self.log(data)
+ endtime = time.time() + timeout
+
+
+ # process hasn't returned yet
+ if not eof:
+ self.process.terminate()
+ time.sleep(5)
+ try:
+ self.process.kill()
+ except OSError:
+ pass
+ lastline = "\nProcess killed - no output for %d seconds. Total running time: %d seconds." % (timeout, time.time() - self.starttime)
+ self.log(lastline)
+ output += lastline
+ else:
+ output = self.process.communicate()[0]
+ self.log(output.rstrip())
+
+ self.status = self.process.wait()
+ self.output = output.rstrip()
+
+ def run(self, command, timeout=None, logfile=None):
+ try:
+ self._run(command, timeout, logfile)
+ except:
+ # Need to guard against a SystemExit or other exception occuring whilst running
+ # and ensure we don't leave a process behind.
+ if self.process.poll() is None:
+ self.process.kill()
+ self.status = self.process.wait()
+ raise
+ return (self.status, self.output)
+
+class SSHControl(object):
+ def __init__(self, ip, logfile=None, timeout=300, user='root', port=None):
+ self.ip = ip
+ self.defaulttimeout = timeout
+ self.ignore_status = True
+ self.logfile = logfile
+ self.user = user
+ self.ssh_options = [
+ '-o', 'UserKnownHostsFile=/dev/null',
+ '-o', 'StrictHostKeyChecking=no',
+ '-o', 'LogLevel=ERROR'
+ ]
+ self.ssh = ['ssh', '-l', self.user ] + self.ssh_options
+ self.scp = ['scp'] + self.ssh_options
+ if port:
+ self.ssh = self.ssh + [ '-p', port ]
+ self.scp = self.scp + [ '-P', port ]
+
+ def log(self, msg):
+ if self.logfile:
+ with open(self.logfile, "a") as f:
+ f.write("%s\n" % msg)
+
+ def _internal_run(self, command, timeout=None, ignore_status = True):
+ self.log("[Running]$ %s" % " ".join(command))
+
+ proc = SSHProcess()
+ status, output = proc.run(command, timeout, logfile=self.logfile)
+
+ self.log("[Command returned '%d' after %.2f seconds]" % (status, time.time() - proc.starttime))
+
+ if status and not ignore_status:
+ raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, status, output))
+
+ return (status, output)
+
+ def run(self, command, timeout=None):
+ """
+ command - ssh command to run
+ timeout=<val> - kill command if there is no output after <val> seconds
+ timeout=None - kill command if there is no output after a default value seconds
+ timeout=0 - no timeout, let command run until it returns
+ """
+
+ # We need to source /etc/profile for a proper PATH on the target
+ command = self.ssh + [self.ip, ' . /etc/profile; ' + command]
+
+ if timeout is None:
+ return self._internal_run(command, self.defaulttimeout, self.ignore_status)
+ if timeout == 0:
+ return self._internal_run(command, None, self.ignore_status)
+ return self._internal_run(command, timeout, self.ignore_status)
+
+ def copy_to(self, localpath, remotepath):
+ command = self.scp + [localpath, '%s@%s:%s' % (self.user, self.ip, remotepath)]
+ return self._internal_run(command, ignore_status=False)
+
+ def copy_from(self, remotepath, localpath):
+ command = self.scp + ['%s@%s:%s' % (self.user, self.ip, remotepath), localpath]
+ return self._internal_run(command, ignore_status=False)
diff --git a/meta/lib/oeqa/utils/targetbuild.py b/meta/lib/oeqa/utils/targetbuild.py
new file mode 100644
index 0000000..f850d78
--- /dev/null
+++ b/meta/lib/oeqa/utils/targetbuild.py
@@ -0,0 +1,137 @@
+# Copyright (C) 2013 Intel Corporation
+#
+# Released under the MIT license (see COPYING.MIT)
+
+# Provides a class for automating build tests for projects
+
+import os
+import re
+import bb.utils
+import subprocess
+from abc import ABCMeta, abstractmethod
+
+class BuildProject():
+
+ __metaclass__ = ABCMeta
+
+ def __init__(self, d, uri, foldername=None, tmpdir="/tmp/"):
+ self.d = d
+ self.uri = uri
+ self.archive = os.path.basename(uri)
+ self.localarchive = os.path.join(tmpdir,self.archive)
+ self.fname = re.sub(r'.tar.bz2|tar.gz$', '', self.archive)
+ if foldername:
+ self.fname = foldername
+
+ # Download self.archive to self.localarchive
+ def _download_archive(self):
+
+ dl_dir = self.d.getVar("DL_DIR", True)
+ if dl_dir and os.path.exists(os.path.join(dl_dir, self.archive)):
+ bb.utils.copyfile(os.path.join(dl_dir, self.archive), self.localarchive)
+ return
+
+ exportvars = ['HTTP_PROXY', 'http_proxy',
+ 'HTTPS_PROXY', 'https_proxy',
+ 'FTP_PROXY', 'ftp_proxy',
+ 'FTPS_PROXY', 'ftps_proxy',
+ 'NO_PROXY', 'no_proxy',
+ 'ALL_PROXY', 'all_proxy',
+ 'SOCKS5_USER', 'SOCKS5_PASSWD']
+
+ cmd = ''
+ for var in exportvars:
+ val = self.d.getVar(var, True)
+ if val:
+ cmd = 'export ' + var + '=\"%s\"; %s' % (val, cmd)
+
+ cmd = cmd + "wget -O %s %s" % (self.localarchive, self.uri)
+ subprocess.check_call(cmd, shell=True)
+
+ # This method should provide a way to run a command in the desired environment.
+ @abstractmethod
+ def _run(self, cmd):
+ pass
+
+ # The timeout parameter of target.run is set to 0 to make the ssh command
+ # run with no timeout.
+ def run_configure(self, configure_args='', extra_cmds=''):
+ return self._run('cd %s; %s ./configure %s' % (self.targetdir, extra_cmds, configure_args))
+
+ def run_make(self, make_args=''):
+ return self._run('cd %s; make %s' % (self.targetdir, make_args))
+
+ def run_install(self, install_args=''):
+ return self._run('cd %s; make install %s' % (self.targetdir, install_args))
+
+ def clean(self):
+ self._run('rm -rf %s' % self.targetdir)
+ subprocess.call('rm -f %s' % self.localarchive, shell=True)
+ pass
+
+class TargetBuildProject(BuildProject):
+
+ def __init__(self, target, d, uri, foldername=None):
+ self.target = target
+ self.targetdir = "~/"
+ BuildProject.__init__(self, d, uri, foldername, tmpdir="/tmp")
+
+ def download_archive(self):
+
+ self._download_archive()
+
+ (status, output) = self.target.copy_to(self.localarchive, self.targetdir)
+ if status != 0:
+ raise Exception("Failed to copy archive to target, output: %s" % output)
+
+ (status, output) = self.target.run('tar xf %s%s -C %s' % (self.targetdir, self.archive, self.targetdir))
+ if status != 0:
+ raise Exception("Failed to extract archive, output: %s" % output)
+
+ #Change targetdir to project folder
+ self.targetdir = self.targetdir + self.fname
+
+ # The timeout parameter of target.run is set to 0 to make the ssh command
+ # run with no timeout.
+ def _run(self, cmd):
+ return self.target.run(cmd, 0)[0]
+
+
+class SDKBuildProject(BuildProject):
+
+ def __init__(self, testpath, sdkenv, d, uri, foldername=None):
+ self.sdkenv = sdkenv
+ self.testdir = testpath
+ self.targetdir = testpath
+ bb.utils.mkdirhier(testpath)
+ self.datetime = d.getVar('DATETIME', True)
+ self.testlogdir = d.getVar("TEST_LOG_DIR", True)
+ bb.utils.mkdirhier(self.testlogdir)
+ self.logfile = os.path.join(self.testlogdir, "sdk_target_log.%s" % self.datetime)
+ BuildProject.__init__(self, d, uri, foldername, tmpdir=testpath)
+
+ def download_archive(self):
+
+ self._download_archive()
+
+ cmd = 'tar xf %s%s -C %s' % (self.targetdir, self.archive, self.targetdir)
+ subprocess.check_call(cmd, shell=True)
+
+ #Change targetdir to project folder
+ self.targetdir = self.targetdir + self.fname
+
+ def run_configure(self, configure_args=''):
+ return super(SDKBuildProject, self).run_configure(configure_args=(configure_args or '$CONFIGURE_FLAGS'), extra_cmds=' gnu-configize; ')
+
+ def run_install(self, install_args=''):
+ return super(SDKBuildProject, self).run_install(install_args=(install_args or "DESTDIR=%s/../install" % self.targetdir))
+
+ def log(self, msg):
+ if self.logfile:
+ with open(self.logfile, "a") as f:
+ f.write("%s\n" % msg)
+
+ def _run(self, cmd):
+ self.log("Running . %s; " % self.sdkenv + cmd)
+ return subprocess.call(". %s; " % self.sdkenv + cmd, shell=True)
+