xref: /OK3568_Linux_fs/yocto/poky/scripts/bitbake-whatchanged (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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