aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/lib/checklayer
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/lib/checklayer')
-rw-r--r--scripts/lib/checklayer/__init__.py392
-rw-r--r--scripts/lib/checklayer/case.py7
-rw-r--r--scripts/lib/checklayer/cases/__init__.py0
-rw-r--r--scripts/lib/checklayer/cases/bsp.py204
-rw-r--r--scripts/lib/checklayer/cases/common.py53
-rw-r--r--scripts/lib/checklayer/cases/distro.py26
-rw-r--r--scripts/lib/checklayer/context.py15
7 files changed, 697 insertions, 0 deletions
diff --git a/scripts/lib/checklayer/__init__.py b/scripts/lib/checklayer/__init__.py
new file mode 100644
index 00000000000..6c2b86a79a1
--- /dev/null
+++ b/scripts/lib/checklayer/__init__.py
@@ -0,0 +1,392 @@
+# Yocto Project layer check tool
+#
+# Copyright (C) 2017 Intel Corporation
+# Released under the MIT license (see COPYING.MIT)
+
+import os
+import re
+import subprocess
+from enum import Enum
+
+import bb.tinfoil
+
+class LayerType(Enum):
+ BSP = 0
+ DISTRO = 1
+ SOFTWARE = 2
+ ERROR_NO_LAYER_CONF = 98
+ ERROR_BSP_DISTRO = 99
+
+def _get_configurations(path):
+ configs = []
+
+ for f in os.listdir(path):
+ file_path = os.path.join(path, f)
+ if os.path.isfile(file_path) and f.endswith('.conf'):
+ configs.append(f[:-5]) # strip .conf
+ return configs
+
+def _get_layer_collections(layer_path, lconf=None, data=None):
+ import bb.parse
+ import bb.data
+
+ if lconf is None:
+ lconf = os.path.join(layer_path, 'conf', 'layer.conf')
+
+ if data is None:
+ ldata = bb.data.init()
+ bb.parse.init_parser(ldata)
+ else:
+ ldata = data.createCopy()
+
+ ldata.setVar('LAYERDIR', layer_path)
+ try:
+ ldata = bb.parse.handle(lconf, ldata, include=True)
+ except BaseException as exc:
+ raise LayerError(exc)
+ ldata.expandVarref('LAYERDIR')
+
+ collections = (ldata.getVar('BBFILE_COLLECTIONS', True) or '').split()
+ if not collections:
+ name = os.path.basename(layer_path)
+ collections = [name]
+
+ collections = {c: {} for c in collections}
+ for name in collections:
+ priority = ldata.getVar('BBFILE_PRIORITY_%s' % name, True)
+ pattern = ldata.getVar('BBFILE_PATTERN_%s' % name, True)
+ depends = ldata.getVar('LAYERDEPENDS_%s' % name, True)
+ collections[name]['priority'] = priority
+ collections[name]['pattern'] = pattern
+ collections[name]['depends'] = depends
+
+ return collections
+
+def _detect_layer(layer_path):
+ """
+ Scans layer directory to detect what type of layer
+ is BSP, Distro or Software.
+
+ Returns a dictionary with layer name, type and path.
+ """
+
+ layer = {}
+ layer_name = os.path.basename(layer_path)
+
+ layer['name'] = layer_name
+ layer['path'] = layer_path
+ layer['conf'] = {}
+
+ if not os.path.isfile(os.path.join(layer_path, 'conf', 'layer.conf')):
+ layer['type'] = LayerType.ERROR_NO_LAYER_CONF
+ return layer
+
+ machine_conf = os.path.join(layer_path, 'conf', 'machine')
+ distro_conf = os.path.join(layer_path, 'conf', 'distro')
+
+ is_bsp = False
+ is_distro = False
+
+ if os.path.isdir(machine_conf):
+ machines = _get_configurations(machine_conf)
+ if machines:
+ is_bsp = True
+
+ if os.path.isdir(distro_conf):
+ distros = _get_configurations(distro_conf)
+ if distros:
+ is_distro = True
+
+ if is_bsp and is_distro:
+ layer['type'] = LayerType.ERROR_BSP_DISTRO
+ elif is_bsp:
+ layer['type'] = LayerType.BSP
+ layer['conf']['machines'] = machines
+ elif is_distro:
+ layer['type'] = LayerType.DISTRO
+ layer['conf']['distros'] = distros
+ else:
+ layer['type'] = LayerType.SOFTWARE
+
+ layer['collections'] = _get_layer_collections(layer['path'])
+
+ return layer
+
+def detect_layers(layer_directories, no_auto):
+ layers = []
+
+ for directory in layer_directories:
+ directory = os.path.realpath(directory)
+ if directory[-1] == '/':
+ directory = directory[0:-1]
+
+ if no_auto:
+ conf_dir = os.path.join(directory, 'conf')
+ if os.path.isdir(conf_dir):
+ layer = _detect_layer(directory)
+ if layer:
+ layers.append(layer)
+ else:
+ for root, dirs, files in os.walk(directory):
+ dir_name = os.path.basename(root)
+ conf_dir = os.path.join(root, 'conf')
+ if os.path.isdir(conf_dir):
+ layer = _detect_layer(root)
+ if layer:
+ layers.append(layer)
+
+ return layers
+
+def _find_layer_depends(depend, layers):
+ for layer in layers:
+ for collection in layer['collections']:
+ if depend == collection:
+ return layer
+ return None
+
+def add_layer_dependencies(bblayersconf, layer, layers, logger):
+ def recurse_dependencies(depends, layer, layers, logger, ret = []):
+ logger.debug('Processing dependencies %s for layer %s.' % \
+ (depends, layer['name']))
+
+ for depend in depends.split():
+ # core (oe-core) is suppose to be provided
+ if depend == 'core':
+ continue
+
+ layer_depend = _find_layer_depends(depend, layers)
+ if not layer_depend:
+ logger.error('Layer %s depends on %s and isn\'t found.' % \
+ (layer['name'], depend))
+ ret = None
+ continue
+
+ # We keep processing, even if ret is None, this allows us to report
+ # multiple errors at once
+ if ret is not None and layer_depend not in ret:
+ ret.append(layer_depend)
+
+ # Recursively process...
+ if 'collections' not in layer_depend:
+ continue
+
+ for collection in layer_depend['collections']:
+ collect_deps = layer_depend['collections'][collection]['depends']
+ if not collect_deps:
+ continue
+ ret = recurse_dependencies(collect_deps, layer_depend, layers, logger, ret)
+
+ return ret
+
+ layer_depends = []
+ for collection in layer['collections']:
+ depends = layer['collections'][collection]['depends']
+ if not depends:
+ continue
+
+ layer_depends = recurse_dependencies(depends, layer, layers, logger, layer_depends)
+
+ # Note: [] (empty) is allowed, None is not!
+ if layer_depends is None:
+ return False
+ else:
+ # Don't add a layer that is already present.
+ added = set()
+ output = check_command('Getting existing layers failed.', 'bitbake-layers show-layers').decode('utf-8')
+ for layer, path, pri in re.findall(r'^(\S+) +([^\n]*?) +(\d+)$', output, re.MULTILINE):
+ added.add(path)
+
+ for layer_depend in layer_depends:
+ name = layer_depend['name']
+ path = layer_depend['path']
+ if path in added:
+ continue
+ else:
+ added.add(path)
+ logger.info('Adding layer dependency %s' % name)
+ with open(bblayersconf, 'a+') as f:
+ f.write("\nBBLAYERS += \"%s\"\n" % path)
+ return True
+
+def add_layer(bblayersconf, layer, layers, logger):
+ logger.info('Adding layer %s' % layer['name'])
+ with open(bblayersconf, 'a+') as f:
+ f.write("\nBBLAYERS += \"%s\"\n" % layer['path'])
+
+ return True
+
+def check_command(error_msg, cmd):
+ '''
+ Run a command under a shell, capture stdout and stderr in a single stream,
+ throw an error when command returns non-zero exit code. Returns the output.
+ '''
+
+ p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ output, _ = p.communicate()
+ if p.returncode:
+ msg = "%s\nCommand: %s\nOutput:\n%s" % (error_msg, cmd, output.decode('utf-8'))
+ raise RuntimeError(msg)
+ return output
+
+def get_signatures(builddir, failsafe=False, machine=None):
+ import re
+
+ # some recipes needs to be excluded like meta-world-pkgdata
+ # because a layer can add recipes to a world build so signature
+ # will be change
+ exclude_recipes = ('meta-world-pkgdata',)
+
+ sigs = {}
+ tune2tasks = {}
+
+ cmd = ''
+ if machine:
+ cmd += 'MACHINE=%s ' % machine
+ cmd += 'bitbake '
+ if failsafe:
+ cmd += '-k '
+ cmd += '-S none world'
+ sigs_file = os.path.join(builddir, 'locked-sigs.inc')
+ if os.path.exists(sigs_file):
+ os.unlink(sigs_file)
+ try:
+ check_command('Generating signatures failed. This might be due to some parse error and/or general layer incompatibilities.',
+ cmd)
+ except RuntimeError as ex:
+ if failsafe and os.path.exists(sigs_file):
+ # Ignore the error here. Most likely some recipes active
+ # in a world build lack some dependencies. There is a
+ # separate test_machine_world_build which exposes the
+ # failure.
+ pass
+ else:
+ raise
+
+ sig_regex = re.compile("^(?P<task>.*:.*):(?P<hash>.*) .$")
+ tune_regex = re.compile("(^|\s)SIGGEN_LOCKEDSIGS_t-(?P<tune>\S*)\s*=\s*")
+ current_tune = None
+ with open(sigs_file, 'r') as f:
+ for line in f.readlines():
+ line = line.strip()
+ t = tune_regex.search(line)
+ if t:
+ current_tune = t.group('tune')
+ s = sig_regex.match(line)
+ if s:
+ exclude = False
+ for er in exclude_recipes:
+ (recipe, task) = s.group('task').split(':')
+ if er == recipe:
+ exclude = True
+ break
+ if exclude:
+ continue
+
+ sigs[s.group('task')] = s.group('hash')
+ tune2tasks.setdefault(current_tune, []).append(s.group('task'))
+
+ if not sigs:
+ raise RuntimeError('Can\'t load signatures from %s' % sigs_file)
+
+ return (sigs, tune2tasks)
+
+def get_depgraph(targets=['world'], failsafe=False):
+ '''
+ Returns the dependency graph for the given target(s).
+ The dependency graph is taken directly from DepTreeEvent.
+ '''
+ depgraph = None
+ with bb.tinfoil.Tinfoil() as tinfoil:
+ tinfoil.prepare(config_only=False)
+ tinfoil.set_event_mask(['bb.event.NoProvider', 'bb.event.DepTreeGenerated', 'bb.command.CommandCompleted'])
+ if not tinfoil.run_command('generateDepTreeEvent', targets, 'do_build'):
+ raise RuntimeError('starting generateDepTreeEvent failed')
+ while True:
+ event = tinfoil.wait_event(timeout=1000)
+ if event:
+ if isinstance(event, bb.command.CommandFailed):
+ raise RuntimeError('Generating dependency information failed: %s' % event.error)
+ elif isinstance(event, bb.command.CommandCompleted):
+ break
+ elif isinstance(event, bb.event.NoProvider):
+ if failsafe:
+ # The event is informational, we will get information about the
+ # remaining dependencies eventually and thus can ignore this
+ # here like we do in get_signatures(), if desired.
+ continue
+ if event._reasons:
+ raise RuntimeError('Nothing provides %s: %s' % (event._item, event._reasons))
+ else:
+ raise RuntimeError('Nothing provides %s.' % (event._item))
+ elif isinstance(event, bb.event.DepTreeGenerated):
+ depgraph = event._depgraph
+
+ if depgraph is None:
+ raise RuntimeError('Could not retrieve the depgraph.')
+ return depgraph
+
+def compare_signatures(old_sigs, curr_sigs):
+ '''
+ Compares the result of two get_signatures() calls. Returns None if no
+ problems found, otherwise a string that can be used as additional
+ explanation in self.fail().
+ '''
+ # task -> (old signature, new signature)
+ sig_diff = {}
+ for task in old_sigs:
+ if task in curr_sigs and \
+ old_sigs[task] != curr_sigs[task]:
+ sig_diff[task] = (old_sigs[task], curr_sigs[task])
+
+ if not sig_diff:
+ return None
+
+ # Beware, depgraph uses task=<pn>.<taskname> whereas get_signatures()
+ # uses <pn>:<taskname>. Need to convert sometimes. The output follows
+ # the convention from get_signatures() because that seems closer to
+ # normal bitbake output.
+ def sig2graph(task):
+ pn, taskname = task.rsplit(':', 1)
+ return pn + '.' + taskname
+ def graph2sig(task):
+ pn, taskname = task.rsplit('.', 1)
+ return pn + ':' + taskname
+ depgraph = get_depgraph(failsafe=True)
+ depends = depgraph['tdepends']
+
+ # If a task A has a changed signature, but none of its
+ # dependencies, then we need to report it because it is
+ # the one which introduces a change. Any task depending on
+ # A (directly or indirectly) will also have a changed
+ # signature, but we don't need to report it. It might have
+ # its own changes, which will become apparent once the
+ # issues that we do report are fixed and the test gets run
+ # again.
+ sig_diff_filtered = []
+ for task, (old_sig, new_sig) in sig_diff.items():
+ deps_tainted = False
+ for dep in depends.get(sig2graph(task), ()):
+ if graph2sig(dep) in sig_diff:
+ deps_tainted = True
+ break
+ if not deps_tainted:
+ sig_diff_filtered.append((task, old_sig, new_sig))
+
+ msg = []
+ msg.append('%d signatures changed, initial differences (first hash before, second after):' %
+ len(sig_diff))
+ for diff in sorted(sig_diff_filtered):
+ recipe, taskname = diff[0].rsplit(':', 1)
+ cmd = 'bitbake-diffsigs --task %s %s --signature %s %s' % \
+ (recipe, taskname, diff[1], diff[2])
+ msg.append(' %s: %s -> %s' % diff)
+ msg.append(' %s' % cmd)
+ try:
+ output = check_command('Determining signature difference failed.',
+ cmd).decode('utf-8')
+ except RuntimeError as error:
+ output = str(error)
+ if output:
+ msg.extend([' ' + line for line in output.splitlines()])
+ msg.append('')
+ return '\n'.join(msg)
diff --git a/scripts/lib/checklayer/case.py b/scripts/lib/checklayer/case.py
new file mode 100644
index 00000000000..9dd00412e52
--- /dev/null
+++ b/scripts/lib/checklayer/case.py
@@ -0,0 +1,7 @@
+# Copyright (C) 2017 Intel Corporation
+# Released under the MIT license (see COPYING.MIT)
+
+from oeqa.core.case import OETestCase
+
+class OECheckLayerTestCase(OETestCase):
+ pass
diff --git a/scripts/lib/checklayer/cases/__init__.py b/scripts/lib/checklayer/cases/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/scripts/lib/checklayer/cases/__init__.py
diff --git a/scripts/lib/checklayer/cases/bsp.py b/scripts/lib/checklayer/cases/bsp.py
new file mode 100644
index 00000000000..b6b611be731
--- /dev/null
+++ b/scripts/lib/checklayer/cases/bsp.py
@@ -0,0 +1,204 @@
+# Copyright (C) 2017 Intel Corporation
+# Released under the MIT license (see COPYING.MIT)
+
+import unittest
+
+from checklayer import LayerType, get_signatures, check_command, get_depgraph
+from checklayer.case import OECheckLayerTestCase
+
+class BSPCheckLayer(OECheckLayerTestCase):
+ @classmethod
+ def setUpClass(self):
+ if self.tc.layer['type'] != LayerType.BSP:
+ raise unittest.SkipTest("BSPCheckLayer: Layer %s isn't BSP one." %\
+ self.tc.layer['name'])
+
+ def test_bsp_defines_machines(self):
+ self.assertTrue(self.tc.layer['conf']['machines'],
+ "Layer is BSP but doesn't defines machines.")
+
+ def test_bsp_no_set_machine(self):
+ from oeqa.utils.commands import get_bb_var
+
+ machine = get_bb_var('MACHINE')
+ self.assertEqual(self.td['bbvars']['MACHINE'], machine,
+ msg="Layer %s modified machine %s -> %s" % \
+ (self.tc.layer['name'], self.td['bbvars']['MACHINE'], machine))
+
+
+ def test_machine_world(self):
+ '''
+ "bitbake world" is expected to work regardless which machine is selected.
+ BSP layers sometimes break that by enabling a recipe for a certain machine
+ without checking whether that recipe actually can be built in the current
+ distro configuration (for example, OpenGL might not enabled).
+
+ This test iterates over all machines. It would be nicer to instantiate
+ it once per machine. It merely checks for errors during parse
+ time. It does not actually attempt to build anything.
+ '''
+
+ if not self.td['machines']:
+ self.skipTest('No machines set with --machines.')
+ msg = []
+ for machine in self.td['machines']:
+ # In contrast to test_machine_signatures() below, errors are fatal here.
+ try:
+ get_signatures(self.td['builddir'], failsafe=False, machine=machine)
+ except RuntimeError as ex:
+ msg.append(str(ex))
+ if msg:
+ msg.insert(0, 'The following machines broke a world build:')
+ self.fail('\n'.join(msg))
+
+ def test_machine_signatures(self):
+ '''
+ Selecting a machine may only affect the signature of tasks that are specific
+ to that machine. In other words, when MACHINE=A and MACHINE=B share a recipe
+ foo and the output of foo, then both machine configurations must build foo
+ in exactly the same way. Otherwise it is not possible to use both machines
+ in the same distribution.
+
+ This criteria can only be tested by testing different machines in combination,
+ i.e. one main layer, potentially several additional BSP layers and an explicit
+ choice of machines:
+ yocto-check-layer --additional-layers .../meta-intel --machines intel-corei7-64 imx6slevk -- .../meta-freescale
+ '''
+
+ if not self.td['machines']:
+ self.skipTest('No machines set with --machines.')
+
+ # Collect signatures for all machines that we are testing
+ # and merge that into a hash:
+ # tune -> task -> signature -> list of machines with that combination
+ #
+ # It is an error if any tune/task pair has more than one signature,
+ # because that implies that the machines that caused those different
+ # signatures do not agree on how to execute the task.
+ tunes = {}
+ # Preserve ordering of machines as chosen by the user.
+ for machine in self.td['machines']:
+ curr_sigs, tune2tasks = get_signatures(self.td['builddir'], failsafe=True, machine=machine)
+ # Invert the tune -> [tasks] mapping.
+ tasks2tune = {}
+ for tune, tasks in tune2tasks.items():
+ for task in tasks:
+ tasks2tune[task] = tune
+ for task, sighash in curr_sigs.items():
+ tunes.setdefault(tasks2tune[task], {}).setdefault(task, {}).setdefault(sighash, []).append(machine)
+
+ msg = []
+ pruned = 0
+ last_line_key = None
+ # do_fetch, do_unpack, ..., do_build
+ taskname_list = []
+ if tunes:
+ # The output below is most useful when we start with tasks that are at
+ # the bottom of the dependency chain, i.e. those that run first. If
+ # those tasks differ, the rest also does.
+ #
+ # To get an ordering of tasks, we do a topological sort of the entire
+ # depgraph for the base configuration, then on-the-fly flatten that list by stripping
+ # out the recipe names and removing duplicates. The base configuration
+ # is not necessarily representative, but should be close enough. Tasks
+ # that were not encountered get a default priority.
+ depgraph = get_depgraph()
+ depends = depgraph['tdepends']
+ WHITE = 1
+ GRAY = 2
+ BLACK = 3
+ color = {}
+ found = set()
+ def visit(task):
+ color[task] = GRAY
+ for dep in depends.get(task, ()):
+ if color.setdefault(dep, WHITE) == WHITE:
+ visit(dep)
+ color[task] = BLACK
+ pn, taskname = task.rsplit('.', 1)
+ if taskname not in found:
+ taskname_list.append(taskname)
+ found.add(taskname)
+ for task in depends.keys():
+ if color.setdefault(task, WHITE) == WHITE:
+ visit(task)
+
+ taskname_order = dict([(task, index) for index, task in enumerate(taskname_list) ])
+ def task_key(task):
+ pn, taskname = task.rsplit(':', 1)
+ return (pn, taskname_order.get(taskname, len(taskname_list)), taskname)
+
+ for tune in sorted(tunes.keys()):
+ tasks = tunes[tune]
+ # As for test_signatures it would be nicer to sort tasks
+ # by dependencies here, but that is harder because we have
+ # to report on tasks from different machines, which might
+ # have different dependencies. We resort to pruning the
+ # output by reporting only one task per recipe if the set
+ # of machines matches.
+ #
+ # "bitbake-diffsigs -t -s" is intelligent enough to print
+ # diffs recursively, so often it does not matter that much
+ # if we don't pick the underlying difference
+ # here. However, sometimes recursion fails
+ # (https://bugzilla.yoctoproject.org/show_bug.cgi?id=6428).
+ #
+ # To mitigate that a bit, we use a hard-coded ordering of
+ # tasks that represents how they normally run and prefer
+ # to print the ones that run first.
+ for task in sorted(tasks.keys(), key=task_key):
+ signatures = tasks[task]
+ # do_build can be ignored: it is know to have
+ # different signatures in some cases, for example in
+ # the allarch ca-certificates due to RDEPENDS=openssl.
+ # That particular dependency is whitelisted via
+ # SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS, but still shows up
+ # in the sstate signature hash because filtering it
+ # out would be hard and running do_build multiple
+ # times doesn't really matter.
+ if len(signatures.keys()) > 1 and \
+ not task.endswith(':do_build'):
+ # Error!
+ #
+ # Sort signatures by machines, because the hex values don't mean anything.
+ # => all-arch adwaita-icon-theme:do_build: 1234... (beaglebone, qemux86) != abcdf... (qemux86-64)
+ #
+ # Skip the line if it is covered already by the predecessor (same pn, same sets of machines).
+ pn, taskname = task.rsplit(':', 1)
+ next_line_key = (pn, sorted(signatures.values()))
+ if next_line_key != last_line_key:
+ line = ' %s %s: ' % (tune, task)
+ line += ' != '.join(['%s (%s)' % (signature, ', '.join([m for m in signatures[signature]])) for
+ signature in sorted(signatures.keys(), key=lambda s: signatures[s])])
+ last_line_key = next_line_key
+ msg.append(line)
+ # Randomly pick two mismatched signatures and remember how to invoke
+ # bitbake-diffsigs for them.
+ iterator = iter(signatures.items())
+ a = next(iterator)
+ b = next(iterator)
+ diffsig_machines = '(%s) != (%s)' % (', '.join(a[1]), ', '.join(b[1]))
+ diffsig_params = '-t %s %s -s %s %s' % (pn, taskname, a[0], b[0])
+ else:
+ pruned += 1
+
+ if msg:
+ msg.insert(0, 'The machines have conflicting signatures for some shared tasks:')
+ if pruned > 0:
+ msg.append('')
+ msg.append('%d tasks where not listed because some other task of the recipe already differed.' % pruned)
+ msg.append('It is likely that differences from different recipes also have the same root cause.')
+ msg.append('')
+ # Explain how to investigate...
+ msg.append('To investigate, run bitbake-diffsigs -t recipename taskname -s fromsig tosig.')
+ cmd = 'bitbake-diffsigs %s' % diffsig_params
+ msg.append('Example: %s in the last line' % diffsig_machines)
+ msg.append('Command: %s' % cmd)
+ # ... and actually do it automatically for that example, but without aborting
+ # when that fails.
+ try:
+ output = check_command('Comparing signatures failed.', cmd).decode('utf-8')
+ except RuntimeError as ex:
+ output = str(ex)
+ msg.extend([' ' + line for line in output.splitlines()])
+ self.fail('\n'.join(msg))
diff --git a/scripts/lib/checklayer/cases/common.py b/scripts/lib/checklayer/cases/common.py
new file mode 100644
index 00000000000..a13c1088f07
--- /dev/null
+++ b/scripts/lib/checklayer/cases/common.py
@@ -0,0 +1,53 @@
+# Copyright (C) 2017 Intel Corporation
+# Released under the MIT license (see COPYING.MIT)
+
+import glob
+import os
+import unittest
+from checklayer import get_signatures, LayerType, check_command, get_depgraph, compare_signatures
+from checklayer.case import OECheckLayerTestCase
+
+class CommonCheckLayer(OECheckLayerTestCase):
+ def test_readme(self):
+ # The top-level README file may have a suffix (like README.rst or README.txt).
+ readme_files = glob.glob(os.path.join(self.tc.layer['path'], 'README*'))
+ self.assertTrue(len(readme_files) > 0,
+ msg="Layer doesn't contains README file.")
+
+ # There might be more than one file matching the file pattern above
+ # (for example, README.rst and README-COPYING.rst). The one with the shortest
+ # name is considered the "main" one.
+ readme_file = sorted(readme_files)[0]
+ data = ''
+ with open(readme_file, 'r') as f:
+ data = f.read()
+ self.assertTrue(data,
+ msg="Layer contains a README file but it is empty.")
+
+ def test_parse(self):
+ check_command('Layer %s failed to parse.' % self.tc.layer['name'],
+ 'bitbake -p')
+
+ def test_show_environment(self):
+ check_command('Layer %s failed to show environment.' % self.tc.layer['name'],
+ 'bitbake -e')
+
+ def test_world(self):
+ '''
+ "bitbake world" is expected to work. test_signatures does not cover that
+ because it is more lenient and ignores recipes in a world build that
+ are not actually buildable, so here we fail when "bitbake -S none world"
+ fails.
+ '''
+ get_signatures(self.td['builddir'], failsafe=False)
+
+ def test_signatures(self):
+ if self.tc.layer['type'] == LayerType.SOFTWARE and \
+ not self.tc.test_software_layer_signatures:
+ raise unittest.SkipTest("Not testing for signature changes in a software layer %s." \
+ % self.tc.layer['name'])
+
+ curr_sigs, _ = get_signatures(self.td['builddir'], failsafe=True)
+ msg = compare_signatures(self.td['sigs'], curr_sigs)
+ if msg is not None:
+ self.fail('Adding layer %s changed signatures.\n%s' % (self.tc.layer['name'], msg))
diff --git a/scripts/lib/checklayer/cases/distro.py b/scripts/lib/checklayer/cases/distro.py
new file mode 100644
index 00000000000..df1b3035eb3
--- /dev/null
+++ b/scripts/lib/checklayer/cases/distro.py
@@ -0,0 +1,26 @@
+# Copyright (C) 2017 Intel Corporation
+# Released under the MIT license (see COPYING.MIT)
+
+import unittest
+
+from checklayer import LayerType
+from checklayer.case import OECheckLayerTestCase
+
+class DistroCheckLayer(OECheckLayerTestCase):
+ @classmethod
+ def setUpClass(self):
+ if self.tc.layer['type'] != LayerType.DISTRO:
+ raise unittest.SkipTest("DistroCheckLayer: Layer %s isn't Distro one." %\
+ self.tc.layer['name'])
+
+ def test_distro_defines_distros(self):
+ self.assertTrue(self.tc.layer['conf']['distros'],
+ "Layer is BSP but doesn't defines machines.")
+
+ def test_distro_no_set_distros(self):
+ from oeqa.utils.commands import get_bb_var
+
+ distro = get_bb_var('DISTRO')
+ self.assertEqual(self.td['bbvars']['DISTRO'], distro,
+ msg="Layer %s modified distro %s -> %s" % \
+ (self.tc.layer['name'], self.td['bbvars']['DISTRO'], distro))
diff --git a/scripts/lib/checklayer/context.py b/scripts/lib/checklayer/context.py
new file mode 100644
index 00000000000..1bec2c41031
--- /dev/null
+++ b/scripts/lib/checklayer/context.py
@@ -0,0 +1,15 @@
+# Copyright (C) 2017 Intel Corporation
+# Released under the MIT license (see COPYING.MIT)
+
+import os
+import sys
+import glob
+import re
+
+from oeqa.core.context import OETestContext
+
+class CheckLayerTestContext(OETestContext):
+ def __init__(self, td=None, logger=None, layer=None, test_software_layer_signatures=True):
+ super(CheckLayerTestContext, self).__init__(td, logger)
+ self.layer = layer
+ self.test_software_layer_signatures = test_software_layer_signatures