1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun# ex:ts=4:sw=4:sts=4:et 3*4882a593Smuzhiyun# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- 4*4882a593Smuzhiyun 5*4882a593Smuzhiyun# Copyright (c) 2013 Wind River Systems, Inc. 6*4882a593Smuzhiyun# 7*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 8*4882a593Smuzhiyun# 9*4882a593Smuzhiyun 10*4882a593Smuzhiyunimport os 11*4882a593Smuzhiyunimport sys 12*4882a593Smuzhiyunimport getopt 13*4882a593Smuzhiyunimport shutil 14*4882a593Smuzhiyunimport re 15*4882a593Smuzhiyunimport warnings 16*4882a593Smuzhiyunimport subprocess 17*4882a593Smuzhiyunimport argparse 18*4882a593Smuzhiyun 19*4882a593Smuzhiyunscripts_path = os.path.abspath(os.path.dirname(os.path.abspath(sys.argv[0]))) 20*4882a593Smuzhiyunlib_path = scripts_path + '/lib' 21*4882a593Smuzhiyunsys.path = sys.path + [lib_path] 22*4882a593Smuzhiyun 23*4882a593Smuzhiyunimport scriptpath 24*4882a593Smuzhiyun 25*4882a593Smuzhiyun# Figure out where is the bitbake/lib/bb since we need bb.siggen and bb.process 26*4882a593Smuzhiyunbitbakepath = scriptpath.add_bitbake_lib_path() 27*4882a593Smuzhiyunif not bitbakepath: 28*4882a593Smuzhiyun sys.stderr.write("Unable to find bitbake by searching parent directory of this script or PATH\n") 29*4882a593Smuzhiyun sys.exit(1) 30*4882a593Smuzhiyunscriptpath.add_oe_lib_path() 31*4882a593Smuzhiyunimport argparse_oe 32*4882a593Smuzhiyun 33*4882a593Smuzhiyunimport bb.siggen 34*4882a593Smuzhiyunimport bb.process 35*4882a593Smuzhiyun 36*4882a593Smuzhiyun# Match the stamp's filename 37*4882a593Smuzhiyun# group(1): PE_PV (may no PE) 38*4882a593Smuzhiyun# group(2): PR 39*4882a593Smuzhiyun# group(3): TASK 40*4882a593Smuzhiyun# group(4): HASH 41*4882a593Smuzhiyunstamp_re = re.compile("(?P<pv>.*)-(?P<pr>r\d+)\.(?P<task>do_\w+)\.(?P<hash>[^\.]*)") 42*4882a593Smuzhiyunsigdata_re = re.compile(".*\.sigdata\..*") 43*4882a593Smuzhiyun 44*4882a593Smuzhiyundef gen_dict(stamps): 45*4882a593Smuzhiyun """ 46*4882a593Smuzhiyun Generate the dict from the stamps dir. 47*4882a593Smuzhiyun The output dict format is: 48*4882a593Smuzhiyun {fake_f: {pn: PN, pv: PV, pr: PR, task: TASK, path: PATH}} 49*4882a593Smuzhiyun Where: 50*4882a593Smuzhiyun fake_f: pv + task + hash 51*4882a593Smuzhiyun path: the path to the stamp file 52*4882a593Smuzhiyun """ 53*4882a593Smuzhiyun # The member of the sub dict (A "path" will be appended below) 54*4882a593Smuzhiyun sub_mem = ("pv", "pr", "task") 55*4882a593Smuzhiyun d = {} 56*4882a593Smuzhiyun for dirpath, _, files in os.walk(stamps): 57*4882a593Smuzhiyun for f in files: 58*4882a593Smuzhiyun # The "bitbake -S" would generate ".sigdata", but no "_setscene". 59*4882a593Smuzhiyun fake_f = re.sub('_setscene.', '.', f) 60*4882a593Smuzhiyun fake_f = re.sub('.sigdata', '', fake_f) 61*4882a593Smuzhiyun subdict = {} 62*4882a593Smuzhiyun tmp = stamp_re.match(fake_f) 63*4882a593Smuzhiyun if tmp: 64*4882a593Smuzhiyun for i in sub_mem: 65*4882a593Smuzhiyun subdict[i] = tmp.group(i) 66*4882a593Smuzhiyun if len(subdict) != 0: 67*4882a593Smuzhiyun pn = os.path.basename(dirpath) 68*4882a593Smuzhiyun subdict['pn'] = pn 69*4882a593Smuzhiyun # The path will be used by os.stat() and bb.siggen 70*4882a593Smuzhiyun subdict['path'] = dirpath + "/" + f 71*4882a593Smuzhiyun fake_f = tmp.group('pv') + tmp.group('task') + tmp.group('hash') 72*4882a593Smuzhiyun d[fake_f] = subdict 73*4882a593Smuzhiyun return d 74*4882a593Smuzhiyun 75*4882a593Smuzhiyun# Re-construct the dict 76*4882a593Smuzhiyundef recon_dict(dict_in): 77*4882a593Smuzhiyun """ 78*4882a593Smuzhiyun The output dict format is: 79*4882a593Smuzhiyun {pn_task: {pv: PV, pr: PR, path: PATH}} 80*4882a593Smuzhiyun """ 81*4882a593Smuzhiyun dict_out = {} 82*4882a593Smuzhiyun for k in dict_in.keys(): 83*4882a593Smuzhiyun subdict = {} 84*4882a593Smuzhiyun # The key 85*4882a593Smuzhiyun pn_task = "%s_%s" % (dict_in.get(k).get('pn'), dict_in.get(k).get('task')) 86*4882a593Smuzhiyun # If more than one stamps are found, use the latest one. 87*4882a593Smuzhiyun if pn_task in dict_out: 88*4882a593Smuzhiyun full_path_pre = dict_out.get(pn_task).get('path') 89*4882a593Smuzhiyun full_path_cur = dict_in.get(k).get('path') 90*4882a593Smuzhiyun if os.stat(full_path_pre).st_mtime > os.stat(full_path_cur).st_mtime: 91*4882a593Smuzhiyun continue 92*4882a593Smuzhiyun subdict['pv'] = dict_in.get(k).get('pv') 93*4882a593Smuzhiyun subdict['pr'] = dict_in.get(k).get('pr') 94*4882a593Smuzhiyun subdict['path'] = dict_in.get(k).get('path') 95*4882a593Smuzhiyun dict_out[pn_task] = subdict 96*4882a593Smuzhiyun 97*4882a593Smuzhiyun return dict_out 98*4882a593Smuzhiyun 99*4882a593Smuzhiyundef split_pntask(s): 100*4882a593Smuzhiyun """ 101*4882a593Smuzhiyun Split the pn_task in to (pn, task) and return it 102*4882a593Smuzhiyun """ 103*4882a593Smuzhiyun tmp = re.match("(.*)_(do_.*)", s) 104*4882a593Smuzhiyun return (tmp.group(1), tmp.group(2)) 105*4882a593Smuzhiyun 106*4882a593Smuzhiyun 107*4882a593Smuzhiyundef print_added(d_new = None, d_old = None): 108*4882a593Smuzhiyun """ 109*4882a593Smuzhiyun Print the newly added tasks 110*4882a593Smuzhiyun """ 111*4882a593Smuzhiyun added = {} 112*4882a593Smuzhiyun for k in list(d_new.keys()): 113*4882a593Smuzhiyun if k not in d_old: 114*4882a593Smuzhiyun # Add the new one to added dict, and remove it from 115*4882a593Smuzhiyun # d_new, so the remaining ones are the changed ones 116*4882a593Smuzhiyun added[k] = d_new.get(k) 117*4882a593Smuzhiyun del(d_new[k]) 118*4882a593Smuzhiyun 119*4882a593Smuzhiyun if not added: 120*4882a593Smuzhiyun return 0 121*4882a593Smuzhiyun 122*4882a593Smuzhiyun # Format the output, the dict format is: 123*4882a593Smuzhiyun # {pn: task1, task2 ...} 124*4882a593Smuzhiyun added_format = {} 125*4882a593Smuzhiyun counter = 0 126*4882a593Smuzhiyun for k in added.keys(): 127*4882a593Smuzhiyun pn, task = split_pntask(k) 128*4882a593Smuzhiyun if pn in added_format: 129*4882a593Smuzhiyun # Append the value 130*4882a593Smuzhiyun added_format[pn] = "%s %s" % (added_format.get(pn), task) 131*4882a593Smuzhiyun else: 132*4882a593Smuzhiyun added_format[pn] = task 133*4882a593Smuzhiyun counter += 1 134*4882a593Smuzhiyun print("=== Newly added tasks: (%s tasks)" % counter) 135*4882a593Smuzhiyun for k in added_format.keys(): 136*4882a593Smuzhiyun print(" %s: %s" % (k, added_format.get(k))) 137*4882a593Smuzhiyun 138*4882a593Smuzhiyun return counter 139*4882a593Smuzhiyun 140*4882a593Smuzhiyundef print_vrchanged(d_new = None, d_old = None, vr = None): 141*4882a593Smuzhiyun """ 142*4882a593Smuzhiyun Print the pv or pr changed tasks. 143*4882a593Smuzhiyun The arg "vr" is "pv" or "pr" 144*4882a593Smuzhiyun """ 145*4882a593Smuzhiyun pvchanged = {} 146*4882a593Smuzhiyun counter = 0 147*4882a593Smuzhiyun for k in list(d_new.keys()): 148*4882a593Smuzhiyun if d_new.get(k).get(vr) != d_old.get(k).get(vr): 149*4882a593Smuzhiyun counter += 1 150*4882a593Smuzhiyun pn, task = split_pntask(k) 151*4882a593Smuzhiyun if pn not in pvchanged: 152*4882a593Smuzhiyun # Format the output, we only print pn (no task) since 153*4882a593Smuzhiyun # all the tasks would be changed when pn or pr changed, 154*4882a593Smuzhiyun # the dict format is: 155*4882a593Smuzhiyun # {pn: pv/pr_old -> pv/pr_new} 156*4882a593Smuzhiyun pvchanged[pn] = "%s -> %s" % (d_old.get(k).get(vr), d_new.get(k).get(vr)) 157*4882a593Smuzhiyun del(d_new[k]) 158*4882a593Smuzhiyun 159*4882a593Smuzhiyun if not pvchanged: 160*4882a593Smuzhiyun return 0 161*4882a593Smuzhiyun 162*4882a593Smuzhiyun print("\n=== %s changed: (%s tasks)" % (vr.upper(), counter)) 163*4882a593Smuzhiyun for k in pvchanged.keys(): 164*4882a593Smuzhiyun print(" %s: %s" % (k, pvchanged.get(k))) 165*4882a593Smuzhiyun 166*4882a593Smuzhiyun return counter 167*4882a593Smuzhiyun 168*4882a593Smuzhiyundef print_depchanged(d_new = None, d_old = None, verbose = False): 169*4882a593Smuzhiyun """ 170*4882a593Smuzhiyun Print the dependency changes 171*4882a593Smuzhiyun """ 172*4882a593Smuzhiyun depchanged = {} 173*4882a593Smuzhiyun counter = 0 174*4882a593Smuzhiyun for k in d_new.keys(): 175*4882a593Smuzhiyun counter += 1 176*4882a593Smuzhiyun pn, task = split_pntask(k) 177*4882a593Smuzhiyun if (verbose): 178*4882a593Smuzhiyun full_path_old = d_old.get(k).get("path") 179*4882a593Smuzhiyun full_path_new = d_new.get(k).get("path") 180*4882a593Smuzhiyun # No counter since it is not ready here 181*4882a593Smuzhiyun if sigdata_re.match(full_path_old) and sigdata_re.match(full_path_new): 182*4882a593Smuzhiyun output = bb.siggen.compare_sigfiles(full_path_old, full_path_new) 183*4882a593Smuzhiyun if output: 184*4882a593Smuzhiyun print("\n=== The verbose changes of %s.%s:" % (pn, task)) 185*4882a593Smuzhiyun print('\n'.join(output)) 186*4882a593Smuzhiyun else: 187*4882a593Smuzhiyun # Format the output, the format is: 188*4882a593Smuzhiyun # {pn: task1, task2, ...} 189*4882a593Smuzhiyun if pn in depchanged: 190*4882a593Smuzhiyun depchanged[pn] = "%s %s" % (depchanged.get(pn), task) 191*4882a593Smuzhiyun else: 192*4882a593Smuzhiyun depchanged[pn] = task 193*4882a593Smuzhiyun 194*4882a593Smuzhiyun if len(depchanged) > 0: 195*4882a593Smuzhiyun print("\n=== Dependencies changed: (%s tasks)" % counter) 196*4882a593Smuzhiyun for k in depchanged.keys(): 197*4882a593Smuzhiyun print(" %s: %s" % (k, depchanged[k])) 198*4882a593Smuzhiyun 199*4882a593Smuzhiyun return counter 200*4882a593Smuzhiyun 201*4882a593Smuzhiyun 202*4882a593Smuzhiyundef main(): 203*4882a593Smuzhiyun """ 204*4882a593Smuzhiyun Print what will be done between the current and last builds: 205*4882a593Smuzhiyun 1) Run "STAMPS_DIR=<path> bitbake -S recipe" to re-generate the stamps 206*4882a593Smuzhiyun 2) Figure out what are newly added and changed, can't figure out 207*4882a593Smuzhiyun what are removed since we can't know the previous stamps 208*4882a593Smuzhiyun clearly, for example, if there are several builds, we can't know 209*4882a593Smuzhiyun which stamps the last build has used exactly. 210*4882a593Smuzhiyun 3) Use bb.siggen.compare_sigfiles to diff the old and new stamps 211*4882a593Smuzhiyun """ 212*4882a593Smuzhiyun 213*4882a593Smuzhiyun parser = argparse_oe.ArgumentParser(usage = """%(prog)s [options] [package ...] 214*4882a593Smuzhiyunprint what will be done between the current and last builds, for example: 215*4882a593Smuzhiyun 216*4882a593Smuzhiyun $ bitbake core-image-sato 217*4882a593Smuzhiyun # Edit the recipes 218*4882a593Smuzhiyun $ bitbake-whatchanged core-image-sato 219*4882a593Smuzhiyun 220*4882a593SmuzhiyunThe changes will be printed. 221*4882a593Smuzhiyun 222*4882a593SmuzhiyunNote: 223*4882a593Smuzhiyun The amount of tasks is not accurate when the task is "do_build" since 224*4882a593Smuzhiyun it usually depends on other tasks. 225*4882a593Smuzhiyun The "nostamp" task is not included. 226*4882a593Smuzhiyun""" 227*4882a593Smuzhiyun) 228*4882a593Smuzhiyun parser.add_argument("recipe", help="recipe to check") 229*4882a593Smuzhiyun parser.add_argument("-v", "--verbose", help = "print the verbose changes", action = "store_true") 230*4882a593Smuzhiyun args = parser.parse_args() 231*4882a593Smuzhiyun 232*4882a593Smuzhiyun # Get the STAMPS_DIR 233*4882a593Smuzhiyun print("Figuring out the STAMPS_DIR ...") 234*4882a593Smuzhiyun cmdline = "bitbake -e | sed -ne 's/^STAMPS_DIR=\"\(.*\)\"/\\1/p'" 235*4882a593Smuzhiyun try: 236*4882a593Smuzhiyun stampsdir, err = bb.process.run(cmdline) 237*4882a593Smuzhiyun except: 238*4882a593Smuzhiyun raise 239*4882a593Smuzhiyun if not stampsdir: 240*4882a593Smuzhiyun print("ERROR: No STAMPS_DIR found for '%s'" % args.recipe, file=sys.stderr) 241*4882a593Smuzhiyun return 2 242*4882a593Smuzhiyun stampsdir = stampsdir.rstrip("\n") 243*4882a593Smuzhiyun if not os.path.isdir(stampsdir): 244*4882a593Smuzhiyun print("ERROR: stamps directory \"%s\" not found!" % stampsdir, file=sys.stderr) 245*4882a593Smuzhiyun return 2 246*4882a593Smuzhiyun 247*4882a593Smuzhiyun # The new stamps dir 248*4882a593Smuzhiyun new_stampsdir = stampsdir + ".bbs" 249*4882a593Smuzhiyun if os.path.exists(new_stampsdir): 250*4882a593Smuzhiyun print("ERROR: %s already exists!" % new_stampsdir, file=sys.stderr) 251*4882a593Smuzhiyun return 2 252*4882a593Smuzhiyun 253*4882a593Smuzhiyun try: 254*4882a593Smuzhiyun # Generate the new stamps dir 255*4882a593Smuzhiyun print("Generating the new stamps ... (need several minutes)") 256*4882a593Smuzhiyun cmdline = "STAMPS_DIR=%s bitbake -S none %s" % (new_stampsdir, args.recipe) 257*4882a593Smuzhiyun # FIXME 258*4882a593Smuzhiyun # The "bitbake -S" may fail, not fatal error, the stamps will still 259*4882a593Smuzhiyun # be generated, this might be a bug of "bitbake -S". 260*4882a593Smuzhiyun try: 261*4882a593Smuzhiyun bb.process.run(cmdline) 262*4882a593Smuzhiyun except Exception as exc: 263*4882a593Smuzhiyun print(exc) 264*4882a593Smuzhiyun 265*4882a593Smuzhiyun # The dict for the new and old stamps. 266*4882a593Smuzhiyun old_dict = gen_dict(stampsdir) 267*4882a593Smuzhiyun new_dict = gen_dict(new_stampsdir) 268*4882a593Smuzhiyun 269*4882a593Smuzhiyun # Remove the same one from both stamps. 270*4882a593Smuzhiyun cnt_unchanged = 0 271*4882a593Smuzhiyun for k in list(new_dict.keys()): 272*4882a593Smuzhiyun if k in old_dict: 273*4882a593Smuzhiyun cnt_unchanged += 1 274*4882a593Smuzhiyun del(new_dict[k]) 275*4882a593Smuzhiyun del(old_dict[k]) 276*4882a593Smuzhiyun 277*4882a593Smuzhiyun # Re-construct the dict to easily find out what is added or changed. 278*4882a593Smuzhiyun # The dict format is: 279*4882a593Smuzhiyun # {pn_task: {pv: PV, pr: PR, path: PATH}} 280*4882a593Smuzhiyun new_recon = recon_dict(new_dict) 281*4882a593Smuzhiyun old_recon = recon_dict(old_dict) 282*4882a593Smuzhiyun 283*4882a593Smuzhiyun del new_dict 284*4882a593Smuzhiyun del old_dict 285*4882a593Smuzhiyun 286*4882a593Smuzhiyun # Figure out what are changed, the new_recon would be changed 287*4882a593Smuzhiyun # by the print_xxx function. 288*4882a593Smuzhiyun # Newly added 289*4882a593Smuzhiyun cnt_added = print_added(new_recon, old_recon) 290*4882a593Smuzhiyun 291*4882a593Smuzhiyun # PV (including PE) and PR changed 292*4882a593Smuzhiyun # Let the bb.siggen handle them if verbose 293*4882a593Smuzhiyun cnt_rv = {} 294*4882a593Smuzhiyun if not args.verbose: 295*4882a593Smuzhiyun for i in ('pv', 'pr'): 296*4882a593Smuzhiyun cnt_rv[i] = print_vrchanged(new_recon, old_recon, i) 297*4882a593Smuzhiyun 298*4882a593Smuzhiyun # Dependencies changed (use bitbake-diffsigs) 299*4882a593Smuzhiyun cnt_dep = print_depchanged(new_recon, old_recon, args.verbose) 300*4882a593Smuzhiyun 301*4882a593Smuzhiyun total_changed = cnt_added + (cnt_rv.get('pv') or 0) + (cnt_rv.get('pr') or 0) + cnt_dep 302*4882a593Smuzhiyun 303*4882a593Smuzhiyun print("\n=== Summary: (%s changed, %s unchanged)" % (total_changed, cnt_unchanged)) 304*4882a593Smuzhiyun if args.verbose: 305*4882a593Smuzhiyun print("Newly added: %s\nDependencies changed: %s\n" % \ 306*4882a593Smuzhiyun (cnt_added, cnt_dep)) 307*4882a593Smuzhiyun else: 308*4882a593Smuzhiyun print("Newly added: %s\nPV changed: %s\nPR changed: %s\nDependencies changed: %s\n" % \ 309*4882a593Smuzhiyun (cnt_added, cnt_rv.get('pv') or 0, cnt_rv.get('pr') or 0, cnt_dep)) 310*4882a593Smuzhiyun except: 311*4882a593Smuzhiyun print("ERROR occurred!") 312*4882a593Smuzhiyun raise 313*4882a593Smuzhiyun finally: 314*4882a593Smuzhiyun # Remove the newly generated stamps dir 315*4882a593Smuzhiyun if os.path.exists(new_stampsdir): 316*4882a593Smuzhiyun print("Removing the newly generated stamps dir ...") 317*4882a593Smuzhiyun shutil.rmtree(new_stampsdir) 318*4882a593Smuzhiyun 319*4882a593Smuzhiyunif __name__ == "__main__": 320*4882a593Smuzhiyun sys.exit(main()) 321