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