| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2013 Wind River Systems, Inc. |
| # |
| # SPDX-License-Identifier: GPL-2.0-only |
| # |
| |
| import os |
| import sys |
| import getopt |
| import shutil |
| import re |
| import warnings |
| import subprocess |
| import argparse |
| |
| scripts_path = os.path.abspath(os.path.dirname(os.path.abspath(sys.argv[0]))) |
| lib_path = scripts_path + '/lib' |
| sys.path = sys.path + [lib_path] |
| |
| import scriptpath |
| |
| # Figure out where is the bitbake/lib/bb since we need bb.siggen and bb.process |
| bitbakepath = scriptpath.add_bitbake_lib_path() |
| if not bitbakepath: |
| sys.stderr.write("Unable to find bitbake by searching parent directory of this script or PATH\n") |
| sys.exit(1) |
| scriptpath.add_oe_lib_path() |
| import argparse_oe |
| |
| import bb.siggen |
| import bb.process |
| |
| # Match the stamp's filename |
| # group(1): PE_PV (may no PE) |
| # group(2): PR |
| # group(3): TASK |
| # group(4): HASH |
| stamp_re = re.compile("(?P<pv>.*)-(?P<pr>r\d+)\.(?P<task>do_\w+)\.(?P<hash>[^\.]*)") |
| sigdata_re = re.compile(".*\.sigdata\..*") |
| |
| def gen_dict(stamps): |
| """ |
| Generate the dict from the stamps dir. |
| The output dict format is: |
| {fake_f: {pn: PN, pv: PV, pr: PR, task: TASK, path: PATH}} |
| Where: |
| fake_f: pv + task + hash |
| path: the path to the stamp file |
| """ |
| # The member of the sub dict (A "path" will be appended below) |
| sub_mem = ("pv", "pr", "task") |
| d = {} |
| for dirpath, _, files in os.walk(stamps): |
| for f in files: |
| # The "bitbake -S" would generate ".sigdata", but no "_setscene". |
| fake_f = re.sub('_setscene.', '.', f) |
| fake_f = re.sub('.sigdata', '', fake_f) |
| subdict = {} |
| tmp = stamp_re.match(fake_f) |
| if tmp: |
| for i in sub_mem: |
| subdict[i] = tmp.group(i) |
| if len(subdict) != 0: |
| pn = os.path.basename(dirpath) |
| subdict['pn'] = pn |
| # The path will be used by os.stat() and bb.siggen |
| subdict['path'] = dirpath + "/" + f |
| fake_f = tmp.group('pv') + tmp.group('task') + tmp.group('hash') |
| d[fake_f] = subdict |
| return d |
| |
| # Re-construct the dict |
| def recon_dict(dict_in): |
| """ |
| The output dict format is: |
| {pn_task: {pv: PV, pr: PR, path: PATH}} |
| """ |
| dict_out = {} |
| for k in dict_in.keys(): |
| subdict = {} |
| # The key |
| pn_task = "%s_%s" % (dict_in.get(k).get('pn'), dict_in.get(k).get('task')) |
| # If more than one stamps are found, use the latest one. |
| if pn_task in dict_out: |
| full_path_pre = dict_out.get(pn_task).get('path') |
| full_path_cur = dict_in.get(k).get('path') |
| if os.stat(full_path_pre).st_mtime > os.stat(full_path_cur).st_mtime: |
| continue |
| subdict['pv'] = dict_in.get(k).get('pv') |
| subdict['pr'] = dict_in.get(k).get('pr') |
| subdict['path'] = dict_in.get(k).get('path') |
| dict_out[pn_task] = subdict |
| |
| return dict_out |
| |
| def split_pntask(s): |
| """ |
| Split the pn_task in to (pn, task) and return it |
| """ |
| tmp = re.match("(.*)_(do_.*)", s) |
| return (tmp.group(1), tmp.group(2)) |
| |
| |
| def print_added(d_new = None, d_old = None): |
| """ |
| Print the newly added tasks |
| """ |
| added = {} |
| for k in list(d_new.keys()): |
| if k not in d_old: |
| # Add the new one to added dict, and remove it from |
| # d_new, so the remaining ones are the changed ones |
| added[k] = d_new.get(k) |
| del(d_new[k]) |
| |
| if not added: |
| return 0 |
| |
| # Format the output, the dict format is: |
| # {pn: task1, task2 ...} |
| added_format = {} |
| counter = 0 |
| for k in added.keys(): |
| pn, task = split_pntask(k) |
| if pn in added_format: |
| # Append the value |
| added_format[pn] = "%s %s" % (added_format.get(pn), task) |
| else: |
| added_format[pn] = task |
| counter += 1 |
| print("=== Newly added tasks: (%s tasks)" % counter) |
| for k in added_format.keys(): |
| print(" %s: %s" % (k, added_format.get(k))) |
| |
| return counter |
| |
| def print_vrchanged(d_new = None, d_old = None, vr = None): |
| """ |
| Print the pv or pr changed tasks. |
| The arg "vr" is "pv" or "pr" |
| """ |
| pvchanged = {} |
| counter = 0 |
| for k in list(d_new.keys()): |
| if d_new.get(k).get(vr) != d_old.get(k).get(vr): |
| counter += 1 |
| pn, task = split_pntask(k) |
| if pn not in pvchanged: |
| # Format the output, we only print pn (no task) since |
| # all the tasks would be changed when pn or pr changed, |
| # the dict format is: |
| # {pn: pv/pr_old -> pv/pr_new} |
| pvchanged[pn] = "%s -> %s" % (d_old.get(k).get(vr), d_new.get(k).get(vr)) |
| del(d_new[k]) |
| |
| if not pvchanged: |
| return 0 |
| |
| print("\n=== %s changed: (%s tasks)" % (vr.upper(), counter)) |
| for k in pvchanged.keys(): |
| print(" %s: %s" % (k, pvchanged.get(k))) |
| |
| return counter |
| |
| def print_depchanged(d_new = None, d_old = None, verbose = False): |
| """ |
| Print the dependency changes |
| """ |
| depchanged = {} |
| counter = 0 |
| for k in d_new.keys(): |
| counter += 1 |
| pn, task = split_pntask(k) |
| if (verbose): |
| full_path_old = d_old.get(k).get("path") |
| full_path_new = d_new.get(k).get("path") |
| # No counter since it is not ready here |
| if sigdata_re.match(full_path_old) and sigdata_re.match(full_path_new): |
| output = bb.siggen.compare_sigfiles(full_path_old, full_path_new) |
| if output: |
| print("\n=== The verbose changes of %s.%s:" % (pn, task)) |
| print('\n'.join(output)) |
| else: |
| # Format the output, the format is: |
| # {pn: task1, task2, ...} |
| if pn in depchanged: |
| depchanged[pn] = "%s %s" % (depchanged.get(pn), task) |
| else: |
| depchanged[pn] = task |
| |
| if len(depchanged) > 0: |
| print("\n=== Dependencies changed: (%s tasks)" % counter) |
| for k in depchanged.keys(): |
| print(" %s: %s" % (k, depchanged[k])) |
| |
| return counter |
| |
| |
| def main(): |
| """ |
| Print what will be done between the current and last builds: |
| 1) Run "STAMPS_DIR=<path> bitbake -S recipe" to re-generate the stamps |
| 2) Figure out what are newly added and changed, can't figure out |
| what are removed since we can't know the previous stamps |
| clearly, for example, if there are several builds, we can't know |
| which stamps the last build has used exactly. |
| 3) Use bb.siggen.compare_sigfiles to diff the old and new stamps |
| """ |
| |
| parser = argparse_oe.ArgumentParser(usage = """%(prog)s [options] [package ...] |
| print what will be done between the current and last builds, for example: |
| |
| $ bitbake core-image-sato |
| # Edit the recipes |
| $ bitbake-whatchanged core-image-sato |
| |
| The changes will be printed. |
| |
| Note: |
| The amount of tasks is not accurate when the task is "do_build" since |
| it usually depends on other tasks. |
| The "nostamp" task is not included. |
| """ |
| ) |
| parser.add_argument("recipe", help="recipe to check") |
| parser.add_argument("-v", "--verbose", help = "print the verbose changes", action = "store_true") |
| args = parser.parse_args() |
| |
| # Get the STAMPS_DIR |
| print("Figuring out the STAMPS_DIR ...") |
| cmdline = "bitbake -e | sed -ne 's/^STAMPS_DIR=\"\(.*\)\"/\\1/p'" |
| try: |
| stampsdir, err = bb.process.run(cmdline) |
| except: |
| raise |
| if not stampsdir: |
| print("ERROR: No STAMPS_DIR found for '%s'" % args.recipe, file=sys.stderr) |
| return 2 |
| stampsdir = stampsdir.rstrip("\n") |
| if not os.path.isdir(stampsdir): |
| print("ERROR: stamps directory \"%s\" not found!" % stampsdir, file=sys.stderr) |
| return 2 |
| |
| # The new stamps dir |
| new_stampsdir = stampsdir + ".bbs" |
| if os.path.exists(new_stampsdir): |
| print("ERROR: %s already exists!" % new_stampsdir, file=sys.stderr) |
| return 2 |
| |
| try: |
| # Generate the new stamps dir |
| print("Generating the new stamps ... (need several minutes)") |
| cmdline = "STAMPS_DIR=%s bitbake -S none %s" % (new_stampsdir, args.recipe) |
| # FIXME |
| # The "bitbake -S" may fail, not fatal error, the stamps will still |
| # be generated, this might be a bug of "bitbake -S". |
| try: |
| bb.process.run(cmdline) |
| except Exception as exc: |
| print(exc) |
| |
| # The dict for the new and old stamps. |
| old_dict = gen_dict(stampsdir) |
| new_dict = gen_dict(new_stampsdir) |
| |
| # Remove the same one from both stamps. |
| cnt_unchanged = 0 |
| for k in list(new_dict.keys()): |
| if k in old_dict: |
| cnt_unchanged += 1 |
| del(new_dict[k]) |
| del(old_dict[k]) |
| |
| # Re-construct the dict to easily find out what is added or changed. |
| # The dict format is: |
| # {pn_task: {pv: PV, pr: PR, path: PATH}} |
| new_recon = recon_dict(new_dict) |
| old_recon = recon_dict(old_dict) |
| |
| del new_dict |
| del old_dict |
| |
| # Figure out what are changed, the new_recon would be changed |
| # by the print_xxx function. |
| # Newly added |
| cnt_added = print_added(new_recon, old_recon) |
| |
| # PV (including PE) and PR changed |
| # Let the bb.siggen handle them if verbose |
| cnt_rv = {} |
| if not args.verbose: |
| for i in ('pv', 'pr'): |
| cnt_rv[i] = print_vrchanged(new_recon, old_recon, i) |
| |
| # Dependencies changed (use bitbake-diffsigs) |
| cnt_dep = print_depchanged(new_recon, old_recon, args.verbose) |
| |
| total_changed = cnt_added + (cnt_rv.get('pv') or 0) + (cnt_rv.get('pr') or 0) + cnt_dep |
| |
| print("\n=== Summary: (%s changed, %s unchanged)" % (total_changed, cnt_unchanged)) |
| if args.verbose: |
| print("Newly added: %s\nDependencies changed: %s\n" % \ |
| (cnt_added, cnt_dep)) |
| else: |
| print("Newly added: %s\nPV changed: %s\nPR changed: %s\nDependencies changed: %s\n" % \ |
| (cnt_added, cnt_rv.get('pv') or 0, cnt_rv.get('pr') or 0, cnt_dep)) |
| except: |
| print("ERROR occurred!") |
| raise |
| finally: |
| # Remove the newly generated stamps dir |
| if os.path.exists(new_stampsdir): |
| print("Removing the newly generated stamps dir ...") |
| shutil.rmtree(new_stampsdir) |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |