xref: /openbmc/openbmc/poky/bitbake/lib/bb/ui/taskexp.py (revision c5535c91)
1#
2# BitBake Graphical GTK based Dependency Explorer
3#
4# Copyright (C) 2007        Ross Burton
5# Copyright (C) 2007 - 2008 Richard Purdie
6#
7# SPDX-License-Identifier: GPL-2.0-only
8#
9
10import sys
11import traceback
12
13try:
14    import gi
15    gi.require_version('Gtk', '3.0')
16    from gi.repository import Gtk, Gdk, GObject
17except ValueError:
18    sys.exit("FATAL: Gtk version needs to be 3.0")
19except ImportError:
20    sys.exit("FATAL: Gtk ui could not load the required gi python module")
21
22import threading
23from xmlrpc import client
24import bb
25import bb.event
26
27# Package Model
28(COL_PKG_NAME) = (0)
29
30# Dependency Model
31(TYPE_DEP, TYPE_RDEP) = (0, 1)
32(COL_DEP_TYPE, COL_DEP_PARENT, COL_DEP_PACKAGE) = (0, 1, 2)
33
34
35class PackageDepView(Gtk.TreeView):
36    def __init__(self, model, dep_type, label):
37        Gtk.TreeView.__init__(self)
38        self.current = None
39        self.dep_type = dep_type
40        self.filter_model = model.filter_new()
41        self.filter_model.set_visible_func(self._filter, data=None)
42        self.set_model(self.filter_model)
43        self.append_column(Gtk.TreeViewColumn(label, Gtk.CellRendererText(), text=COL_DEP_PACKAGE))
44
45    def _filter(self, model, iter, data):
46        this_type = model[iter][COL_DEP_TYPE]
47        package = model[iter][COL_DEP_PARENT]
48        if this_type != self.dep_type: return False
49        return package == self.current
50
51    def set_current_package(self, package):
52        self.current = package
53        self.filter_model.refilter()
54
55
56class PackageReverseDepView(Gtk.TreeView):
57    def __init__(self, model, label):
58        Gtk.TreeView.__init__(self)
59        self.current = None
60        self.filter_model = model.filter_new()
61        self.filter_model.set_visible_func(self._filter)
62        # The introspected API was fixed but we can't rely on a pygobject that hides this.
63        # https://gitlab.gnome.org/GNOME/pygobject/-/commit/9cdbc56fbac4db2de78dc080934b8f0a7efc892a
64        if hasattr(Gtk.TreeModelSort, "new_with_model"):
65            self.sort_model = Gtk.TreeModelSort.new_with_model(self.filter_model)
66        else:
67            self.sort_model = self.filter_model.sort_new_with_model()
68        self.sort_model.set_sort_column_id(COL_DEP_PARENT, Gtk.SortType.ASCENDING)
69        self.set_model(self.sort_model)
70        self.append_column(Gtk.TreeViewColumn(label, Gtk.CellRendererText(), text=COL_DEP_PARENT))
71
72    def _filter(self, model, iter, data):
73        package = model[iter][COL_DEP_PACKAGE]
74        return package == self.current
75
76    def set_current_package(self, package):
77        self.current = package
78        self.filter_model.refilter()
79
80
81class DepExplorer(Gtk.Window):
82    def __init__(self):
83        Gtk.Window.__init__(self)
84        self.set_title("Task Dependency Explorer")
85        self.set_default_size(500, 500)
86        self.connect("delete-event", Gtk.main_quit)
87
88        # Create the data models
89        self.pkg_model = Gtk.ListStore(GObject.TYPE_STRING)
90        self.pkg_model.set_sort_column_id(COL_PKG_NAME, Gtk.SortType.ASCENDING)
91        self.depends_model = Gtk.ListStore(GObject.TYPE_INT, GObject.TYPE_STRING, GObject.TYPE_STRING)
92        self.depends_model.set_sort_column_id(COL_DEP_PACKAGE, Gtk.SortType.ASCENDING)
93
94        pane = Gtk.HPaned()
95        pane.set_position(250)
96        self.add(pane)
97
98        # The master list of packages
99        scrolled = Gtk.ScrolledWindow()
100        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
101        scrolled.set_shadow_type(Gtk.ShadowType.IN)
102
103        self.pkg_treeview = Gtk.TreeView(self.pkg_model)
104        self.pkg_treeview.get_selection().connect("changed", self.on_cursor_changed)
105        column = Gtk.TreeViewColumn("Package", Gtk.CellRendererText(), text=COL_PKG_NAME)
106        self.pkg_treeview.append_column(column)
107        scrolled.add(self.pkg_treeview)
108
109        self.search_entry = Gtk.SearchEntry.new()
110        self.pkg_treeview.set_search_entry(self.search_entry)
111
112        left_panel = Gtk.VPaned()
113        left_panel.add(self.search_entry)
114        left_panel.add(scrolled)
115        pane.add1(left_panel)
116
117        box = Gtk.VBox(homogeneous=True, spacing=4)
118
119        # Task Depends
120        scrolled = Gtk.ScrolledWindow()
121        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
122        scrolled.set_shadow_type(Gtk.ShadowType.IN)
123        self.dep_treeview = PackageDepView(self.depends_model, TYPE_DEP, "Dependencies")
124        self.dep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE)
125        scrolled.add(self.dep_treeview)
126        box.add(scrolled)
127        pane.add2(box)
128
129        # Reverse Task Depends
130        scrolled = Gtk.ScrolledWindow()
131        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
132        scrolled.set_shadow_type(Gtk.ShadowType.IN)
133        self.revdep_treeview = PackageReverseDepView(self.depends_model, "Dependent Tasks")
134        self.revdep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PARENT)
135        scrolled.add(self.revdep_treeview)
136        box.add(scrolled)
137        pane.add2(box)
138
139        self.show_all()
140        self.search_entry.grab_focus()
141
142    def on_package_activated(self, treeview, path, column, data_col):
143        model = treeview.get_model()
144        package = model.get_value(model.get_iter(path), data_col)
145
146        pkg_path = []
147        def finder(model, path, iter, needle):
148            package = model.get_value(iter, COL_PKG_NAME)
149            if package == needle:
150                pkg_path.append(path)
151                return True
152            else:
153                return False
154        self.pkg_model.foreach(finder, package)
155        if pkg_path:
156            self.pkg_treeview.get_selection().select_path(pkg_path[0])
157            self.pkg_treeview.scroll_to_cell(pkg_path[0])
158
159    def on_cursor_changed(self, selection):
160        (model, it) = selection.get_selected()
161        if it is None:
162            current_package = None
163        else:
164            current_package = model.get_value(it, COL_PKG_NAME)
165        self.dep_treeview.set_current_package(current_package)
166        self.revdep_treeview.set_current_package(current_package)
167
168
169    def parse(self, depgraph):
170        for task in depgraph["tdepends"]:
171            self.pkg_model.insert(0, (task,))
172            for depend in depgraph["tdepends"][task]:
173                self.depends_model.insert (0, (TYPE_DEP, task, depend))
174
175
176class gtkthread(threading.Thread):
177    quit = threading.Event()
178    def __init__(self, shutdown):
179        threading.Thread.__init__(self)
180        self.daemon = True
181        self.shutdown = shutdown
182        if not Gtk.init_check()[0]:
183            sys.stderr.write("Gtk+ init failed. Make sure DISPLAY variable is set.\n")
184            gtkthread.quit.set()
185
186    def run(self):
187        GObject.threads_init()
188        Gdk.threads_init()
189        Gtk.main()
190        gtkthread.quit.set()
191
192
193def main(server, eventHandler, params):
194    shutdown = 0
195
196    gtkgui = gtkthread(shutdown)
197    gtkgui.start()
198
199    try:
200        params.updateToServer(server, os.environ.copy())
201        params.updateFromServer(server)
202        cmdline = params.parseActions()
203        if not cmdline:
204            print("Nothing to do.  Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
205            return 1
206        if 'msg' in cmdline and cmdline['msg']:
207            print(cmdline['msg'])
208            return 1
209        cmdline = cmdline['action']
210        if not cmdline or cmdline[0] != "generateDotGraph":
211            print("This UI requires the -g option")
212            return 1
213        ret, error = server.runCommand(["generateDepTreeEvent", cmdline[1], cmdline[2]])
214        if error:
215            print("Error running command '%s': %s" % (cmdline, error))
216            return 1
217        elif not ret:
218            print("Error running command '%s': returned %s" % (cmdline, ret))
219            return 1
220    except client.Fault as x:
221        print("XMLRPC Fault getting commandline:\n %s" % x)
222        return
223    except Exception as e:
224        print("Exception in startup:\n %s" % traceback.format_exc())
225        return
226
227    if gtkthread.quit.isSet():
228        return
229
230    Gdk.threads_enter()
231    dep = DepExplorer()
232    bardialog = Gtk.Dialog(parent=dep,
233            flags=Gtk.DialogFlags.MODAL|Gtk.DialogFlags.DESTROY_WITH_PARENT)
234    bardialog.set_default_size(400, 50)
235    box = bardialog.get_content_area()
236    pbar = Gtk.ProgressBar()
237    box.pack_start(pbar, True, True, 0)
238    bardialog.show_all()
239    bardialog.connect("delete-event", Gtk.main_quit)
240    Gdk.threads_leave()
241
242    progress_total = 0
243    while True:
244        try:
245            event = eventHandler.waitEvent(0.25)
246            if gtkthread.quit.isSet():
247                _, error = server.runCommand(["stateForceShutdown"])
248                if error:
249                    print('Unable to cleanly stop: %s' % error)
250                break
251
252            if event is None:
253                continue
254
255            if isinstance(event, bb.event.CacheLoadStarted):
256                progress_total = event.total
257                Gdk.threads_enter()
258                bardialog.set_title("Loading Cache")
259                pbar.set_fraction(0.0)
260                Gdk.threads_leave()
261
262            if isinstance(event, bb.event.CacheLoadProgress):
263                x = event.current
264                Gdk.threads_enter()
265                pbar.set_fraction(x * 1.0 / progress_total)
266                Gdk.threads_leave()
267                continue
268
269            if isinstance(event, bb.event.CacheLoadCompleted):
270                continue
271
272            if isinstance(event, bb.event.ParseStarted):
273                progress_total = event.total
274                if progress_total == 0:
275                    continue
276                Gdk.threads_enter()
277                pbar.set_fraction(0.0)
278                bardialog.set_title("Processing recipes")
279                Gdk.threads_leave()
280
281            if isinstance(event, bb.event.ParseProgress):
282                x = event.current
283                Gdk.threads_enter()
284                pbar.set_fraction(x * 1.0 / progress_total)
285                Gdk.threads_leave()
286                continue
287
288            if isinstance(event, bb.event.ParseCompleted):
289                Gdk.threads_enter()
290                bardialog.set_title("Generating dependency tree")
291                Gdk.threads_leave()
292                continue
293
294            if isinstance(event, bb.event.DepTreeGenerated):
295                Gdk.threads_enter()
296                bardialog.hide()
297                dep.parse(event._depgraph)
298                Gdk.threads_leave()
299
300            if isinstance(event, bb.command.CommandCompleted):
301                continue
302
303            if isinstance(event, bb.event.NoProvider):
304                print(str(event))
305
306                _, error = server.runCommand(["stateShutdown"])
307                if error:
308                    print('Unable to cleanly shutdown: %s' % error)
309                break
310
311            if isinstance(event, bb.command.CommandFailed):
312                print(str(event))
313                return event.exitcode
314
315            if isinstance(event, bb.command.CommandExit):
316                return event.exitcode
317
318            if isinstance(event, bb.cooker.CookerExit):
319                break
320
321            continue
322        except EnvironmentError as ioerror:
323            # ignore interrupted io
324            if ioerror.args[0] == 4:
325                pass
326        except KeyboardInterrupt:
327            if shutdown == 2:
328                print("\nThird Keyboard Interrupt, exit.\n")
329                break
330            if shutdown == 1:
331                print("\nSecond Keyboard Interrupt, stopping...\n")
332                _, error = server.runCommand(["stateForceShutdown"])
333                if error:
334                    print('Unable to cleanly stop: %s' % error)
335            if shutdown == 0:
336                print("\nKeyboard Interrupt, closing down...\n")
337                _, error = server.runCommand(["stateShutdown"])
338                if error:
339                    print('Unable to cleanly shutdown: %s' % error)
340            shutdown = shutdown + 1
341            pass
342