blob: aa946f3036cd14dce89be24c570623b270fe5262 [file] [log] [blame]
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001# Yocto Project layer check tool
2#
3# Copyright (C) 2017 Intel Corporation
Brad Bishopc342db32019-05-15 21:57:59 -04004#
5# SPDX-License-Identifier: MIT
6#
Brad Bishopd7bf8c12018-02-25 22:55:05 -05007
8import os
9import re
10import subprocess
11from enum import Enum
12
13import bb.tinfoil
14
15class LayerType(Enum):
16 BSP = 0
17 DISTRO = 1
18 SOFTWARE = 2
19 ERROR_NO_LAYER_CONF = 98
20 ERROR_BSP_DISTRO = 99
21
22def _get_configurations(path):
23 configs = []
24
25 for f in os.listdir(path):
26 file_path = os.path.join(path, f)
27 if os.path.isfile(file_path) and f.endswith('.conf'):
28 configs.append(f[:-5]) # strip .conf
29 return configs
30
31def _get_layer_collections(layer_path, lconf=None, data=None):
32 import bb.parse
33 import bb.data
34
35 if lconf is None:
36 lconf = os.path.join(layer_path, 'conf', 'layer.conf')
37
38 if data is None:
39 ldata = bb.data.init()
40 bb.parse.init_parser(ldata)
41 else:
42 ldata = data.createCopy()
43
44 ldata.setVar('LAYERDIR', layer_path)
45 try:
46 ldata = bb.parse.handle(lconf, ldata, include=True)
Brad Bishop00111322018-04-01 22:23:53 -040047 except:
48 raise RuntimeError("Parsing of layer.conf from layer: %s failed" % layer_path)
Brad Bishopd7bf8c12018-02-25 22:55:05 -050049 ldata.expandVarref('LAYERDIR')
50
51 collections = (ldata.getVar('BBFILE_COLLECTIONS') or '').split()
52 if not collections:
53 name = os.path.basename(layer_path)
54 collections = [name]
55
56 collections = {c: {} for c in collections}
57 for name in collections:
58 priority = ldata.getVar('BBFILE_PRIORITY_%s' % name)
59 pattern = ldata.getVar('BBFILE_PATTERN_%s' % name)
60 depends = ldata.getVar('LAYERDEPENDS_%s' % name)
Brad Bishop316dfdd2018-06-25 12:45:53 -040061 compat = ldata.getVar('LAYERSERIES_COMPAT_%s' % name)
Andrew Geissler475cb722020-07-10 16:00:51 -050062 try:
63 depDict = bb.utils.explode_dep_versions2(depends or "")
64 except bb.utils.VersionStringException as vse:
65 bb.fatal('Error parsing LAYERDEPENDS_%s: %s' % (name, str(vse)))
66
Brad Bishopd7bf8c12018-02-25 22:55:05 -050067 collections[name]['priority'] = priority
68 collections[name]['pattern'] = pattern
Andrew Geissler475cb722020-07-10 16:00:51 -050069 collections[name]['depends'] = ' '.join(depDict.keys())
Brad Bishop316dfdd2018-06-25 12:45:53 -040070 collections[name]['compat'] = compat
Brad Bishopd7bf8c12018-02-25 22:55:05 -050071
72 return collections
73
74def _detect_layer(layer_path):
75 """
76 Scans layer directory to detect what type of layer
77 is BSP, Distro or Software.
78
79 Returns a dictionary with layer name, type and path.
80 """
81
82 layer = {}
83 layer_name = os.path.basename(layer_path)
84
85 layer['name'] = layer_name
86 layer['path'] = layer_path
87 layer['conf'] = {}
88
89 if not os.path.isfile(os.path.join(layer_path, 'conf', 'layer.conf')):
90 layer['type'] = LayerType.ERROR_NO_LAYER_CONF
91 return layer
92
93 machine_conf = os.path.join(layer_path, 'conf', 'machine')
94 distro_conf = os.path.join(layer_path, 'conf', 'distro')
95
96 is_bsp = False
97 is_distro = False
98
99 if os.path.isdir(machine_conf):
100 machines = _get_configurations(machine_conf)
101 if machines:
102 is_bsp = True
103
104 if os.path.isdir(distro_conf):
105 distros = _get_configurations(distro_conf)
106 if distros:
107 is_distro = True
108
109 if is_bsp and is_distro:
110 layer['type'] = LayerType.ERROR_BSP_DISTRO
111 elif is_bsp:
112 layer['type'] = LayerType.BSP
113 layer['conf']['machines'] = machines
114 elif is_distro:
115 layer['type'] = LayerType.DISTRO
116 layer['conf']['distros'] = distros
117 else:
118 layer['type'] = LayerType.SOFTWARE
119
120 layer['collections'] = _get_layer_collections(layer['path'])
121
122 return layer
123
124def detect_layers(layer_directories, no_auto):
125 layers = []
126
127 for directory in layer_directories:
128 directory = os.path.realpath(directory)
129 if directory[-1] == '/':
130 directory = directory[0:-1]
131
132 if no_auto:
133 conf_dir = os.path.join(directory, 'conf')
134 if os.path.isdir(conf_dir):
135 layer = _detect_layer(directory)
136 if layer:
137 layers.append(layer)
138 else:
139 for root, dirs, files in os.walk(directory):
140 dir_name = os.path.basename(root)
141 conf_dir = os.path.join(root, 'conf')
142 if os.path.isdir(conf_dir):
143 layer = _detect_layer(root)
144 if layer:
145 layers.append(layer)
146
147 return layers
148
Patrick Williams213cb262021-08-07 19:21:33 -0500149def _find_layer(depend, layers):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500150 for layer in layers:
Andrew Geissler1e34c2d2020-05-29 16:02:59 -0500151 if 'collections' not in layer:
152 continue
153
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500154 for collection in layer['collections']:
155 if depend == collection:
156 return layer
157 return None
158
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000159def sanity_check_layers(layers, logger):
160 """
161 Check that we didn't find duplicate collection names, as the layer that will
162 be used is non-deterministic. The precise check is duplicate collections
163 with different patterns, as the same pattern being repeated won't cause
164 problems.
165 """
166 import collections
167
168 passed = True
169 seen = collections.defaultdict(set)
170 for layer in layers:
171 for name, data in layer.get("collections", {}).items():
172 seen[name].add(data["pattern"])
173
174 for name, patterns in seen.items():
175 if len(patterns) > 1:
176 passed = False
177 logger.error("Collection %s found multiple times: %s" % (name, ", ".join(patterns)))
178 return passed
179
Patrick Williams213cb262021-08-07 19:21:33 -0500180def get_layer_dependencies(layer, layers, logger):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500181 def recurse_dependencies(depends, layer, layers, logger, ret = []):
182 logger.debug('Processing dependencies %s for layer %s.' % \
183 (depends, layer['name']))
184
185 for depend in depends.split():
186 # core (oe-core) is suppose to be provided
187 if depend == 'core':
188 continue
189
Patrick Williams213cb262021-08-07 19:21:33 -0500190 layer_depend = _find_layer(depend, layers)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500191 if not layer_depend:
192 logger.error('Layer %s depends on %s and isn\'t found.' % \
193 (layer['name'], depend))
194 ret = None
195 continue
196
197 # We keep processing, even if ret is None, this allows us to report
198 # multiple errors at once
199 if ret is not None and layer_depend not in ret:
200 ret.append(layer_depend)
Brad Bishop004d4992018-10-02 23:54:45 +0200201 else:
202 # we might have processed this dependency already, in which case
203 # we should not do it again (avoid recursive loop)
204 continue
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500205
206 # Recursively process...
207 if 'collections' not in layer_depend:
208 continue
209
210 for collection in layer_depend['collections']:
211 collect_deps = layer_depend['collections'][collection]['depends']
212 if not collect_deps:
213 continue
214 ret = recurse_dependencies(collect_deps, layer_depend, layers, logger, ret)
215
216 return ret
217
218 layer_depends = []
219 for collection in layer['collections']:
220 depends = layer['collections'][collection]['depends']
221 if not depends:
222 continue
223
224 layer_depends = recurse_dependencies(depends, layer, layers, logger, layer_depends)
225
226 # Note: [] (empty) is allowed, None is not!
Patrick Williams213cb262021-08-07 19:21:33 -0500227 return layer_depends
228
229def add_layer_dependencies(bblayersconf, layer, layers, logger):
230
231 layer_depends = get_layer_dependencies(layer, layers, logger)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500232 if layer_depends is None:
233 return False
234 else:
Andrew Geissler99467da2019-02-25 18:54:23 -0600235 add_layers(bblayersconf, layer_depends, logger)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500236
Andrew Geissler99467da2019-02-25 18:54:23 -0600237 return True
238
239def add_layers(bblayersconf, layers, logger):
240 # Don't add a layer that is already present.
241 added = set()
242 output = check_command('Getting existing layers failed.', 'bitbake-layers show-layers').decode('utf-8')
243 for layer, path, pri in re.findall(r'^(\S+) +([^\n]*?) +(\d+)$', output, re.MULTILINE):
244 added.add(path)
245
246 with open(bblayersconf, 'a+') as f:
247 for layer in layers:
248 logger.info('Adding layer %s' % layer['name'])
249 name = layer['name']
250 path = layer['path']
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500251 if path in added:
Andrew Geissler99467da2019-02-25 18:54:23 -0600252 logger.info('%s is already in %s' % (name, bblayersconf))
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500253 else:
254 added.add(path)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500255 f.write("\nBBLAYERS += \"%s\"\n" % path)
256 return True
257
Andrew Geissler635e0e42020-08-21 15:58:33 -0500258def check_bblayers(bblayersconf, layer_path, logger):
259 '''
260 If layer_path found in BBLAYERS return True
261 '''
262 import bb.parse
263 import bb.data
264
265 ldata = bb.parse.handle(bblayersconf, bb.data.init(), include=True)
266 for bblayer in (ldata.getVar('BBLAYERS') or '').split():
267 if os.path.normpath(bblayer) == os.path.normpath(layer_path):
268 return True
269
270 return False
271
Andrew Geissler99467da2019-02-25 18:54:23 -0600272def check_command(error_msg, cmd, cwd=None):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500273 '''
274 Run a command under a shell, capture stdout and stderr in a single stream,
275 throw an error when command returns non-zero exit code. Returns the output.
276 '''
277
Andrew Geissler99467da2019-02-25 18:54:23 -0600278 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500279 output, _ = p.communicate()
280 if p.returncode:
281 msg = "%s\nCommand: %s\nOutput:\n%s" % (error_msg, cmd, output.decode('utf-8'))
282 raise RuntimeError(msg)
283 return output
284
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000285def get_signatures(builddir, failsafe=False, machine=None, extravars=None):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500286 import re
287
288 # some recipes needs to be excluded like meta-world-pkgdata
289 # because a layer can add recipes to a world build so signature
290 # will be change
291 exclude_recipes = ('meta-world-pkgdata',)
292
293 sigs = {}
294 tune2tasks = {}
295
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000296 cmd = 'BB_ENV_PASSTHROUGH_ADDITIONS="$BB_ENV_PASSTHROUGH_ADDITIONS BB_SIGNATURE_HANDLER" BB_SIGNATURE_HANDLER="OEBasicHash" '
297 if extravars:
298 cmd += extravars
299 cmd += ' '
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500300 if machine:
301 cmd += 'MACHINE=%s ' % machine
302 cmd += 'bitbake '
303 if failsafe:
304 cmd += '-k '
305 cmd += '-S none world'
306 sigs_file = os.path.join(builddir, 'locked-sigs.inc')
307 if os.path.exists(sigs_file):
308 os.unlink(sigs_file)
309 try:
310 check_command('Generating signatures failed. This might be due to some parse error and/or general layer incompatibilities.',
Andrew Geissler99467da2019-02-25 18:54:23 -0600311 cmd, builddir)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500312 except RuntimeError as ex:
313 if failsafe and os.path.exists(sigs_file):
314 # Ignore the error here. Most likely some recipes active
315 # in a world build lack some dependencies. There is a
316 # separate test_machine_world_build which exposes the
317 # failure.
318 pass
319 else:
320 raise
321
322 sig_regex = re.compile("^(?P<task>.*:.*):(?P<hash>.*) .$")
323 tune_regex = re.compile("(^|\s)SIGGEN_LOCKEDSIGS_t-(?P<tune>\S*)\s*=\s*")
324 current_tune = None
325 with open(sigs_file, 'r') as f:
326 for line in f.readlines():
327 line = line.strip()
328 t = tune_regex.search(line)
329 if t:
330 current_tune = t.group('tune')
331 s = sig_regex.match(line)
332 if s:
333 exclude = False
334 for er in exclude_recipes:
335 (recipe, task) = s.group('task').split(':')
336 if er == recipe:
337 exclude = True
338 break
339 if exclude:
340 continue
341
342 sigs[s.group('task')] = s.group('hash')
343 tune2tasks.setdefault(current_tune, []).append(s.group('task'))
344
345 if not sigs:
346 raise RuntimeError('Can\'t load signatures from %s' % sigs_file)
347
348 return (sigs, tune2tasks)
349
350def get_depgraph(targets=['world'], failsafe=False):
351 '''
352 Returns the dependency graph for the given target(s).
353 The dependency graph is taken directly from DepTreeEvent.
354 '''
355 depgraph = None
356 with bb.tinfoil.Tinfoil() as tinfoil:
357 tinfoil.prepare(config_only=False)
358 tinfoil.set_event_mask(['bb.event.NoProvider', 'bb.event.DepTreeGenerated', 'bb.command.CommandCompleted'])
359 if not tinfoil.run_command('generateDepTreeEvent', targets, 'do_build'):
360 raise RuntimeError('starting generateDepTreeEvent failed')
361 while True:
362 event = tinfoil.wait_event(timeout=1000)
363 if event:
364 if isinstance(event, bb.command.CommandFailed):
365 raise RuntimeError('Generating dependency information failed: %s' % event.error)
366 elif isinstance(event, bb.command.CommandCompleted):
367 break
368 elif isinstance(event, bb.event.NoProvider):
369 if failsafe:
370 # The event is informational, we will get information about the
371 # remaining dependencies eventually and thus can ignore this
372 # here like we do in get_signatures(), if desired.
373 continue
374 if event._reasons:
375 raise RuntimeError('Nothing provides %s: %s' % (event._item, event._reasons))
376 else:
377 raise RuntimeError('Nothing provides %s.' % (event._item))
378 elif isinstance(event, bb.event.DepTreeGenerated):
379 depgraph = event._depgraph
380
381 if depgraph is None:
382 raise RuntimeError('Could not retrieve the depgraph.')
383 return depgraph
384
385def compare_signatures(old_sigs, curr_sigs):
386 '''
387 Compares the result of two get_signatures() calls. Returns None if no
388 problems found, otherwise a string that can be used as additional
389 explanation in self.fail().
390 '''
391 # task -> (old signature, new signature)
392 sig_diff = {}
393 for task in old_sigs:
394 if task in curr_sigs and \
395 old_sigs[task] != curr_sigs[task]:
396 sig_diff[task] = (old_sigs[task], curr_sigs[task])
397
398 if not sig_diff:
399 return None
400
401 # Beware, depgraph uses task=<pn>.<taskname> whereas get_signatures()
402 # uses <pn>:<taskname>. Need to convert sometimes. The output follows
403 # the convention from get_signatures() because that seems closer to
404 # normal bitbake output.
405 def sig2graph(task):
406 pn, taskname = task.rsplit(':', 1)
407 return pn + '.' + taskname
408 def graph2sig(task):
409 pn, taskname = task.rsplit('.', 1)
410 return pn + ':' + taskname
411 depgraph = get_depgraph(failsafe=True)
412 depends = depgraph['tdepends']
413
414 # If a task A has a changed signature, but none of its
415 # dependencies, then we need to report it because it is
416 # the one which introduces a change. Any task depending on
417 # A (directly or indirectly) will also have a changed
418 # signature, but we don't need to report it. It might have
419 # its own changes, which will become apparent once the
420 # issues that we do report are fixed and the test gets run
421 # again.
422 sig_diff_filtered = []
423 for task, (old_sig, new_sig) in sig_diff.items():
424 deps_tainted = False
425 for dep in depends.get(sig2graph(task), ()):
426 if graph2sig(dep) in sig_diff:
427 deps_tainted = True
428 break
429 if not deps_tainted:
430 sig_diff_filtered.append((task, old_sig, new_sig))
431
432 msg = []
433 msg.append('%d signatures changed, initial differences (first hash before, second after):' %
434 len(sig_diff))
435 for diff in sorted(sig_diff_filtered):
436 recipe, taskname = diff[0].rsplit(':', 1)
437 cmd = 'bitbake-diffsigs --task %s %s --signature %s %s' % \
438 (recipe, taskname, diff[1], diff[2])
439 msg.append(' %s: %s -> %s' % diff)
440 msg.append(' %s' % cmd)
441 try:
442 output = check_command('Determining signature difference failed.',
443 cmd).decode('utf-8')
444 except RuntimeError as error:
445 output = str(error)
446 if output:
447 msg.extend([' ' + line for line in output.splitlines()])
448 msg.append('')
449 return '\n'.join(msg)