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