summaryrefslogtreecommitdiffstats
path: root/scripts/lib/checklayer
diff options
context:
space:
mode:
authorPaul Eggleton <paul.eggleton@linux.intel.com>2017-09-19 15:57:07 +1200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2017-09-21 11:34:19 +0100
commitb32174e58e89119e3ca2315030629a6580ccbd52 (patch)
tree1be70a32209bd098988f1d60cd7ce496329129d0 /scripts/lib/checklayer
parent455877548e7a685f0dacf3b10056ff85c7aeedf2 (diff)
downloadpoky-b32174e58e89119e3ca2315030629a6580ccbd52.tar.gz
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 (From OE-Core rev: 2a6126a115f10750ea89f95629d3699ad41c5665) Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com> Signed-off-by: Ross Burton <ross.burton@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
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 0000000000..6c2b86a79a
--- /dev/null
+++ b/scripts/lib/checklayer/__init__.py
@@ -0,0 +1,392 @@
1# Yocto Project layer check tool
2#
3# Copyright (C) 2017 Intel Corporation
4# Released under the MIT license (see COPYING.MIT)
5
6import os
7import re
8import subprocess
9from enum import Enum
10
11import bb.tinfoil
12
13class LayerType(Enum):
14 BSP = 0
15 DISTRO = 1
16 SOFTWARE = 2
17 ERROR_NO_LAYER_CONF = 98
18 ERROR_BSP_DISTRO = 99
19
20def _get_configurations(path):
21 configs = []
22
23 for f in os.listdir(path):
24 file_path = os.path.join(path, f)
25 if os.path.isfile(file_path) and f.endswith('.conf'):
26 configs.append(f[:-5]) # strip .conf
27 return configs
28
29def _get_layer_collections(layer_path, lconf=None, data=None):
30 import bb.parse
31 import bb.data
32
33 if lconf is None:
34 lconf = os.path.join(layer_path, 'conf', 'layer.conf')
35
36 if data is None:
37 ldata = bb.data.init()
38 bb.parse.init_parser(ldata)
39 else:
40 ldata = data.createCopy()
41
42 ldata.setVar('LAYERDIR', layer_path)
43 try:
44 ldata = bb.parse.handle(lconf, ldata, include=True)
45 except BaseException as exc:
46 raise LayerError(exc)
47 ldata.expandVarref('LAYERDIR')
48
49 collections = (ldata.getVar('BBFILE_COLLECTIONS', True) or '').split()
50 if not collections:
51 name = os.path.basename(layer_path)
52 collections = [name]
53
54 collections = {c: {} for c in collections}
55 for name in collections:
56 priority = ldata.getVar('BBFILE_PRIORITY_%s' % name, True)
57 pattern = ldata.getVar('BBFILE_PATTERN_%s' % name, True)
58 depends = ldata.getVar('LAYERDEPENDS_%s' % name, True)
59 collections[name]['priority'] = priority
60 collections[name]['pattern'] = pattern
61 collections[name]['depends'] = depends
62
63 return collections
64
65def _detect_layer(layer_path):
66 """
67 Scans layer directory to detect what type of layer
68 is BSP, Distro or Software.
69
70 Returns a dictionary with layer name, type and path.
71 """
72
73 layer = {}
74 layer_name = os.path.basename(layer_path)
75
76 layer['name'] = layer_name
77 layer['path'] = layer_path
78 layer['conf'] = {}
79
80 if not os.path.isfile(os.path.join(layer_path, 'conf', 'layer.conf')):
81 layer['type'] = LayerType.ERROR_NO_LAYER_CONF
82 return layer
83
84 machine_conf = os.path.join(layer_path, 'conf', 'machine')
85 distro_conf = os.path.join(layer_path, 'conf', 'distro')
86
87 is_bsp = False
88 is_distro = False
89
90 if os.path.isdir(machine_conf):
91 machines = _get_configurations(machine_conf)
92 if machines:
93 is_bsp = True
94
95 if os.path.isdir(distro_conf):
96 distros = _get_configurations(distro_conf)
97 if distros:
98 is_distro = True
99
100 if is_bsp and is_distro:
101 layer['type'] = LayerType.ERROR_BSP_DISTRO
102 elif is_bsp:
103 layer['type'] = LayerType.BSP
104 layer['conf']['machines'] = machines
105 elif is_distro:
106 layer['type'] = LayerType.DISTRO
107 layer['conf']['distros'] = distros
108 else:
109 layer['type'] = LayerType.SOFTWARE
110
111 layer['collections'] = _get_layer_collections(layer['path'])
112
113 return layer
114
115def detect_layers(layer_directories, no_auto):
116 layers = []
117
118 for directory in layer_directories:
119 directory = os.path.realpath(directory)
120 if directory[-1] == '/':
121 directory = directory[0:-1]
122
123 if no_auto:
124 conf_dir = os.path.join(directory, 'conf')
125 if os.path.isdir(conf_dir):
126 layer = _detect_layer(directory)
127 if layer:
128 layers.append(layer)
129 else:
130 for root, dirs, files in os.walk(directory):
131 dir_name = os.path.basename(root)
132 conf_dir = os.path.join(root, 'conf')
133 if os.path.isdir(conf_dir):
134 layer = _detect_layer(root)
135 if layer:
136 layers.append(layer)
137
138 return layers
139
140def _find_layer_depends(depend, layers):
141 for layer in layers:
142 for collection in layer['collections']:
143 if depend == collection:
144 return layer
145 return None
146
147def add_layer_dependencies(bblayersconf, layer, layers, logger):
148 def recurse_dependencies(depends, layer, layers, logger, ret = []):
149 logger.debug('Processing dependencies %s for layer %s.' % \
150 (depends, layer['name']))
151
152 for depend in depends.split():
153 # core (oe-core) is suppose to be provided
154 if depend == 'core':
155 continue
156
157 layer_depend = _find_layer_depends(depend, layers)
158 if not layer_depend:
159 logger.error('Layer %s depends on %s and isn\'t found.' % \
160 (layer['name'], depend))
161 ret = None
162 continue
163
164 # We keep processing, even if ret is None, this allows us to report
165 # multiple errors at once
166 if ret is not None and layer_depend not in ret:
167 ret.append(layer_depend)
168
169 # Recursively process...
170 if 'collections' not in layer_depend:
171 continue
172
173 for collection in layer_depend['collections']:
174 collect_deps = layer_depend['collections'][collection]['depends']
175 if not collect_deps:
176 continue
177 ret = recurse_dependencies(collect_deps, layer_depend, layers, logger, ret)
178
179 return ret
180
181 layer_depends = []
182 for collection in layer['collections']:
183 depends = layer['collections'][collection]['depends']
184 if not depends:
185 continue
186
187 layer_depends = recurse_dependencies(depends, layer, layers, logger, layer_depends)
188
189 # Note: [] (empty) is allowed, None is not!
190 if layer_depends is None:
191 return False
192 else:
193 # Don't add a layer that is already present.
194 added = set()
195 output = check_command('Getting existing layers failed.', 'bitbake-layers show-layers').decode('utf-8')
196 for layer, path, pri in re.findall(r'^(\S+) +([^\n]*?) +(\d+)$', output, re.MULTILINE):
197 added.add(path)
198
199 for layer_depend in layer_depends:
200 name = layer_depend['name']
201 path = layer_depend['path']
202 if path in added:
203 continue
204 else:
205 added.add(path)
206 logger.info('Adding layer dependency %s' % name)
207 with open(bblayersconf, 'a+') as f:
208 f.write("\nBBLAYERS += \"%s\"\n" % path)
209 return True
210
211def add_layer(bblayersconf, layer, layers, logger):
212 logger.info('Adding layer %s' % layer['name'])
213 with open(bblayersconf, 'a+') as f:
214 f.write("\nBBLAYERS += \"%s\"\n" % layer['path'])
215
216 return True
217
218def check_command(error_msg, cmd):
219 '''
220 Run a command under a shell, capture stdout and stderr in a single stream,
221 throw an error when command returns non-zero exit code. Returns the output.
222 '''
223
224 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
225 output, _ = p.communicate()
226 if p.returncode:
227 msg = "%s\nCommand: %s\nOutput:\n%s" % (error_msg, cmd, output.decode('utf-8'))
228 raise RuntimeError(msg)
229 return output
230
231def get_signatures(builddir, failsafe=False, machine=None):
232 import re
233
234 # some recipes needs to be excluded like meta-world-pkgdata
235 # because a layer can add recipes to a world build so signature
236 # will be change
237 exclude_recipes = ('meta-world-pkgdata',)
238
239 sigs = {}
240 tune2tasks = {}
241
242 cmd = ''
243 if machine:
244 cmd += 'MACHINE=%s ' % machine
245 cmd += 'bitbake '
246 if failsafe:
247 cmd += '-k '
248 cmd += '-S none world'
249 sigs_file = os.path.join(builddir, 'locked-sigs.inc')
250 if os.path.exists(sigs_file):
251 os.unlink(sigs_file)
252 try:
253 check_command('Generating signatures failed. This might be due to some parse error and/or general layer incompatibilities.',
254 cmd)
255 except RuntimeError as ex:
256 if failsafe and os.path.exists(sigs_file):
257 # Ignore the error here. Most likely some recipes active
258 # in a world build lack some dependencies. There is a
259 # separate test_machine_world_build which exposes the
260 # failure.
261 pass
262 else:
263 raise
264
265 sig_regex = re.compile("^(?P<task>.*:.*):(?P<hash>.*) .$")
266 tune_regex = re.compile("(^|\s)SIGGEN_LOCKEDSIGS_t-(?P<tune>\S*)\s*=\s*")
267 current_tune = None
268 with open(sigs_file, 'r') as f:
269 for line in f.readlines():
270 line = line.strip()
271 t = tune_regex.search(line)
272 if t:
273 current_tune = t.group('tune')
274 s = sig_regex.match(line)
275 if s:
276 exclude = False
277 for er in exclude_recipes:
278 (recipe, task) = s.group('task').split(':')
279 if er == recipe:
280 exclude = True
281 break
282 if exclude:
283 continue
284
285 sigs[s.group('task')] = s.group('hash')
286 tune2tasks.setdefault(current_tune, []).append(s.group('task'))
287
288 if not sigs:
289 raise RuntimeError('Can\'t load signatures from %s' % sigs_file)
290
291 return (sigs, tune2tasks)
292
293def get_depgraph(targets=['world'], failsafe=False):
294 '''
295 Returns the dependency graph for the given target(s).
296 The dependency graph is taken directly from DepTreeEvent.
297 '''
298 depgraph = None
299 with bb.tinfoil.Tinfoil() as tinfoil:
300 tinfoil.prepare(config_only=False)
301 tinfoil.set_event_mask(['bb.event.NoProvider', 'bb.event.DepTreeGenerated', 'bb.command.CommandCompleted'])
302 if not tinfoil.run_command('generateDepTreeEvent', targets, 'do_build'):
303 raise RuntimeError('starting generateDepTreeEvent failed')
304 while True:
305 event = tinfoil.wait_event(timeout=1000)
306 if event:
307 if isinstance(event, bb.command.CommandFailed):
308 raise RuntimeError('Generating dependency information failed: %s' % event.error)
309 elif isinstance(event, bb.command.CommandCompleted):
310 break
311 elif isinstance(event, bb.event.NoProvider):
312 if failsafe:
313 # The event is informational, we will get information about the
314 # remaining dependencies eventually and thus can ignore this
315 # here like we do in get_signatures(), if desired.
316 continue
317 if event._reasons:
318 raise RuntimeError('Nothing provides %s: %s' % (event._item, event._reasons))
319 else:
320 raise RuntimeError('Nothing provides %s.' % (event._item))
321 elif isinstance(event, bb.event.DepTreeGenerated):
322 depgraph = event._depgraph
323
324 if depgraph is None:
325 raise RuntimeError('Could not retrieve the depgraph.')
326 return depgraph
327
328def compare_signatures(old_sigs, curr_sigs):
329 '''
330 Compares the result of two get_signatures() calls. Returns None if no
331 problems found, otherwise a string that can be used as additional
332 explanation in self.fail().
333 '''
334 # task -> (old signature, new signature)
335 sig_diff = {}
336 for task in old_sigs:
337 if task in curr_sigs and \
338 old_sigs[task] != curr_sigs[task]:
339 sig_diff[task] = (old_sigs[task], curr_sigs[task])
340
341 if not sig_diff:
342 return None
343
344 # Beware, depgraph uses task=<pn>.<taskname> whereas get_signatures()
345 # uses <pn>:<taskname>. Need to convert sometimes. The output follows
346 # the convention from get_signatures() because that seems closer to
347 # normal bitbake output.
348 def sig2graph(task):
349 pn, taskname = task.rsplit(':', 1)
350 return pn + '.' + taskname
351 def graph2sig(task):
352 pn, taskname = task.rsplit('.', 1)
353 return pn + ':' + taskname
354 depgraph = get_depgraph(failsafe=True)
355 depends = depgraph['tdepends']
356
357 # If a task A has a changed signature, but none of its
358 # dependencies, then we need to report it because it is
359 # the one which introduces a change. Any task depending on
360 # A (directly or indirectly) will also have a changed
361 # signature, but we don't need to report it. It might have
362 # its own changes, which will become apparent once the
363 # issues that we do report are fixed and the test gets run
364 # again.
365 sig_diff_filtered = []
366 for task, (old_sig, new_sig) in sig_diff.items():
367 deps_tainted = False
368 for dep in depends.get(sig2graph(task), ()):
369 if graph2sig(dep) in sig_diff:
370 deps_tainted = True
371 break
372 if not deps_tainted:
373 sig_diff_filtered.append((task, old_sig, new_sig))
374
375 msg = []
376 msg.append('%d signatures changed, initial differences (first hash before, second after):' %
377 len(sig_diff))
378 for diff in sorted(sig_diff_filtered):
379 recipe, taskname = diff[0].rsplit(':', 1)
380 cmd = 'bitbake-diffsigs --task %s %s --signature %s %s' % \
381 (recipe, taskname, diff[1], diff[2])
382 msg.append(' %s: %s -> %s' % diff)
383 msg.append(' %s' % cmd)
384 try:
385 output = check_command('Determining signature difference failed.',
386 cmd).decode('utf-8')
387 except RuntimeError as error:
388 output = str(error)
389 if output:
390 msg.extend([' ' + line for line in output.splitlines()])
391 msg.append('')
392 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 @@
1# Copyright (C) 2017 Intel Corporation
2# Released under the MIT license (see COPYING.MIT)
3
4from oeqa.core.case import OETestCase
5
6class OECheckLayerTestCase(OETestCase):
7 pass
diff --git a/scripts/lib/checklayer/cases/__init__.py b/scripts/lib/checklayer/cases/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /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 0000000000..b6b611be73
--- /dev/null
+++ b/scripts/lib/checklayer/cases/bsp.py
@@ -0,0 +1,204 @@
1# Copyright (C) 2017 Intel Corporation
2# Released under the MIT license (see COPYING.MIT)
3
4import unittest
5
6from checklayer import LayerType, get_signatures, check_command, get_depgraph
7from checklayer.case import OECheckLayerTestCase
8
9class BSPCheckLayer(OECheckLayerTestCase):
10 @classmethod
11 def setUpClass(self):
12 if self.tc.layer['type'] != LayerType.BSP:
13 raise unittest.SkipTest("BSPCheckLayer: Layer %s isn't BSP one." %\
14 self.tc.layer['name'])
15
16 def test_bsp_defines_machines(self):
17 self.assertTrue(self.tc.layer['conf']['machines'],
18 "Layer is BSP but doesn't defines machines.")
19
20 def test_bsp_no_set_machine(self):
21 from oeqa.utils.commands import get_bb_var
22
23 machine = get_bb_var('MACHINE')
24 self.assertEqual(self.td['bbvars']['MACHINE'], machine,
25 msg="Layer %s modified machine %s -> %s" % \
26 (self.tc.layer['name'], self.td['bbvars']['MACHINE'], machine))
27
28
29 def test_machine_world(self):
30 '''
31 "bitbake world" is expected to work regardless which machine is selected.
32 BSP layers sometimes break that by enabling a recipe for a certain machine
33 without checking whether that recipe actually can be built in the current
34 distro configuration (for example, OpenGL might not enabled).
35
36 This test iterates over all machines. It would be nicer to instantiate
37 it once per machine. It merely checks for errors during parse
38 time. It does not actually attempt to build anything.
39 '''
40
41 if not self.td['machines']:
42 self.skipTest('No machines set with --machines.')
43 msg = []
44 for machine in self.td['machines']:
45 # In contrast to test_machine_signatures() below, errors are fatal here.
46 try:
47 get_signatures(self.td['builddir'], failsafe=False, machine=machine)
48 except RuntimeError as ex:
49 msg.append(str(ex))
50 if msg:
51 msg.insert(0, 'The following machines broke a world build:')
52 self.fail('\n'.join(msg))
53
54 def test_machine_signatures(self):
55 '''
56 Selecting a machine may only affect the signature of tasks that are specific
57 to that machine. In other words, when MACHINE=A and MACHINE=B share a recipe
58 foo and the output of foo, then both machine configurations must build foo
59 in exactly the same way. Otherwise it is not possible to use both machines
60 in the same distribution.
61
62 This criteria can only be tested by testing different machines in combination,
63 i.e. one main layer, potentially several additional BSP layers and an explicit
64 choice of machines:
65 yocto-check-layer --additional-layers .../meta-intel --machines intel-corei7-64 imx6slevk -- .../meta-freescale
66 '''
67
68 if not self.td['machines']:
69 self.skipTest('No machines set with --machines.')
70
71 # Collect signatures for all machines that we are testing
72 # and merge that into a hash:
73 # tune -> task -> signature -> list of machines with that combination
74 #
75 # It is an error if any tune/task pair has more than one signature,
76 # because that implies that the machines that caused those different
77 # signatures do not agree on how to execute the task.
78 tunes = {}
79 # Preserve ordering of machines as chosen by the user.
80 for machine in self.td['machines']:
81 curr_sigs, tune2tasks = get_signatures(self.td['builddir'], failsafe=True, machine=machine)
82 # Invert the tune -> [tasks] mapping.
83 tasks2tune = {}
84 for tune, tasks in tune2tasks.items():
85 for task in tasks:
86 tasks2tune[task] = tune
87 for task, sighash in curr_sigs.items():
88 tunes.setdefault(tasks2tune[task], {}).setdefault(task, {}).setdefault(sighash, []).append(machine)
89
90 msg = []
91 pruned = 0
92 last_line_key = None
93 # do_fetch, do_unpack, ..., do_build
94 taskname_list = []
95 if tunes:
96 # The output below is most useful when we start with tasks that are at
97 # the bottom of the dependency chain, i.e. those that run first. If
98 # those tasks differ, the rest also does.
99 #
100 # To get an ordering of tasks, we do a topological sort of the entire
101 # depgraph for the base configuration, then on-the-fly flatten that list by stripping
102 # out the recipe names and removing duplicates. The base configuration
103 # is not necessarily representative, but should be close enough. Tasks
104 # that were not encountered get a default priority.
105 depgraph = get_depgraph()
106 depends = depgraph['tdepends']
107 WHITE = 1
108 GRAY = 2
109 BLACK = 3
110 color = {}
111 found = set()
112 def visit(task):
113 color[task] = GRAY
114 for dep in depends.get(task, ()):
115 if color.setdefault(dep, WHITE) == WHITE:
116 visit(dep)
117 color[task] = BLACK
118 pn, taskname = task.rsplit('.', 1)
119 if taskname not in found:
120 taskname_list.append(taskname)
121 found.add(taskname)
122 for task in depends.keys():
123 if color.setdefault(task, WHITE) == WHITE:
124 visit(task)
125
126 taskname_order = dict([(task, index) for index, task in enumerate(taskname_list) ])
127 def task_key(task):
128 pn, taskname = task.rsplit(':', 1)
129 return (pn, taskname_order.get(taskname, len(taskname_list)), taskname)
130
131 for tune in sorted(tunes.keys()):
132 tasks = tunes[tune]
133 # As for test_signatures it would be nicer to sort tasks
134 # by dependencies here, but that is harder because we have
135 # to report on tasks from different machines, which might
136 # have different dependencies. We resort to pruning the
137 # output by reporting only one task per recipe if the set
138 # of machines matches.
139 #
140 # "bitbake-diffsigs -t -s" is intelligent enough to print
141 # diffs recursively, so often it does not matter that much
142 # if we don't pick the underlying difference
143 # here. However, sometimes recursion fails
144 # (https://bugzilla.yoctoproject.org/show_bug.cgi?id=6428).
145 #
146 # To mitigate that a bit, we use a hard-coded ordering of
147 # tasks that represents how they normally run and prefer
148 # to print the ones that run first.
149 for task in sorted(tasks.keys(), key=task_key):
150 signatures = tasks[task]
151 # do_build can be ignored: it is know to have
152 # different signatures in some cases, for example in
153 # the allarch ca-certificates due to RDEPENDS=openssl.
154 # That particular dependency is whitelisted via
155 # SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS, but still shows up
156 # in the sstate signature hash because filtering it
157 # out would be hard and running do_build multiple
158 # times doesn't really matter.
159 if len(signatures.keys()) > 1 and \
160 not task.endswith(':do_build'):
161 # Error!
162 #
163 # Sort signatures by machines, because the hex values don't mean anything.
164 # => all-arch adwaita-icon-theme:do_build: 1234... (beaglebone, qemux86) != abcdf... (qemux86-64)
165 #
166 # Skip the line if it is covered already by the predecessor (same pn, same sets of machines).
167 pn, taskname = task.rsplit(':', 1)
168 next_line_key = (pn, sorted(signatures.values()))
169 if next_line_key != last_line_key:
170 line = ' %s %s: ' % (tune, task)
171 line += ' != '.join(['%s (%s)' % (signature, ', '.join([m for m in signatures[signature]])) for
172 signature in sorted(signatures.keys(), key=lambda s: signatures[s])])
173 last_line_key = next_line_key
174 msg.append(line)
175 # Randomly pick two mismatched signatures and remember how to invoke
176 # bitbake-diffsigs for them.
177 iterator = iter(signatures.items())
178 a = next(iterator)
179 b = next(iterator)
180 diffsig_machines = '(%s) != (%s)' % (', '.join(a[1]), ', '.join(b[1]))
181 diffsig_params = '-t %s %s -s %s %s' % (pn, taskname, a[0], b[0])
182 else:
183 pruned += 1
184
185 if msg:
186 msg.insert(0, 'The machines have conflicting signatures for some shared tasks:')
187 if pruned > 0:
188 msg.append('')
189 msg.append('%d tasks where not listed because some other task of the recipe already differed.' % pruned)
190 msg.append('It is likely that differences from different recipes also have the same root cause.')
191 msg.append('')
192 # Explain how to investigate...
193 msg.append('To investigate, run bitbake-diffsigs -t recipename taskname -s fromsig tosig.')
194 cmd = 'bitbake-diffsigs %s' % diffsig_params
195 msg.append('Example: %s in the last line' % diffsig_machines)
196 msg.append('Command: %s' % cmd)
197 # ... and actually do it automatically for that example, but without aborting
198 # when that fails.
199 try:
200 output = check_command('Comparing signatures failed.', cmd).decode('utf-8')
201 except RuntimeError as ex:
202 output = str(ex)
203 msg.extend([' ' + line for line in output.splitlines()])
204 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 @@
1# Copyright (C) 2017 Intel Corporation
2# Released under the MIT license (see COPYING.MIT)
3
4import glob
5import os
6import unittest
7from checklayer import get_signatures, LayerType, check_command, get_depgraph, compare_signatures
8from checklayer.case import OECheckLayerTestCase
9
10class CommonCheckLayer(OECheckLayerTestCase):
11 def test_readme(self):
12 # The top-level README file may have a suffix (like README.rst or README.txt).
13 readme_files = glob.glob(os.path.join(self.tc.layer['path'], 'README*'))
14 self.assertTrue(len(readme_files) > 0,
15 msg="Layer doesn't contains README file.")
16
17 # There might be more than one file matching the file pattern above
18 # (for example, README.rst and README-COPYING.rst). The one with the shortest
19 # name is considered the "main" one.
20 readme_file = sorted(readme_files)[0]
21 data = ''
22 with open(readme_file, 'r') as f:
23 data = f.read()
24 self.assertTrue(data,
25 msg="Layer contains a README file but it is empty.")
26
27 def test_parse(self):
28 check_command('Layer %s failed to parse.' % self.tc.layer['name'],
29 'bitbake -p')
30
31 def test_show_environment(self):
32 check_command('Layer %s failed to show environment.' % self.tc.layer['name'],
33 'bitbake -e')
34
35 def test_world(self):
36 '''
37 "bitbake world" is expected to work. test_signatures does not cover that
38 because it is more lenient and ignores recipes in a world build that
39 are not actually buildable, so here we fail when "bitbake -S none world"
40 fails.
41 '''
42 get_signatures(self.td['builddir'], failsafe=False)
43
44 def test_signatures(self):
45 if self.tc.layer['type'] == LayerType.SOFTWARE and \
46 not self.tc.test_software_layer_signatures:
47 raise unittest.SkipTest("Not testing for signature changes in a software layer %s." \
48 % self.tc.layer['name'])
49
50 curr_sigs, _ = get_signatures(self.td['builddir'], failsafe=True)
51 msg = compare_signatures(self.td['sigs'], curr_sigs)
52 if msg is not None:
53 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 @@
1# Copyright (C) 2017 Intel Corporation
2# Released under the MIT license (see COPYING.MIT)
3
4import unittest
5
6from checklayer import LayerType
7from checklayer.case import OECheckLayerTestCase
8
9class DistroCheckLayer(OECheckLayerTestCase):
10 @classmethod
11 def setUpClass(self):
12 if self.tc.layer['type'] != LayerType.DISTRO:
13 raise unittest.SkipTest("DistroCheckLayer: Layer %s isn't Distro one." %\
14 self.tc.layer['name'])
15
16 def test_distro_defines_distros(self):
17 self.assertTrue(self.tc.layer['conf']['distros'],
18 "Layer is BSP but doesn't defines machines.")
19
20 def test_distro_no_set_distros(self):
21 from oeqa.utils.commands import get_bb_var
22
23 distro = get_bb_var('DISTRO')
24 self.assertEqual(self.td['bbvars']['DISTRO'], distro,
25 msg="Layer %s modified distro %s -> %s" % \
26 (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 @@
1# Copyright (C) 2017 Intel Corporation
2# Released under the MIT license (see COPYING.MIT)
3
4import os
5import sys
6import glob
7import re
8
9from oeqa.core.context import OETestContext
10
11class CheckLayerTestContext(OETestContext):
12 def __init__(self, td=None, logger=None, layer=None, test_software_layer_signatures=True):
13 super(CheckLayerTestContext, self).__init__(td, logger)
14 self.layer = layer
15 self.test_software_layer_signatures = test_software_layer_signatures