xref: /openbmc/openbmc/poky/scripts/lib/checklayer/__init__.py (revision 8460358c3d24c71d9d38fd126c745854a6301564)
1eb8dc403SDave Cobbley# Yocto Project layer check tool
2eb8dc403SDave Cobbley#
3eb8dc403SDave Cobbley# Copyright (C) 2017 Intel Corporation
4c342db35SBrad Bishop#
5c342db35SBrad Bishop# SPDX-License-Identifier: MIT
6c342db35SBrad Bishop#
7eb8dc403SDave Cobbley
8eb8dc403SDave Cobbleyimport os
9eb8dc403SDave Cobbleyimport re
10eb8dc403SDave Cobbleyimport subprocess
11eb8dc403SDave Cobbleyfrom enum import Enum
12eb8dc403SDave Cobbley
13eb8dc403SDave Cobbleyimport bb.tinfoil
14eb8dc403SDave Cobbley
15eb8dc403SDave Cobbleyclass LayerType(Enum):
16eb8dc403SDave Cobbley    BSP = 0
17eb8dc403SDave Cobbley    DISTRO = 1
18eb8dc403SDave Cobbley    SOFTWARE = 2
19517393d9SAndrew Geissler    CORE = 3
20eb8dc403SDave Cobbley    ERROR_NO_LAYER_CONF = 98
21eb8dc403SDave Cobbley    ERROR_BSP_DISTRO = 99
22eb8dc403SDave Cobbley
23eb8dc403SDave Cobbleydef _get_configurations(path):
24eb8dc403SDave Cobbley    configs = []
25eb8dc403SDave Cobbley
26eb8dc403SDave Cobbley    for f in os.listdir(path):
27eb8dc403SDave Cobbley        file_path = os.path.join(path, f)
28eb8dc403SDave Cobbley        if os.path.isfile(file_path) and f.endswith('.conf'):
29eb8dc403SDave Cobbley            configs.append(f[:-5]) # strip .conf
30eb8dc403SDave Cobbley    return configs
31eb8dc403SDave Cobbley
32eb8dc403SDave Cobbleydef _get_layer_collections(layer_path, lconf=None, data=None):
33eb8dc403SDave Cobbley    import bb.parse
34eb8dc403SDave Cobbley    import bb.data
35eb8dc403SDave Cobbley
36eb8dc403SDave Cobbley    if lconf is None:
37eb8dc403SDave Cobbley        lconf = os.path.join(layer_path, 'conf', 'layer.conf')
38eb8dc403SDave Cobbley
39eb8dc403SDave Cobbley    if data is None:
40eb8dc403SDave Cobbley        ldata = bb.data.init()
41eb8dc403SDave Cobbley        bb.parse.init_parser(ldata)
42eb8dc403SDave Cobbley    else:
43eb8dc403SDave Cobbley        ldata = data.createCopy()
44eb8dc403SDave Cobbley
45eb8dc403SDave Cobbley    ldata.setVar('LAYERDIR', layer_path)
46eb8dc403SDave Cobbley    try:
47517393d9SAndrew Geissler        ldata = bb.parse.handle(lconf, ldata, include=True, baseconfig=True)
48eb8dc403SDave Cobbley    except:
49eb8dc403SDave Cobbley        raise RuntimeError("Parsing of layer.conf from layer: %s failed" % layer_path)
50eb8dc403SDave Cobbley    ldata.expandVarref('LAYERDIR')
51eb8dc403SDave Cobbley
52eb8dc403SDave Cobbley    collections = (ldata.getVar('BBFILE_COLLECTIONS') or '').split()
53eb8dc403SDave Cobbley    if not collections:
54eb8dc403SDave Cobbley        name = os.path.basename(layer_path)
55eb8dc403SDave Cobbley        collections = [name]
56eb8dc403SDave Cobbley
57eb8dc403SDave Cobbley    collections = {c: {} for c in collections}
58eb8dc403SDave Cobbley    for name in collections:
59eb8dc403SDave Cobbley        priority = ldata.getVar('BBFILE_PRIORITY_%s' % name)
60eb8dc403SDave Cobbley        pattern = ldata.getVar('BBFILE_PATTERN_%s' % name)
61eb8dc403SDave Cobbley        depends = ldata.getVar('LAYERDEPENDS_%s' % name)
62eb8dc403SDave Cobbley        compat = ldata.getVar('LAYERSERIES_COMPAT_%s' % name)
63475cb72dSAndrew Geissler        try:
64475cb72dSAndrew Geissler            depDict = bb.utils.explode_dep_versions2(depends or "")
65475cb72dSAndrew Geissler        except bb.utils.VersionStringException as vse:
66475cb72dSAndrew Geissler            bb.fatal('Error parsing LAYERDEPENDS_%s: %s' % (name, str(vse)))
67475cb72dSAndrew Geissler
68eb8dc403SDave Cobbley        collections[name]['priority'] = priority
69eb8dc403SDave Cobbley        collections[name]['pattern'] = pattern
70475cb72dSAndrew Geissler        collections[name]['depends'] = ' '.join(depDict.keys())
71eb8dc403SDave Cobbley        collections[name]['compat'] = compat
72eb8dc403SDave Cobbley
73eb8dc403SDave Cobbley    return collections
74eb8dc403SDave Cobbley
75eb8dc403SDave Cobbleydef _detect_layer(layer_path):
76eb8dc403SDave Cobbley    """
77eb8dc403SDave Cobbley        Scans layer directory to detect what type of layer
78eb8dc403SDave Cobbley        is BSP, Distro or Software.
79eb8dc403SDave Cobbley
80eb8dc403SDave Cobbley        Returns a dictionary with layer name, type and path.
81eb8dc403SDave Cobbley    """
82eb8dc403SDave Cobbley
83eb8dc403SDave Cobbley    layer = {}
84eb8dc403SDave Cobbley    layer_name = os.path.basename(layer_path)
85eb8dc403SDave Cobbley
86eb8dc403SDave Cobbley    layer['name'] = layer_name
87eb8dc403SDave Cobbley    layer['path'] = layer_path
88eb8dc403SDave Cobbley    layer['conf'] = {}
89eb8dc403SDave Cobbley
90eb8dc403SDave Cobbley    if not os.path.isfile(os.path.join(layer_path, 'conf', 'layer.conf')):
91eb8dc403SDave Cobbley        layer['type'] = LayerType.ERROR_NO_LAYER_CONF
92eb8dc403SDave Cobbley        return layer
93eb8dc403SDave Cobbley
94eb8dc403SDave Cobbley    machine_conf = os.path.join(layer_path, 'conf', 'machine')
95eb8dc403SDave Cobbley    distro_conf = os.path.join(layer_path, 'conf', 'distro')
96eb8dc403SDave Cobbley
97eb8dc403SDave Cobbley    is_bsp = False
98eb8dc403SDave Cobbley    is_distro = False
99eb8dc403SDave Cobbley
100eb8dc403SDave Cobbley    if os.path.isdir(machine_conf):
101eb8dc403SDave Cobbley        machines = _get_configurations(machine_conf)
102eb8dc403SDave Cobbley        if machines:
103eb8dc403SDave Cobbley            is_bsp = True
104eb8dc403SDave Cobbley
105eb8dc403SDave Cobbley    if os.path.isdir(distro_conf):
106eb8dc403SDave Cobbley        distros = _get_configurations(distro_conf)
107eb8dc403SDave Cobbley        if distros:
108eb8dc403SDave Cobbley            is_distro = True
109eb8dc403SDave Cobbley
110517393d9SAndrew Geissler    layer['collections'] = _get_layer_collections(layer['path'])
111517393d9SAndrew Geissler
112517393d9SAndrew Geissler    if layer_name == "meta" and "core" in layer['collections']:
113517393d9SAndrew Geissler        layer['type'] = LayerType.CORE
114517393d9SAndrew Geissler        layer['conf']['machines'] = machines
115517393d9SAndrew Geissler        layer['conf']['distros'] = distros
116517393d9SAndrew Geissler    elif is_bsp and is_distro:
117eb8dc403SDave Cobbley        layer['type'] = LayerType.ERROR_BSP_DISTRO
118eb8dc403SDave Cobbley    elif is_bsp:
119eb8dc403SDave Cobbley        layer['type'] = LayerType.BSP
120eb8dc403SDave Cobbley        layer['conf']['machines'] = machines
121eb8dc403SDave Cobbley    elif is_distro:
122eb8dc403SDave Cobbley        layer['type'] = LayerType.DISTRO
123eb8dc403SDave Cobbley        layer['conf']['distros'] = distros
124eb8dc403SDave Cobbley    else:
125eb8dc403SDave Cobbley        layer['type'] = LayerType.SOFTWARE
126eb8dc403SDave Cobbley
127eb8dc403SDave Cobbley    return layer
128eb8dc403SDave Cobbley
129eb8dc403SDave Cobbleydef detect_layers(layer_directories, no_auto):
130eb8dc403SDave Cobbley    layers = []
131eb8dc403SDave Cobbley
132eb8dc403SDave Cobbley    for directory in layer_directories:
133eb8dc403SDave Cobbley        directory = os.path.realpath(directory)
134eb8dc403SDave Cobbley        if directory[-1] == '/':
135eb8dc403SDave Cobbley            directory = directory[0:-1]
136eb8dc403SDave Cobbley
137eb8dc403SDave Cobbley        if no_auto:
138eb8dc403SDave Cobbley            conf_dir = os.path.join(directory, 'conf')
139eb8dc403SDave Cobbley            if os.path.isdir(conf_dir):
140eb8dc403SDave Cobbley                layer = _detect_layer(directory)
141eb8dc403SDave Cobbley                if layer:
142eb8dc403SDave Cobbley                    layers.append(layer)
143eb8dc403SDave Cobbley        else:
144eb8dc403SDave Cobbley            for root, dirs, files in os.walk(directory):
145eb8dc403SDave Cobbley                dir_name = os.path.basename(root)
146eb8dc403SDave Cobbley                conf_dir = os.path.join(root, 'conf')
147eb8dc403SDave Cobbley                if os.path.isdir(conf_dir):
148eb8dc403SDave Cobbley                    layer = _detect_layer(root)
149eb8dc403SDave Cobbley                    if layer:
150eb8dc403SDave Cobbley                        layers.append(layer)
151eb8dc403SDave Cobbley
152eb8dc403SDave Cobbley    return layers
153eb8dc403SDave Cobbley
154213cb269SPatrick Williamsdef _find_layer(depend, layers):
155eb8dc403SDave Cobbley    for layer in layers:
1561e34c2d0SAndrew Geissler        if 'collections' not in layer:
1571e34c2d0SAndrew Geissler            continue
1581e34c2d0SAndrew Geissler
159eb8dc403SDave Cobbley        for collection in layer['collections']:
160eb8dc403SDave Cobbley            if depend == collection:
161eb8dc403SDave Cobbley                return layer
162eb8dc403SDave Cobbley    return None
163eb8dc403SDave Cobbley
1647e0e3c0cSAndrew Geisslerdef sanity_check_layers(layers, logger):
1657e0e3c0cSAndrew Geissler    """
1667e0e3c0cSAndrew Geissler    Check that we didn't find duplicate collection names, as the layer that will
1677e0e3c0cSAndrew Geissler    be used is non-deterministic. The precise check is duplicate collections
1687e0e3c0cSAndrew Geissler    with different patterns, as the same pattern being repeated won't cause
1697e0e3c0cSAndrew Geissler    problems.
1707e0e3c0cSAndrew Geissler    """
1717e0e3c0cSAndrew Geissler    import collections
1727e0e3c0cSAndrew Geissler
1737e0e3c0cSAndrew Geissler    passed = True
1747e0e3c0cSAndrew Geissler    seen = collections.defaultdict(set)
1757e0e3c0cSAndrew Geissler    for layer in layers:
1767e0e3c0cSAndrew Geissler        for name, data in layer.get("collections", {}).items():
1777e0e3c0cSAndrew Geissler            seen[name].add(data["pattern"])
1787e0e3c0cSAndrew Geissler
1797e0e3c0cSAndrew Geissler    for name, patterns in seen.items():
1807e0e3c0cSAndrew Geissler        if len(patterns) > 1:
1817e0e3c0cSAndrew Geissler            passed = False
1827e0e3c0cSAndrew Geissler            logger.error("Collection %s found multiple times: %s" % (name, ", ".join(patterns)))
1837e0e3c0cSAndrew Geissler    return passed
1847e0e3c0cSAndrew Geissler
185213cb269SPatrick Williamsdef get_layer_dependencies(layer, layers, logger):
186eb8dc403SDave Cobbley    def recurse_dependencies(depends, layer, layers, logger, ret = []):
187eb8dc403SDave Cobbley        logger.debug('Processing dependencies %s for layer %s.' % \
188eb8dc403SDave Cobbley                    (depends, layer['name']))
189eb8dc403SDave Cobbley
190eb8dc403SDave Cobbley        for depend in depends.split():
191eb8dc403SDave Cobbley            # core (oe-core) is suppose to be provided
192eb8dc403SDave Cobbley            if depend == 'core':
193eb8dc403SDave Cobbley                continue
194eb8dc403SDave Cobbley
195213cb269SPatrick Williams            layer_depend = _find_layer(depend, layers)
196eb8dc403SDave Cobbley            if not layer_depend:
197eb8dc403SDave Cobbley                logger.error('Layer %s depends on %s and isn\'t found.' % \
198eb8dc403SDave Cobbley                        (layer['name'], depend))
199eb8dc403SDave Cobbley                ret = None
200eb8dc403SDave Cobbley                continue
201eb8dc403SDave Cobbley
202eb8dc403SDave Cobbley            # We keep processing, even if ret is None, this allows us to report
203eb8dc403SDave Cobbley            # multiple errors at once
204eb8dc403SDave Cobbley            if ret is not None and layer_depend not in ret:
205eb8dc403SDave Cobbley                ret.append(layer_depend)
206004d4995SBrad Bishop            else:
207004d4995SBrad Bishop                # we might have processed this dependency already, in which case
208004d4995SBrad Bishop                # we should not do it again (avoid recursive loop)
209004d4995SBrad Bishop                continue
210eb8dc403SDave Cobbley
211eb8dc403SDave Cobbley            # Recursively process...
212eb8dc403SDave Cobbley            if 'collections' not in layer_depend:
213eb8dc403SDave Cobbley                continue
214eb8dc403SDave Cobbley
215eb8dc403SDave Cobbley            for collection in layer_depend['collections']:
216eb8dc403SDave Cobbley                collect_deps = layer_depend['collections'][collection]['depends']
217eb8dc403SDave Cobbley                if not collect_deps:
218eb8dc403SDave Cobbley                    continue
219eb8dc403SDave Cobbley                ret = recurse_dependencies(collect_deps, layer_depend, layers, logger, ret)
220eb8dc403SDave Cobbley
221eb8dc403SDave Cobbley        return ret
222eb8dc403SDave Cobbley
223eb8dc403SDave Cobbley    layer_depends = []
224eb8dc403SDave Cobbley    for collection in layer['collections']:
225eb8dc403SDave Cobbley        depends = layer['collections'][collection]['depends']
226eb8dc403SDave Cobbley        if not depends:
227eb8dc403SDave Cobbley            continue
228eb8dc403SDave Cobbley
229eb8dc403SDave Cobbley        layer_depends = recurse_dependencies(depends, layer, layers, logger, layer_depends)
230eb8dc403SDave Cobbley
231eb8dc403SDave Cobbley    # Note: [] (empty) is allowed, None is not!
232213cb269SPatrick Williams    return layer_depends
233213cb269SPatrick Williams
234213cb269SPatrick Williamsdef add_layer_dependencies(bblayersconf, layer, layers, logger):
235213cb269SPatrick Williams
236213cb269SPatrick Williams    layer_depends = get_layer_dependencies(layer, layers, logger)
237eb8dc403SDave Cobbley    if layer_depends is None:
238eb8dc403SDave Cobbley        return False
239eb8dc403SDave Cobbley    else:
24099467dabSAndrew Geissler        add_layers(bblayersconf, layer_depends, logger)
24199467dabSAndrew Geissler
24299467dabSAndrew Geissler    return True
24399467dabSAndrew Geissler
24499467dabSAndrew Geisslerdef add_layers(bblayersconf, layers, logger):
245eb8dc403SDave Cobbley    # Don't add a layer that is already present.
246eb8dc403SDave Cobbley    added = set()
247eb8dc403SDave Cobbley    output = check_command('Getting existing layers failed.', 'bitbake-layers show-layers').decode('utf-8')
248eb8dc403SDave Cobbley    for layer, path, pri in re.findall(r'^(\S+) +([^\n]*?) +(\d+)$', output, re.MULTILINE):
249eb8dc403SDave Cobbley        added.add(path)
250eb8dc403SDave Cobbley
25199467dabSAndrew Geissler    with open(bblayersconf, 'a+') as f:
25299467dabSAndrew Geissler        for layer in layers:
25399467dabSAndrew Geissler            logger.info('Adding layer %s' % layer['name'])
25499467dabSAndrew Geissler            name = layer['name']
25599467dabSAndrew Geissler            path = layer['path']
256eb8dc403SDave Cobbley            if path in added:
25799467dabSAndrew Geissler                logger.info('%s is already in %s' % (name, bblayersconf))
258eb8dc403SDave Cobbley            else:
259eb8dc403SDave Cobbley                added.add(path)
260eb8dc403SDave Cobbley                f.write("\nBBLAYERS += \"%s\"\n" % path)
261eb8dc403SDave Cobbley    return True
262eb8dc403SDave Cobbley
263635e0e46SAndrew Geisslerdef check_bblayers(bblayersconf, layer_path, logger):
264635e0e46SAndrew Geissler    '''
265635e0e46SAndrew Geissler    If layer_path found in BBLAYERS return True
266635e0e46SAndrew Geissler    '''
267635e0e46SAndrew Geissler    import bb.parse
268635e0e46SAndrew Geissler    import bb.data
269635e0e46SAndrew Geissler
270635e0e46SAndrew Geissler    ldata = bb.parse.handle(bblayersconf, bb.data.init(), include=True)
271635e0e46SAndrew Geissler    for bblayer in (ldata.getVar('BBLAYERS') or '').split():
272635e0e46SAndrew Geissler        if os.path.normpath(bblayer) == os.path.normpath(layer_path):
273635e0e46SAndrew Geissler            return True
274635e0e46SAndrew Geissler
275635e0e46SAndrew Geissler    return False
276635e0e46SAndrew Geissler
27799467dabSAndrew Geisslerdef check_command(error_msg, cmd, cwd=None):
278eb8dc403SDave Cobbley    '''
279eb8dc403SDave Cobbley    Run a command under a shell, capture stdout and stderr in a single stream,
280eb8dc403SDave Cobbley    throw an error when command returns non-zero exit code. Returns the output.
281eb8dc403SDave Cobbley    '''
282eb8dc403SDave Cobbley
28399467dabSAndrew Geissler    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd)
284eb8dc403SDave Cobbley    output, _ = p.communicate()
285eb8dc403SDave Cobbley    if p.returncode:
286eb8dc403SDave Cobbley        msg = "%s\nCommand: %s\nOutput:\n%s" % (error_msg, cmd, output.decode('utf-8'))
287eb8dc403SDave Cobbley        raise RuntimeError(msg)
288eb8dc403SDave Cobbley    return output
289eb8dc403SDave Cobbley
2907e0e3c0cSAndrew Geisslerdef get_signatures(builddir, failsafe=False, machine=None, extravars=None):
291eb8dc403SDave Cobbley    import re
292eb8dc403SDave Cobbley
293eb8dc403SDave Cobbley    # some recipes needs to be excluded like meta-world-pkgdata
294eb8dc403SDave Cobbley    # because a layer can add recipes to a world build so signature
295eb8dc403SDave Cobbley    # will be change
296eb8dc403SDave Cobbley    exclude_recipes = ('meta-world-pkgdata',)
297eb8dc403SDave Cobbley
298eb8dc403SDave Cobbley    sigs = {}
299eb8dc403SDave Cobbley    tune2tasks = {}
300eb8dc403SDave Cobbley
3017e0e3c0cSAndrew Geissler    cmd = 'BB_ENV_PASSTHROUGH_ADDITIONS="$BB_ENV_PASSTHROUGH_ADDITIONS BB_SIGNATURE_HANDLER" BB_SIGNATURE_HANDLER="OEBasicHash" '
3027e0e3c0cSAndrew Geissler    if extravars:
3037e0e3c0cSAndrew Geissler        cmd += extravars
3047e0e3c0cSAndrew Geissler        cmd += ' '
305eb8dc403SDave Cobbley    if machine:
306eb8dc403SDave Cobbley        cmd += 'MACHINE=%s ' % machine
307eb8dc403SDave Cobbley    cmd += 'bitbake '
308eb8dc403SDave Cobbley    if failsafe:
309eb8dc403SDave Cobbley        cmd += '-k '
310ac13d5f3SPatrick Williams    cmd += '-S lockedsigs world'
311eb8dc403SDave Cobbley    sigs_file = os.path.join(builddir, 'locked-sigs.inc')
312eb8dc403SDave Cobbley    if os.path.exists(sigs_file):
313eb8dc403SDave Cobbley        os.unlink(sigs_file)
314eb8dc403SDave Cobbley    try:
315eb8dc403SDave Cobbley        check_command('Generating signatures failed. This might be due to some parse error and/or general layer incompatibilities.',
31699467dabSAndrew Geissler                      cmd, builddir)
317eb8dc403SDave Cobbley    except RuntimeError as ex:
318eb8dc403SDave Cobbley        if failsafe and os.path.exists(sigs_file):
319eb8dc403SDave Cobbley            # Ignore the error here. Most likely some recipes active
320eb8dc403SDave Cobbley            # in a world build lack some dependencies. There is a
321eb8dc403SDave Cobbley            # separate test_machine_world_build which exposes the
322eb8dc403SDave Cobbley            # failure.
323eb8dc403SDave Cobbley            pass
324eb8dc403SDave Cobbley        else:
325eb8dc403SDave Cobbley            raise
326eb8dc403SDave Cobbley
32773bd93f1SPatrick Williams    sig_regex = re.compile(r"^(?P<task>.*:.*):(?P<hash>.*) .$")
32873bd93f1SPatrick Williams    tune_regex = re.compile(r"(^|\s)SIGGEN_LOCKEDSIGS_t-(?P<tune>\S*)\s*=\s*")
329eb8dc403SDave Cobbley    current_tune = None
330eb8dc403SDave Cobbley    with open(sigs_file, 'r') as f:
331eb8dc403SDave Cobbley        for line in f.readlines():
332eb8dc403SDave Cobbley            line = line.strip()
333eb8dc403SDave Cobbley            t = tune_regex.search(line)
334eb8dc403SDave Cobbley            if t:
335eb8dc403SDave Cobbley                current_tune = t.group('tune')
336eb8dc403SDave Cobbley            s = sig_regex.match(line)
337eb8dc403SDave Cobbley            if s:
338eb8dc403SDave Cobbley                exclude = False
339eb8dc403SDave Cobbley                for er in exclude_recipes:
340eb8dc403SDave Cobbley                    (recipe, task) = s.group('task').split(':')
341eb8dc403SDave Cobbley                    if er == recipe:
342eb8dc403SDave Cobbley                        exclude = True
343eb8dc403SDave Cobbley                        break
344eb8dc403SDave Cobbley                if exclude:
345eb8dc403SDave Cobbley                    continue
346eb8dc403SDave Cobbley
347eb8dc403SDave Cobbley                sigs[s.group('task')] = s.group('hash')
348eb8dc403SDave Cobbley                tune2tasks.setdefault(current_tune, []).append(s.group('task'))
349eb8dc403SDave Cobbley
350eb8dc403SDave Cobbley    if not sigs:
351eb8dc403SDave Cobbley        raise RuntimeError('Can\'t load signatures from %s' % sigs_file)
352eb8dc403SDave Cobbley
353eb8dc403SDave Cobbley    return (sigs, tune2tasks)
354eb8dc403SDave Cobbley
355eb8dc403SDave Cobbleydef get_depgraph(targets=['world'], failsafe=False):
356eb8dc403SDave Cobbley    '''
357eb8dc403SDave Cobbley    Returns the dependency graph for the given target(s).
358eb8dc403SDave Cobbley    The dependency graph is taken directly from DepTreeEvent.
359eb8dc403SDave Cobbley    '''
360eb8dc403SDave Cobbley    depgraph = None
361eb8dc403SDave Cobbley    with bb.tinfoil.Tinfoil() as tinfoil:
362eb8dc403SDave Cobbley        tinfoil.prepare(config_only=False)
363eb8dc403SDave Cobbley        tinfoil.set_event_mask(['bb.event.NoProvider', 'bb.event.DepTreeGenerated', 'bb.command.CommandCompleted'])
364eb8dc403SDave Cobbley        if not tinfoil.run_command('generateDepTreeEvent', targets, 'do_build'):
365eb8dc403SDave Cobbley            raise RuntimeError('starting generateDepTreeEvent failed')
366eb8dc403SDave Cobbley        while True:
367eb8dc403SDave Cobbley            event = tinfoil.wait_event(timeout=1000)
368eb8dc403SDave Cobbley            if event:
369eb8dc403SDave Cobbley                if isinstance(event, bb.command.CommandFailed):
370eb8dc403SDave Cobbley                    raise RuntimeError('Generating dependency information failed: %s' % event.error)
371eb8dc403SDave Cobbley                elif isinstance(event, bb.command.CommandCompleted):
372eb8dc403SDave Cobbley                    break
373eb8dc403SDave Cobbley                elif isinstance(event, bb.event.NoProvider):
374eb8dc403SDave Cobbley                    if failsafe:
375eb8dc403SDave Cobbley                        # The event is informational, we will get information about the
376eb8dc403SDave Cobbley                        # remaining dependencies eventually and thus can ignore this
377eb8dc403SDave Cobbley                        # here like we do in get_signatures(), if desired.
378eb8dc403SDave Cobbley                        continue
379eb8dc403SDave Cobbley                    if event._reasons:
380eb8dc403SDave Cobbley                        raise RuntimeError('Nothing provides %s: %s' % (event._item, event._reasons))
381eb8dc403SDave Cobbley                    else:
382eb8dc403SDave Cobbley                        raise RuntimeError('Nothing provides %s.' % (event._item))
383eb8dc403SDave Cobbley                elif isinstance(event, bb.event.DepTreeGenerated):
384eb8dc403SDave Cobbley                    depgraph = event._depgraph
385eb8dc403SDave Cobbley
386eb8dc403SDave Cobbley    if depgraph is None:
387eb8dc403SDave Cobbley        raise RuntimeError('Could not retrieve the depgraph.')
388eb8dc403SDave Cobbley    return depgraph
389eb8dc403SDave Cobbley
390eb8dc403SDave Cobbleydef compare_signatures(old_sigs, curr_sigs):
391eb8dc403SDave Cobbley    '''
392eb8dc403SDave Cobbley    Compares the result of two get_signatures() calls. Returns None if no
393eb8dc403SDave Cobbley    problems found, otherwise a string that can be used as additional
394eb8dc403SDave Cobbley    explanation in self.fail().
395eb8dc403SDave Cobbley    '''
396eb8dc403SDave Cobbley    # task -> (old signature, new signature)
397eb8dc403SDave Cobbley    sig_diff = {}
398eb8dc403SDave Cobbley    for task in old_sigs:
399eb8dc403SDave Cobbley        if task in curr_sigs and \
400eb8dc403SDave Cobbley           old_sigs[task] != curr_sigs[task]:
401eb8dc403SDave Cobbley            sig_diff[task] = (old_sigs[task], curr_sigs[task])
402eb8dc403SDave Cobbley
403eb8dc403SDave Cobbley    if not sig_diff:
404eb8dc403SDave Cobbley        return None
405eb8dc403SDave Cobbley
406eb8dc403SDave Cobbley    # Beware, depgraph uses task=<pn>.<taskname> whereas get_signatures()
407eb8dc403SDave Cobbley    # uses <pn>:<taskname>. Need to convert sometimes. The output follows
408eb8dc403SDave Cobbley    # the convention from get_signatures() because that seems closer to
409eb8dc403SDave Cobbley    # normal bitbake output.
410eb8dc403SDave Cobbley    def sig2graph(task):
411eb8dc403SDave Cobbley        pn, taskname = task.rsplit(':', 1)
412eb8dc403SDave Cobbley        return pn + '.' + taskname
413eb8dc403SDave Cobbley    def graph2sig(task):
414eb8dc403SDave Cobbley        pn, taskname = task.rsplit('.', 1)
415eb8dc403SDave Cobbley        return pn + ':' + taskname
416eb8dc403SDave Cobbley    depgraph = get_depgraph(failsafe=True)
417eb8dc403SDave Cobbley    depends = depgraph['tdepends']
418eb8dc403SDave Cobbley
419eb8dc403SDave Cobbley    # If a task A has a changed signature, but none of its
420eb8dc403SDave Cobbley    # dependencies, then we need to report it because it is
421eb8dc403SDave Cobbley    # the one which introduces a change. Any task depending on
422eb8dc403SDave Cobbley    # A (directly or indirectly) will also have a changed
423eb8dc403SDave Cobbley    # signature, but we don't need to report it. It might have
424eb8dc403SDave Cobbley    # its own changes, which will become apparent once the
425eb8dc403SDave Cobbley    # issues that we do report are fixed and the test gets run
426eb8dc403SDave Cobbley    # again.
427eb8dc403SDave Cobbley    sig_diff_filtered = []
428eb8dc403SDave Cobbley    for task, (old_sig, new_sig) in sig_diff.items():
429eb8dc403SDave Cobbley        deps_tainted = False
430eb8dc403SDave Cobbley        for dep in depends.get(sig2graph(task), ()):
431eb8dc403SDave Cobbley            if graph2sig(dep) in sig_diff:
432eb8dc403SDave Cobbley                deps_tainted = True
433eb8dc403SDave Cobbley                break
434eb8dc403SDave Cobbley        if not deps_tainted:
435eb8dc403SDave Cobbley            sig_diff_filtered.append((task, old_sig, new_sig))
436eb8dc403SDave Cobbley
437eb8dc403SDave Cobbley    msg = []
438eb8dc403SDave Cobbley    msg.append('%d signatures changed, initial differences (first hash before, second after):' %
439eb8dc403SDave Cobbley               len(sig_diff))
440eb8dc403SDave Cobbley    for diff in sorted(sig_diff_filtered):
441eb8dc403SDave Cobbley        recipe, taskname = diff[0].rsplit(':', 1)
442eb8dc403SDave Cobbley        cmd = 'bitbake-diffsigs --task %s %s --signature %s %s' % \
443eb8dc403SDave Cobbley              (recipe, taskname, diff[1], diff[2])
444eb8dc403SDave Cobbley        msg.append('   %s: %s -> %s' % diff)
445eb8dc403SDave Cobbley        msg.append('      %s' % cmd)
446eb8dc403SDave Cobbley        try:
447eb8dc403SDave Cobbley            output = check_command('Determining signature difference failed.',
448eb8dc403SDave Cobbley                                   cmd).decode('utf-8')
449eb8dc403SDave Cobbley        except RuntimeError as error:
450eb8dc403SDave Cobbley            output = str(error)
451eb8dc403SDave Cobbley        if output:
452eb8dc403SDave Cobbley            msg.extend(['      ' + line for line in output.splitlines()])
453eb8dc403SDave Cobbley            msg.append('')
454eb8dc403SDave Cobbley    return '\n'.join(msg)
455*8460358cSPatrick Williams
456*8460358cSPatrick Williams
457*8460358cSPatrick Williamsdef get_git_toplevel(directory):
458*8460358cSPatrick Williams    """
459*8460358cSPatrick Williams    Try and find the top of the git repository that directory might be in.
460*8460358cSPatrick Williams    Returns the top-level directory, or None.
461*8460358cSPatrick Williams    """
462*8460358cSPatrick Williams    cmd = ["git", "-C", directory, "rev-parse", "--show-toplevel"]
463*8460358cSPatrick Williams    try:
464*8460358cSPatrick Williams        return subprocess.check_output(cmd, text=True).strip()
465*8460358cSPatrick Williams    except:
466*8460358cSPatrick Williams        return None
467