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