From 2a6126a115f10750ea89f95629d3699ad41c5665 Mon Sep 17 00:00:00 2001 From: Paul Eggleton Date: Tue, 19 Sep 2017 15:57:07 +1200 Subject: scripts: rename yocto-compat-layer to remove "compatible" nomenclature "Yocto Project Compatible" [1] is a programme which requires you meet specific criteria including going through an application process - it is not sufficient simply to run the script we have created here and have it produce no warnings/errors. To avoid people being confused by the fact that this script uses the term "compatible" or variations thereof, substitute usage of that word with "check" instead. The functionality of the script is unchanged. [1] https://www.yoctoproject.org/ecosystem/yocto-project-branding-program Signed-off-by: Paul Eggleton Signed-off-by: Ross Burton --- scripts/lib/checklayer/__init__.py | 392 ++++++++++++++++++++++++++++++ scripts/lib/checklayer/case.py | 7 + scripts/lib/checklayer/cases/__init__.py | 0 scripts/lib/checklayer/cases/bsp.py | 204 ++++++++++++++++ scripts/lib/checklayer/cases/common.py | 53 ++++ scripts/lib/checklayer/cases/distro.py | 26 ++ scripts/lib/checklayer/context.py | 15 ++ scripts/lib/compatlayer/__init__.py | 392 ------------------------------ scripts/lib/compatlayer/case.py | 7 - scripts/lib/compatlayer/cases/__init__.py | 0 scripts/lib/compatlayer/cases/bsp.py | 204 ---------------- scripts/lib/compatlayer/cases/common.py | 53 ---- scripts/lib/compatlayer/cases/distro.py | 26 -- scripts/lib/compatlayer/context.py | 15 -- scripts/yocto-check-layer | 208 ++++++++++++++++ scripts/yocto-check-layer-wrapper | 43 ++++ scripts/yocto-compat-layer-wrapper | 43 ---- scripts/yocto-compat-layer.py | 208 ---------------- 18 files changed, 948 insertions(+), 948 deletions(-) create mode 100644 scripts/lib/checklayer/__init__.py create mode 100644 scripts/lib/checklayer/case.py create mode 100644 scripts/lib/checklayer/cases/__init__.py create mode 100644 scripts/lib/checklayer/cases/bsp.py create mode 100644 scripts/lib/checklayer/cases/common.py create mode 100644 scripts/lib/checklayer/cases/distro.py create mode 100644 scripts/lib/checklayer/context.py delete mode 100644 scripts/lib/compatlayer/__init__.py delete mode 100644 scripts/lib/compatlayer/case.py delete mode 100644 scripts/lib/compatlayer/cases/__init__.py delete mode 100644 scripts/lib/compatlayer/cases/bsp.py delete mode 100644 scripts/lib/compatlayer/cases/common.py delete mode 100644 scripts/lib/compatlayer/cases/distro.py delete mode 100644 scripts/lib/compatlayer/context.py create mode 100755 scripts/yocto-check-layer create mode 100755 scripts/yocto-check-layer-wrapper delete mode 100755 scripts/yocto-compat-layer-wrapper delete mode 100755 scripts/yocto-compat-layer.py (limited to 'scripts') diff --git a/scripts/lib/checklayer/__init__.py b/scripts/lib/checklayer/__init__.py new file mode 100644 index 0000000000..6c2b86a79a --- /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.*:.*):(?P.*) .$") + tune_regex = re.compile("(^|\s)SIGGEN_LOCKEDSIGS_t-(?P\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=. whereas get_signatures() + # uses :. 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 0000000000..9dd00412e5 --- /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 0000000000..e69de29bb2 diff --git a/scripts/lib/checklayer/cases/bsp.py b/scripts/lib/checklayer/cases/bsp.py new file mode 100644 index 0000000000..b6b611be73 --- /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 0000000000..a13c1088f0 --- /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 0000000000..df1b3035eb --- /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 0000000000..1bec2c4103 --- /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 diff --git a/scripts/lib/compatlayer/__init__.py b/scripts/lib/compatlayer/__init__.py deleted file mode 100644 index 7197e850e4..0000000000 --- a/scripts/lib/compatlayer/__init__.py +++ /dev/null @@ -1,392 +0,0 @@ -# Yocto Project compatibility layer 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.*:.*):(?P.*) .$") - tune_regex = re.compile("(^|\s)SIGGEN_LOCKEDSIGS_t-(?P\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=. whereas get_signatures() - # uses :. 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/compatlayer/case.py b/scripts/lib/compatlayer/case.py deleted file mode 100644 index 54ce78aa60..0000000000 --- a/scripts/lib/compatlayer/case.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (C) 2017 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -from oeqa.core.case import OETestCase - -class OECompatLayerTestCase(OETestCase): - pass diff --git a/scripts/lib/compatlayer/cases/__init__.py b/scripts/lib/compatlayer/cases/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/scripts/lib/compatlayer/cases/bsp.py b/scripts/lib/compatlayer/cases/bsp.py deleted file mode 100644 index 43efae406f..0000000000 --- a/scripts/lib/compatlayer/cases/bsp.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright (C) 2017 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -import unittest - -from compatlayer import LayerType, get_signatures, check_command, get_depgraph -from compatlayer.case import OECompatLayerTestCase - -class BSPCompatLayer(OECompatLayerTestCase): - @classmethod - def setUpClass(self): - if self.tc.layer['type'] != LayerType.BSP: - raise unittest.SkipTest("BSPCompatLayer: 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-compat-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/compatlayer/cases/common.py b/scripts/lib/compatlayer/cases/common.py deleted file mode 100644 index 55e8ba4c51..0000000000 --- a/scripts/lib/compatlayer/cases/common.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright (C) 2017 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -import glob -import os -import unittest -from compatlayer import get_signatures, LayerType, check_command, get_depgraph, compare_signatures -from compatlayer.case import OECompatLayerTestCase - -class CommonCompatLayer(OECompatLayerTestCase): - 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/compatlayer/cases/distro.py b/scripts/lib/compatlayer/cases/distro.py deleted file mode 100644 index 523acc1e78..0000000000 --- a/scripts/lib/compatlayer/cases/distro.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (C) 2017 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -import unittest - -from compatlayer import LayerType -from compatlayer.case import OECompatLayerTestCase - -class DistroCompatLayer(OECompatLayerTestCase): - @classmethod - def setUpClass(self): - if self.tc.layer['type'] != LayerType.DISTRO: - raise unittest.SkipTest("DistroCompatLayer: 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/compatlayer/context.py b/scripts/lib/compatlayer/context.py deleted file mode 100644 index 7811d4ac20..0000000000 --- a/scripts/lib/compatlayer/context.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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 CompatLayerTestContext(OETestContext): - def __init__(self, td=None, logger=None, layer=None, test_software_layer_signatures=True): - super(CompatLayerTestContext, self).__init__(td, logger) - self.layer = layer - self.test_software_layer_signatures = test_software_layer_signatures diff --git a/scripts/yocto-check-layer b/scripts/yocto-check-layer new file mode 100755 index 0000000000..5a4fd752ca --- /dev/null +++ b/scripts/yocto-check-layer @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 + +# Yocto Project layer checking tool +# +# Copyright (C) 2017 Intel Corporation +# Released under the MIT license (see COPYING.MIT) + +import os +import sys +import argparse +import logging +import time +import signal +import shutil +import collections + +scripts_path = os.path.dirname(os.path.realpath(__file__)) +lib_path = scripts_path + '/lib' +sys.path = sys.path + [lib_path] +import scriptutils +import scriptpath +scriptpath.add_oe_lib_path() +scriptpath.add_bitbake_lib_path() + +from checklayer import LayerType, detect_layers, add_layer, add_layer_dependencies, get_signatures +from oeqa.utils.commands import get_bb_vars + +PROGNAME = 'yocto-check-layer' +CASES_PATHS = [os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'lib', 'checklayer', 'cases')] +logger = scriptutils.logger_create(PROGNAME, stream=sys.stdout) + +def test_layer(td, layer, test_software_layer_signatures): + from checklayer.context import CheckLayerTestContext + logger.info("Starting to analyze: %s" % layer['name']) + logger.info("----------------------------------------------------------------------") + + tc = CheckLayerTestContext(td=td, logger=logger, layer=layer, test_software_layer_signatures=test_software_layer_signatures) + tc.loadTests(CASES_PATHS) + return tc.runTests() + +def main(): + parser = argparse.ArgumentParser( + description="Yocto Project layer checking tool", + add_help=False) + parser.add_argument('layers', metavar='LAYER_DIR', nargs='+', + help='Layer to check') + parser.add_argument('-o', '--output-log', + help='File to output log (optional)', action='store') + parser.add_argument('--dependency', nargs="+", + help='Layers to process for dependencies', action='store') + parser.add_argument('--machines', nargs="+", + help='List of MACHINEs to be used during testing', action='store') + parser.add_argument('--additional-layers', nargs="+", + help='List of additional layers to add during testing', action='store') + group = parser.add_mutually_exclusive_group() + group.add_argument('--with-software-layer-signature-check', action='store_true', dest='test_software_layer_signatures', + default=True, + help='check that software layers do not change signatures (on by default)') + group.add_argument('--without-software-layer-signature-check', action='store_false', dest='test_software_layer_signatures', + help='disable signature checking for software layers') + parser.add_argument('-n', '--no-auto', help='Disable auto layer discovery', + action='store_true') + parser.add_argument('-d', '--debug', help='Enable debug output', + action='store_true') + parser.add_argument('-q', '--quiet', help='Print only errors', + action='store_true') + + parser.add_argument('-h', '--help', action='help', + default=argparse.SUPPRESS, + help='show this help message and exit') + + args = parser.parse_args() + + if args.output_log: + fh = logging.FileHandler(args.output_log) + fh.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + logger.addHandler(fh) + if args.debug: + logger.setLevel(logging.DEBUG) + elif args.quiet: + logger.setLevel(logging.ERROR) + + if not 'BUILDDIR' in os.environ: + logger.error("You must source the environment before run this script.") + logger.error("$ source oe-init-build-env") + return 1 + builddir = os.environ['BUILDDIR'] + bblayersconf = os.path.join(builddir, 'conf', 'bblayers.conf') + + layers = detect_layers(args.layers, args.no_auto) + if not layers: + logger.error("Fail to detect layers") + return 1 + if args.additional_layers: + additional_layers = detect_layers(args.additional_layers, args.no_auto) + else: + additional_layers = [] + if args.dependency: + dep_layers = detect_layers(args.dependency, args.no_auto) + dep_layers = dep_layers + layers + else: + dep_layers = layers + + logger.info("Detected layers:") + for layer in layers: + if layer['type'] == LayerType.ERROR_BSP_DISTRO: + logger.error("%s: Can't be DISTRO and BSP type at the same time."\ + " The conf/distro and conf/machine folders was found."\ + % layer['name']) + layers.remove(layer) + elif layer['type'] == LayerType.ERROR_NO_LAYER_CONF: + logger.error("%s: Don't have conf/layer.conf file."\ + % layer['name']) + layers.remove(layer) + else: + logger.info("%s: %s, %s" % (layer['name'], layer['type'], + layer['path'])) + if not layers: + return 1 + + shutil.copyfile(bblayersconf, bblayersconf + '.backup') + def cleanup_bblayers(signum, frame): + shutil.copyfile(bblayersconf + '.backup', bblayersconf) + os.unlink(bblayersconf + '.backup') + signal.signal(signal.SIGTERM, cleanup_bblayers) + signal.signal(signal.SIGINT, cleanup_bblayers) + + td = {} + results = collections.OrderedDict() + results_status = collections.OrderedDict() + + layers_tested = 0 + for layer in layers: + if layer['type'] == LayerType.ERROR_NO_LAYER_CONF or \ + layer['type'] == LayerType.ERROR_BSP_DISTRO: + continue + + logger.info('') + logger.info("Setting up for %s(%s), %s" % (layer['name'], layer['type'], + layer['path'])) + + shutil.copyfile(bblayersconf + '.backup', bblayersconf) + + missing_dependencies = not add_layer_dependencies(bblayersconf, layer, dep_layers, logger) + if not missing_dependencies: + for additional_layer in additional_layers: + if not add_layer_dependencies(bblayersconf, additional_layer, dep_layers, logger): + missing_dependencies = True + break + if not add_layer_dependencies(bblayersconf, layer, dep_layers, logger) or \ + any(map(lambda additional_layer: not add_layer_dependencies(bblayersconf, additional_layer, dep_layers, logger), + additional_layers)): + logger.info('Skipping %s due to missing dependencies.' % layer['name']) + results[layer['name']] = None + results_status[layer['name']] = 'SKIPPED (Missing dependencies)' + layers_tested = layers_tested + 1 + continue + + if any(map(lambda additional_layer: not add_layer(bblayersconf, additional_layer, dep_layers, logger), + additional_layers)): + logger.info('Skipping %s due to missing additional layers.' % layer['name']) + results[layer['name']] = None + results_status[layer['name']] = 'SKIPPED (Missing additional layers)' + layers_tested = layers_tested + 1 + continue + + logger.info('Getting initial bitbake variables ...') + td['bbvars'] = get_bb_vars() + logger.info('Getting initial signatures ...') + td['builddir'] = builddir + td['sigs'], td['tunetasks'] = get_signatures(td['builddir']) + td['machines'] = args.machines + + if not add_layer(bblayersconf, layer, dep_layers, logger): + logger.info('Skipping %s ???.' % layer['name']) + results[layer['name']] = None + results_status[layer['name']] = 'SKIPPED (Unknown)' + layers_tested = layers_tested + 1 + continue + + result = test_layer(td, layer, args.test_software_layer_signatures) + results[layer['name']] = result + results_status[layer['name']] = 'PASS' if results[layer['name']].wasSuccessful() else 'FAIL' + layers_tested = layers_tested + 1 + + ret = 0 + if layers_tested: + logger.info('') + logger.info('Summary of results:') + logger.info('') + for layer_name in results_status: + logger.info('%s ... %s' % (layer_name, results_status[layer_name])) + if not results[layer_name] or not results[layer_name].wasSuccessful(): + ret = 2 # ret = 1 used for initialization errors + + cleanup_bblayers(None, None) + + return ret + +if __name__ == '__main__': + try: + ret = main() + except Exception: + ret = 1 + import traceback + traceback.print_exc() + sys.exit(ret) diff --git a/scripts/yocto-check-layer-wrapper b/scripts/yocto-check-layer-wrapper new file mode 100755 index 0000000000..bbf6ee176d --- /dev/null +++ b/scripts/yocto-check-layer-wrapper @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# Yocto Project layer check tool wrapper +# +# Creates a temporary build directory to run the yocto-check-layer +# script to avoid a contaminated environment. +# +# Copyright (C) 2017 Intel Corporation +# Released under the MIT license (see COPYING.MIT) + +if [ -z "$BUILDDIR" ]; then + echo "Please source oe-init-build-env before run this script." + exit 2 +fi + +# since we are using a temp directory, use the realpath for output +# log option +output_log='' +while getopts o: name +do + case $name in + o) output_log=$(realpath "$OPTARG") + esac +done +shift $(($OPTIND - 1)) + +# generate a temp directory to run check layer script +base_dir=$(realpath $BUILDDIR/../) +cd $base_dir + +build_dir=$(mktemp -p $base_dir -d -t build-XXXX) + +source oe-init-build-env $build_dir +if [[ $output_log != '' ]]; then + yocto-check-layer -o "$output_log" "$*" +else + yocto-check-layer "$@" +fi +retcode=$? + +rm -rf $build_dir + +exit $retcode diff --git a/scripts/yocto-compat-layer-wrapper b/scripts/yocto-compat-layer-wrapper deleted file mode 100755 index b6baeb484e..0000000000 --- a/scripts/yocto-compat-layer-wrapper +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -# Yocto Project compatibility layer tool wrapper -# -# Creates a temprary build directory to run Yocto Project Compatible -# script to avoid a contaminated environment. -# -# Copyright (C) 2017 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -if [ -z "$BUILDDIR" ]; then - echo "Please source oe-init-build-env before run this script." - exit 2 -fi - -# since we are using a temp directory, use the realpath for output -# log option -output_log='' -while getopts o: name -do - case $name in - o) output_log=$(realpath "$OPTARG") - esac -done -shift $(($OPTIND - 1)) - -# generate a temp directory to run compat layer script -base_dir=$(realpath $BUILDDIR/../) -cd $base_dir - -build_dir=$(mktemp -p $base_dir -d -t build-XXXX) - -source oe-init-build-env $build_dir -if [[ $output_log != '' ]]; then - yocto-compat-layer.py -o "$output_log" "$*" -else - yocto-compat-layer.py "$@" -fi -retcode=$? - -rm -rf $build_dir - -exit $retcode diff --git a/scripts/yocto-compat-layer.py b/scripts/yocto-compat-layer.py deleted file mode 100755 index 9a541c66e5..0000000000 --- a/scripts/yocto-compat-layer.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python3 - -# Yocto Project compatibility layer tool -# -# Copyright (C) 2017 Intel Corporation -# Released under the MIT license (see COPYING.MIT) - -import os -import sys -import argparse -import logging -import time -import signal -import shutil -import collections - -scripts_path = os.path.dirname(os.path.realpath(__file__)) -lib_path = scripts_path + '/lib' -sys.path = sys.path + [lib_path] -import scriptutils -import scriptpath -scriptpath.add_oe_lib_path() -scriptpath.add_bitbake_lib_path() - -from compatlayer import LayerType, detect_layers, add_layer, add_layer_dependencies, get_signatures -from oeqa.utils.commands import get_bb_vars - -PROGNAME = 'yocto-compat-layer' -CASES_PATHS = [os.path.join(os.path.abspath(os.path.dirname(__file__)), - 'lib', 'compatlayer', 'cases')] -logger = scriptutils.logger_create(PROGNAME, stream=sys.stdout) - -def test_layer_compatibility(td, layer, test_software_layer_signatures): - from compatlayer.context import CompatLayerTestContext - logger.info("Starting to analyze: %s" % layer['name']) - logger.info("----------------------------------------------------------------------") - - tc = CompatLayerTestContext(td=td, logger=logger, layer=layer, test_software_layer_signatures=test_software_layer_signatures) - tc.loadTests(CASES_PATHS) - return tc.runTests() - -def main(): - parser = argparse.ArgumentParser( - description="Yocto Project compatibility layer tool", - add_help=False) - parser.add_argument('layers', metavar='LAYER_DIR', nargs='+', - help='Layer to test compatibility with Yocto Project') - parser.add_argument('-o', '--output-log', - help='File to output log (optional)', action='store') - parser.add_argument('--dependency', nargs="+", - help='Layers to process for dependencies', action='store') - parser.add_argument('--machines', nargs="+", - help='List of MACHINEs to be used during testing', action='store') - parser.add_argument('--additional-layers', nargs="+", - help='List of additional layers to add during testing', action='store') - group = parser.add_mutually_exclusive_group() - group.add_argument('--with-software-layer-signature-check', action='store_true', dest='test_software_layer_signatures', - default=True, - help='check that software layers do not change signatures (on by default)') - group.add_argument('--without-software-layer-signature-check', action='store_false', dest='test_software_layer_signatures', - help='disable signature checking for software layers') - parser.add_argument('-n', '--no-auto', help='Disable auto layer discovery', - action='store_true') - parser.add_argument('-d', '--debug', help='Enable debug output', - action='store_true') - parser.add_argument('-q', '--quiet', help='Print only errors', - action='store_true') - - parser.add_argument('-h', '--help', action='help', - default=argparse.SUPPRESS, - help='show this help message and exit') - - args = parser.parse_args() - - if args.output_log: - fh = logging.FileHandler(args.output_log) - fh.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) - logger.addHandler(fh) - if args.debug: - logger.setLevel(logging.DEBUG) - elif args.quiet: - logger.setLevel(logging.ERROR) - - if not 'BUILDDIR' in os.environ: - logger.error("You must source the environment before run this script.") - logger.error("$ source oe-init-build-env") - return 1 - builddir = os.environ['BUILDDIR'] - bblayersconf = os.path.join(builddir, 'conf', 'bblayers.conf') - - layers = detect_layers(args.layers, args.no_auto) - if not layers: - logger.error("Fail to detect layers") - return 1 - if args.additional_layers: - additional_layers = detect_layers(args.additional_layers, args.no_auto) - else: - additional_layers = [] - if args.dependency: - dep_layers = detect_layers(args.dependency, args.no_auto) - dep_layers = dep_layers + layers - else: - dep_layers = layers - - logger.info("Detected layers:") - for layer in layers: - if layer['type'] == LayerType.ERROR_BSP_DISTRO: - logger.error("%s: Can't be DISTRO and BSP type at the same time."\ - " The conf/distro and conf/machine folders was found."\ - % layer['name']) - layers.remove(layer) - elif layer['type'] == LayerType.ERROR_NO_LAYER_CONF: - logger.error("%s: Don't have conf/layer.conf file."\ - % layer['name']) - layers.remove(layer) - else: - logger.info("%s: %s, %s" % (layer['name'], layer['type'], - layer['path'])) - if not layers: - return 1 - - shutil.copyfile(bblayersconf, bblayersconf + '.backup') - def cleanup_bblayers(signum, frame): - shutil.copyfile(bblayersconf + '.backup', bblayersconf) - os.unlink(bblayersconf + '.backup') - signal.signal(signal.SIGTERM, cleanup_bblayers) - signal.signal(signal.SIGINT, cleanup_bblayers) - - td = {} - results = collections.OrderedDict() - results_status = collections.OrderedDict() - - layers_tested = 0 - for layer in layers: - if layer['type'] == LayerType.ERROR_NO_LAYER_CONF or \ - layer['type'] == LayerType.ERROR_BSP_DISTRO: - continue - - logger.info('') - logger.info("Setting up for %s(%s), %s" % (layer['name'], layer['type'], - layer['path'])) - - shutil.copyfile(bblayersconf + '.backup', bblayersconf) - - missing_dependencies = not add_layer_dependencies(bblayersconf, layer, dep_layers, logger) - if not missing_dependencies: - for additional_layer in additional_layers: - if not add_layer_dependencies(bblayersconf, additional_layer, dep_layers, logger): - missing_dependencies = True - break - if not add_layer_dependencies(bblayersconf, layer, dep_layers, logger) or \ - any(map(lambda additional_layer: not add_layer_dependencies(bblayersconf, additional_layer, dep_layers, logger), - additional_layers)): - logger.info('Skipping %s due to missing dependencies.' % layer['name']) - results[layer['name']] = None - results_status[layer['name']] = 'SKIPPED (Missing dependencies)' - layers_tested = layers_tested + 1 - continue - - if any(map(lambda additional_layer: not add_layer(bblayersconf, additional_layer, dep_layers, logger), - additional_layers)): - logger.info('Skipping %s due to missing additional layers.' % layer['name']) - results[layer['name']] = None - results_status[layer['name']] = 'SKIPPED (Missing additional layers)' - layers_tested = layers_tested + 1 - continue - - logger.info('Getting initial bitbake variables ...') - td['bbvars'] = get_bb_vars() - logger.info('Getting initial signatures ...') - td['builddir'] = builddir - td['sigs'], td['tunetasks'] = get_signatures(td['builddir']) - td['machines'] = args.machines - - if not add_layer(bblayersconf, layer, dep_layers, logger): - logger.info('Skipping %s ???.' % layer['name']) - results[layer['name']] = None - results_status[layer['name']] = 'SKIPPED (Unknown)' - layers_tested = layers_tested + 1 - continue - - result = test_layer_compatibility(td, layer, args.test_software_layer_signatures) - results[layer['name']] = result - results_status[layer['name']] = 'PASS' if results[layer['name']].wasSuccessful() else 'FAIL' - layers_tested = layers_tested + 1 - - ret = 0 - if layers_tested: - logger.info('') - logger.info('Summary of results:') - logger.info('') - for layer_name in results_status: - logger.info('%s ... %s' % (layer_name, results_status[layer_name])) - if not results[layer_name] or not results[layer_name].wasSuccessful(): - ret = 2 # ret = 1 used for initialization errors - - cleanup_bblayers(None, None) - - return ret - -if __name__ == '__main__': - try: - ret = main() - except Exception: - ret = 1 - import traceback - traceback.print_exc() - sys.exit(ret) -- cgit 1.2.3-korg