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