summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/bb/ui/hob.py
diff options
context:
space:
mode:
authorDongxiao Xu <dongxiao.xu@intel.com>2011-11-28 14:32:40 +0800
committerRichard Purdie <richard.purdie@linuxfoundation.org>2012-02-24 18:04:27 +0000
commit656f9a07588cc00704825a78de9649ca4a1552b8 (patch)
tree653c7941689599994d5876162c540fb7ee22736e /bitbake/lib/bb/ui/hob.py
parent14df6d53b6856ec78322b9c0ef01e26c0406fe28 (diff)
downloadpoky-656f9a07588cc00704825a78de9649ca4a1552b8.tar.gz
Hob: A new implemetation (v2)
This commit implements a new design for hob Some of the new features: - Friendly new designed GUI. Quick response to user actions. - Two step builds support package generation and image generation. - Support running GUI seprarately from bitbake server. - Recipe/package selection and deselection. - Accurate customization for image contents and size. - Progress bars showing the parsing and build status. - Load/save user configurations from/into templates. (Bitbake rev: 4dacd29f9c957d20f4583330b51e5420f9c3338d) Signed-off-by: Dongxiao Xu <dongxiao.xu@intel.com> Signed-off-by: Shane Wang <shane.wang@intel.com> Signed-off-by: Liming An <limingx.l.an@intel.com> Signed-off-by: Fengxia Hua <fengxia.hua@intel.com> Designed-by: Belen Barros Pena <belen.barros.pena@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/bb/ui/hob.py')
-rwxr-xr-x[-rw-r--r--]bitbake/lib/bb/ui/hob.py1115
1 files changed, 48 insertions, 1067 deletions
diff --git a/bitbake/lib/bb/ui/hob.py b/bitbake/lib/bb/ui/hob.py
index 0fcaad54a7..429bb750dd 100644..100755
--- a/bitbake/lib/bb/ui/hob.py
+++ b/bitbake/lib/bb/ui/hob.py
@@ -1,9 +1,11 @@
1#!/usr/bin/env python
1# 2#
2# BitBake Graphical GTK User Interface 3# BitBake Graphical GTK User Interface
3# 4#
4# Copyright (C) 2011 Intel Corporation 5# Copyright (C) 2011 Intel Corporation
5# 6#
6# Authored by Joshua Lock <josh@linux.intel.com> 7# Authored by Joshua Lock <josh@linux.intel.com>
8# Authored by Dongxiao Xu <dongxiao.xu@intel.com>
7# 9#
8# This program is free software; you can redistribute it and/or modify 10# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License version 2 as 11# it under the terms of the GNU General Public License version 2 as
@@ -18,1087 +20,58 @@
18# with this program; if not, write to the Free Software Foundation, Inc., 20# with this program; if not, write to the Free Software Foundation, Inc.,
19# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 22
21import glib
22import gobject 23import gobject
23import gtk 24import gtk
24from bb.ui.crumbs.tasklistmodel import TaskListModel, BuildRep 25import sys
26import os
27sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
28try:
29 import bb
30except RuntimeError as exc:
31 sys.exit(str(exc))
32from bb.ui import uihelper
33from bb.ui.crumbs.hoblistmodel import RecipeListModel, PackageListModel
25from bb.ui.crumbs.hobeventhandler import HobHandler 34from bb.ui.crumbs.hobeventhandler import HobHandler
26from bb.ui.crumbs.configurator import Configurator 35from bb.ui.crumbs.builder import Builder
27from bb.ui.crumbs.hobprefs import HobPrefs
28from bb.ui.crumbs.layereditor import LayerEditor
29from bb.ui.crumbs.runningbuild import RunningBuildTreeView, RunningBuild
30from bb.ui.crumbs.hig import CrumbsDialog
31import xmlrpclib
32import logging
33import Queue
34 36
35extraCaches = ['bb.cache_extra:HobRecipeInfo'] 37extraCaches = ['bb.cache_extra:HobRecipeInfo']
36 38
37class MainWindow (gtk.Window): 39def event_handle_idle_func(eventHandler, hobHandler):
38 40 # Consume as many messages as we can in the time available to us
39 def __init__(self, taskmodel, handler, configurator, prefs, layers, mach): 41 if not eventHandler:
40 gtk.Window.__init__(self)
41 # global state
42 self.curr_mach = mach
43 self.machine_handler_id = None
44 self.image_combo_id = None
45 self.generating = False
46 self.files_to_clean = []
47 self.selected_image = None
48 self.selected_packages = None
49 self.stopping = False
50
51 self.model = taskmodel
52 self.model.connect("tasklist-populated", self.update_model)
53 self.model.connect("image-changed", self.image_changed_string_cb)
54 self.handler = handler
55 self.configurator = configurator
56 self.prefs = prefs
57 self.layers = layers
58 self.save_path = None
59 self.dirty = False
60 self.build_succeeded = False
61
62 self.connect("delete-event", self.destroy_window)
63 self.set_title("Image Creator")
64 self.set_icon_name("applications-development")
65 self.set_default_size(1000, 650)
66
67 self.build = RunningBuild(sequential=True)
68 self.build.connect("build-failed", self.running_build_failed_cb)
69 self.build.connect("build-succeeded", self.running_build_succeeded_cb)
70 self.build.connect("build-started", self.build_started_cb)
71 self.build.connect("build-complete", self.build_complete_cb)
72
73 vbox = gtk.VBox(False, 0)
74 vbox.set_border_width(0)
75 vbox.show()
76 self.add(vbox)
77 self.menu = self.create_menu()
78 vbox.pack_start(self.menu, False)
79 createview = self.create_build_gui()
80 self.back = None
81 self.cancel = None
82 buildview = self.view_build_gui()
83 self.nb = gtk.Notebook()
84 self.nb.append_page(createview)
85 self.nb.append_page(buildview)
86 self.nb.set_current_page(0)
87 self.nb.set_show_tabs(False)
88 vbox.pack_start(self.nb, expand=True, fill=True)
89
90 def destroy_window(self, widget, event):
91 self.quit()
92
93 def menu_quit(self, action):
94 self.quit()
95
96 def quit(self):
97 if self.dirty and len(self.model.contents):
98 question = "Would you like to save your customisations?"
99 dialog = CrumbsDialog(self, question, gtk.STOCK_DIALOG_WARNING)
100 dialog.add_buttons(gtk.STOCK_NO, gtk.RESPONSE_NO,
101 gtk.STOCK_YES, gtk.RESPONSE_YES)
102 resp = dialog.run()
103 dialog.destroy()
104 if resp == gtk.RESPONSE_YES:
105 if not self.save_path:
106 self.get_save_path()
107
108 if self.save_path:
109 self.save_recipe_file()
110 rep = self.model.get_build_rep()
111 rep.writeRecipe(self.save_path, self.model)
112
113 # Prevent the busy cursor being shown after hob exits if quit is called
114 # whilst the busy cursor is set
115 self.set_busy_cursor(False)
116
117 self.handler.remove_temp_dir()
118
119 gtk.main_quit()
120
121 """
122 In the case of a fatal error give the user as much information as possible
123 and then exit.
124 """
125 def fatal_error_cb(self, handler, errormsg, phase):
126 lbl = "<b>Error!</b>\nThere was an unrecoverable error during the"
127 lbl = lbl + " <i>%s</i> phase of BitBake. This must be" % phase
128 lbl = lbl + " rectified before the GUI will function. The error"
129 lbl = lbl + " message which which caused this is:\n\n\"%s\"" % errormsg
130 dialog = CrumbsDialog(self, lbl, gtk.STOCK_DIALOG_ERROR)
131 dialog.add_button("Exit", gtk.RESPONSE_OK)
132 response = dialog.run()
133 dialog.destroy()
134 self.set_busy_cursor(False)
135 gtk.main_quit()
136
137 def scroll_tv_cb(self, model, path, it, view):
138 view.scroll_to_cell(path)
139
140 def running_build_succeeded_cb(self, running_build):
141 self.build_succeeded = True
142
143 def running_build_failed_cb(self, running_build):
144 self.build_succeeded = False
145
146 def image_changed_string_cb(self, model, new_image):
147 self.selected_image = new_image
148 # disconnect the image combo's signal handler
149 if self.image_combo_id:
150 self.image_combo.disconnect(self.image_combo_id)
151 self.image_combo_id = None
152 cnt = 0
153 it = self.model.images.get_iter_first()
154 while it:
155 path = self.model.images.get_path(it)
156 if self.model.images[path][self.model.COL_NAME] == new_image:
157 self.image_combo.set_active(cnt)
158 break
159 it = self.model.images.iter_next(it)
160 cnt = cnt + 1
161 # Reconnect the signal handler
162 if not self.image_combo_id:
163 self.image_combo_id = self.image_combo.connect("changed", self.image_changed_cb)
164
165 def image_changed_cb(self, combo):
166 model = self.image_combo.get_model()
167 it = self.image_combo.get_active_iter()
168 if it:
169 path = model.get_path(it)
170 # Firstly, deselect the previous image
171 userp, _ = self.model.get_selected_packages()
172 self.model.reset()
173 # Now select the new image and save its path in case we
174 # change the image later
175 self.toggle_package(path, model, image=True)
176 if len(userp):
177 self.model.set_selected_packages(userp)
178 self.selected_image = model[path][self.model.COL_NAME]
179
180 def reload_triggered_cb(self, handler, image, packages):
181 if image:
182 self.selected_image = image
183 if len(packages):
184 self.selected_packages = packages.split()
185
186 def data_generated(self, handler):
187 self.generating = False
188 self.enable_widgets()
189
190 def machine_combo_changed_cb(self, combo, handler):
191 mach = combo.get_active_text()
192 if mach != self.curr_mach:
193 self.curr_mach = mach
194 # Flush this straight to the file as MACHINE is changed
195 # independently of other 'Preferences'
196 self.configurator.setConfVar('MACHINE', mach)
197 self.configurator.writeConf()
198 handler.set_machine(mach)
199 handler.reload_data()
200
201 def update_machines(self, handler, machines):
202 active = 0
203 # disconnect the signal handler before updating the combo model
204 if self.machine_handler_id:
205 self.machine_combo.disconnect(self.machine_handler_id)
206 self.machine_handler_id = None
207
208 model = self.machine_combo.get_model()
209 if model:
210 model.clear()
211
212 for machine in machines:
213 self.machine_combo.append_text(machine)
214 if machine == self.curr_mach:
215 self.machine_combo.set_active(active)
216 active = active + 1
217
218 self.machine_handler_id = self.machine_combo.connect("changed", self.machine_combo_changed_cb, handler)
219
220 def set_busy_cursor(self, busy=True):
221 """
222 Convenience method to set the cursor to a spinner when executing
223 a potentially lengthy process.
224 A busy value of False will set the cursor back to the default
225 left pointer.
226 """
227 if busy:
228 cursor = gtk.gdk.Cursor(gtk.gdk.WATCH)
229 else:
230 # TODO: presumably the default cursor is different on RTL
231 # systems. Can we determine the default cursor? Or at least
232 # the cursor which is set before we change it?
233 cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
234 window = self.get_root_window()
235 window.set_cursor(cursor)
236
237 def busy_idle_func(self):
238 if self.generating:
239 self.progress.pulse()
240 return True
241 else:
242 if not self.image_combo_id:
243 self.image_combo_id = self.image_combo.connect("changed", self.image_changed_cb)
244 self.progress.set_text("Loaded")
245 self.progress.set_fraction(0.0)
246 self.set_busy_cursor(False)
247 return False
248
249 def busy(self, handler):
250 self.generating = True
251 self.progress.set_text("Loading...")
252 self.set_busy_cursor()
253 if self.image_combo_id:
254 self.image_combo.disconnect(self.image_combo_id)
255 self.image_combo_id = None
256 self.progress.pulse()
257 gobject.timeout_add (100, self.busy_idle_func)
258 self.disable_widgets()
259
260 def enable_widgets(self):
261 self.menu.set_sensitive(True)
262 self.machine_combo.set_sensitive(True)
263 self.image_combo.set_sensitive(True)
264 self.nb.set_sensitive(True)
265 self.contents_tree.set_sensitive(True)
266
267 def disable_widgets(self):
268 self.menu.set_sensitive(False)
269 self.machine_combo.set_sensitive(False)
270 self.image_combo.set_sensitive(False)
271 self.nb.set_sensitive(False)
272 self.contents_tree.set_sensitive(False)
273
274 def update_model(self, model):
275 # We want the packages model to be alphabetised and sortable so create
276 # a TreeModelSort to use in the view
277 pkgsaz_model = gtk.TreeModelSort(self.model.packages_model())
278 pkgsaz_model.set_sort_column_id(self.model.COL_NAME, gtk.SORT_ASCENDING)
279 # Unset default sort func so that we only toggle between A-Z and
280 # Z-A sorting
281 pkgsaz_model.set_default_sort_func(None)
282 self.pkgsaz_tree.set_model(pkgsaz_model)
283
284 self.image_combo.set_model(self.model.images_model())
285 # Without this the image combo is incorrectly sized on first load of the GUI
286 self.image_combo.set_active(0)
287 self.image_combo.set_active(-1)
288
289 if not self.image_combo_id:
290 self.image_combo_id = self.image_combo.connect("changed", self.image_changed_cb)
291
292 # We want the contents to be alphabetised so create a TreeModelSort to
293 # use in the view
294 contents_model = gtk.TreeModelSort(self.model.contents_model())
295 contents_model.set_sort_column_id(self.model.COL_NAME, gtk.SORT_ASCENDING)
296 # Unset default sort func so that we only toggle between A-Z and
297 # Z-A sorting
298 contents_model.set_default_sort_func(None)
299 self.contents_tree.set_model(contents_model)
300 self.tasks_tree.set_model(self.model.tasks_model())
301
302 if self.selected_image:
303 if self.image_combo_id:
304 self.image_combo.disconnect(self.image_combo_id)
305 self.image_combo_id = None
306 self.model.set_selected_image(self.selected_image)
307 if not self.image_combo_id:
308 self.image_combo_id = self.image_combo.connect("changed", self.image_changed_cb)
309
310 if self.selected_packages:
311 self.model.set_selected_packages(self.selected_packages)
312
313 def reset_clicked_cb(self, button):
314 lbl = "<b>Reset your selections?</b>\n\nAny new changes you have made will be lost"
315 dialog = CrumbsDialog(self, lbl, gtk.STOCK_DIALOG_WARNING)
316 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
317 dialog.add_button("Reset", gtk.RESPONSE_OK)
318 response = dialog.run()
319 dialog.destroy()
320 if response == gtk.RESPONSE_OK:
321 self.reset_build()
322 self.search.set_text("")
323 self.selected_image = None
324 return
325
326 def reset_build(self):
327 if self.image_combo_id:
328 self.image_combo.disconnect(self.image_combo_id)
329 self.image_combo_id = None
330 self.image_combo.set_active(-1)
331 self.model.reset()
332 if not self.image_combo_id:
333 self.image_combo_id = self.image_combo.connect("changed", self.image_changed_cb)
334
335 def layers_cb(self, action):
336 resp = self.layers.run()
337 self.layers.save_current_layers()
338 self.layers.hide()
339
340 def add_layer_cb(self, action):
341 self.layers.find_layer(self)
342 self.layers.save_current_layers()
343
344 def preferences_cb(self, action):
345 resp = self.prefs.run()
346 self.prefs.write_changes()
347 self.prefs.hide()
348
349 def about_cb(self, action):
350 about = gtk.AboutDialog()
351 about.set_name("Image Creator")
352 about.set_copyright("Copyright (C) 2011 Intel Corporation")
353 about.set_authors(["Joshua Lock <josh@linux.intel.com>"])
354 about.set_logo_icon_name("applications-development")
355 about.run()
356 about.destroy()
357
358 def save_recipe_file(self):
359 rep = self.model.get_build_rep()
360 rep.writeRecipe(self.save_path, self.model)
361 self.dirty = False
362
363 def get_save_path(self):
364 chooser = gtk.FileChooserDialog(title=None, parent=self,
365 action=gtk.FILE_CHOOSER_ACTION_SAVE,
366 buttons=(gtk.STOCK_CANCEL,
367 gtk.RESPONSE_CANCEL,
368 gtk.STOCK_SAVE,
369 gtk.RESPONSE_OK,))
370 chooser.set_current_name("myimage.bb")
371 response = chooser.run()
372 if response == gtk.RESPONSE_OK:
373 save_path = chooser.get_filename()
374 else:
375 save_path = None
376 chooser.destroy()
377 self.save_path = save_path
378
379 def save_cb(self, action):
380 if not self.save_path:
381 self.get_save_path()
382 if self.save_path:
383 self.save_recipe_file()
384
385 def save_as_cb(self, action):
386 self.get_save_path()
387 if self.save_path:
388 self.save_recipe_file()
389
390 def open_cb(self, action):
391 chooser = gtk.FileChooserDialog(title=None, parent=self,
392 action=gtk.FILE_CHOOSER_ACTION_OPEN,
393 buttons=(gtk.STOCK_CANCEL,
394 gtk.RESPONSE_CANCEL,
395 gtk.STOCK_OPEN,
396 gtk.RESPONSE_OK))
397 response = chooser.run()
398 rep = BuildRep(None, None, None)
399 recipe = chooser.get_filename()
400 if response == gtk.RESPONSE_OK:
401 rep.loadRecipe(recipe)
402 self.save_path = recipe
403 self.model.load_image_rep(rep)
404 self.dirty = False
405 chooser.destroy()
406
407 def bake_clicked_cb(self, button):
408 build_image = True
409
410 rep = self.model.get_build_rep()
411
412 # If no base image and no user selected packages don't build anything
413 if not self.selected_image and not len(rep.userpkgs):
414 lbl = "<b>No selections made</b>\nYou have not made any selections"
415 lbl = lbl + " so there isn't anything to bake at this time."
416 dialog = CrumbsDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
417 dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
418 dialog.run()
419 dialog.destroy()
420 return
421 # Else if no base image, ask whether to just build packages or whether
422 # to build a rootfs with the selected packages in
423 elif not self.selected_image:
424 lbl = "<b>Build empty image or only packages?</b>\nA base image"
425 lbl = lbl + " has not been selected.\n\'Empty image' will build"
426 lbl = lbl + " an image with only the selected packages as its"
427 lbl = lbl + " contents.\n'Packages Only' will build only the"
428 lbl = lbl + " selected packages, no image will be created"
429 dialog = CrumbsDialog(self, lbl, gtk.STOCK_DIALOG_WARNING)
430 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
431 dialog.add_button("Empty Image", gtk.RESPONSE_OK)
432 dialog.add_button("Packages Only", gtk.RESPONSE_YES)
433 response = dialog.run()
434 dialog.destroy()
435 if response == gtk.RESPONSE_CANCEL:
436 return
437 elif response == gtk.RESPONSE_YES:
438 build_image = False
439 elif response == gtk.RESPONSE_OK:
440 rep.base_image = "empty"
441
442 # Ensure at least one value is set in IMAGE_FSTYPES.
443 have_selected_fstype = False
444 if (len(self.prefs.selected_image_types) and
445 len(self.prefs.selected_image_types[0])):
446 have_selected_fstype = True
447
448 if build_image and not have_selected_fstype:
449 lbl = "<b>No image output type selected</b>\nThere is no image output"
450 lbl = lbl + " selected for the build. Please set an output image type"
451 lbl = lbl + " in the preferences (Edit -> Preferences)."
452 dialog = CrumbsDialog(self, lbl, gtk.STOCK_DIALOG_INFO)
453 dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
454 dialog.run()
455 dialog.destroy()
456 return
457 elif build_image:
458 self.handler.make_temp_dir()
459 recipepath = self.handler.get_temp_recipe_path(rep.base_image)
460 image_name = recipepath.rstrip(".bb")
461 path, sep, image_name = image_name.rpartition("/")
462
463 image = []
464 image.append(image_name)
465
466 rep.writeRecipe(recipepath, self.model)
467 # In the case where we saved the file for the purpose of building
468 # it we should then delete it so that the users workspace doesn't
469 # contain files they haven't explicitly saved there.
470 if not self.save_path:
471 self.files_to_clean.append(recipepath)
472
473 self.handler.build_targets(image, self.configurator)
474 else:
475 self.handler.build_targets(self.model.get_selected_pn(), self.configurator, "packages")
476
477 # Disable parts of the menu which shouldn't be used whilst building
478 self.set_menus_sensitive(False)
479 self.nb.set_current_page(1)
480
481 def set_menus_sensitive(self, sensitive):
482 self.add_layers_action.set_sensitive(sensitive)
483 self.layers_action.set_sensitive(sensitive)
484 self.prefs_action.set_sensitive(sensitive)
485 self.open_action.set_sensitive(sensitive)
486
487 def back_button_clicked_cb(self, button):
488 self.toggle_createview()
489
490 def toggle_createview(self):
491 self.set_menus_sensitive(True)
492 self.build.reset()
493 self.nb.set_current_page(0)
494
495 def build_complete_cb(self, running_build):
496 # Have the handler process BB events again
497 self.handler.building = False
498 self.stopping = False
499 self.back.connect("clicked", self.back_button_clicked_cb)
500 self.back.set_sensitive(True)
501 self.cancel.set_sensitive(False)
502 for f in self.files_to_clean:
503 try:
504 os.remove(f)
505 except OSError:
506 pass
507 self.files_to_clean.remove(f)
508 self.files_to_clean = []
509
510 lbl = "<b>Build completed</b>\n\nClick 'Edit Image' to start another build or 'View Messages' to view the messages output during the build."
511 if self.handler.build_type == "image" and self.build_succeeded:
512 deploy = self.handler.get_image_deploy_dir()
513 lbl = lbl + "\n<a href=\"file://%s\" title=\"%s\">Browse folder of built images</a>." % (deploy, deploy)
514
515 dialog = CrumbsDialog(self, lbl)
516 dialog.add_button("View Messages", gtk.RESPONSE_CANCEL)
517 dialog.add_button("Edit Image", gtk.RESPONSE_OK)
518 response = dialog.run()
519 dialog.destroy()
520 if response == gtk.RESPONSE_OK:
521 self.toggle_createview()
522
523 def build_started_cb(self, running_build):
524 self.back.set_sensitive(False)
525 self.cancel.set_sensitive(True)
526
527 def include_gplv3_cb(self, toggle):
528 excluded = toggle.get_active()
529 self.handler.toggle_gplv3(excluded)
530
531 def change_bb_threads(self, spinner):
532 val = spinner.get_value_as_int()
533 self.handler.set_bbthreads(val)
534
535 def change_make_threads(self, spinner):
536 val = spinner.get_value_as_int()
537 self.handler.set_pmake(val)
538
539 def toggle_toolchain(self, check):
540 enabled = check.get_active()
541 self.handler.toggle_toolchain(enabled)
542
543 def toggle_headers(self, check):
544 enabled = check.get_active()
545 self.handler.toggle_toolchain_headers(enabled)
546
547 def toggle_package_idle_cb(self, opath, image):
548 """
549 As the operations which we're calling on the model can take
550 a significant amount of time (in the order of seconds) during which
551 the GUI is unresponsive as the main loop is blocked perform them in
552 an idle function which at least enables us to set the busy cursor
553 before the UI is blocked giving the appearance of being responsive.
554 """
555 # Whether the item is currently included
556 inc = self.model[opath][self.model.COL_INC]
557 # FIXME: due to inpredictability of the removal of packages we are
558 # temporarily disabling this feature
559 # If the item is already included, mark it for removal then
560 # the sweep_up() method finds affected items and marks them
561 # appropriately
562 # if inc:
563 # self.model.mark(opath)
564 # self.model.sweep_up()
565 # # If the item isn't included, mark it for inclusion
566 # else:
567 if not inc:
568 self.model.include_item(item_path=opath,
569 binb="User Selected",
570 image_contents=image)
571
572 self.set_busy_cursor(False)
573 return False 42 return False
43 event = eventHandler.getEvent()
44 while event:
45 hobHandler.handle_event(event)
46 event = eventHandler.getEvent()
47 return True
48
49def main (server = None, eventHandler = None):
50 bitbake_server = None
51 client_addr = None
52 server_addr = None
53
54 if not eventHandler:
55 helper = uihelper.BBUIHelper()
56 server, eventHandler, server_addr, client_addr = helper.findServerDetails()
57 bitbake_server = server
574 58
575 def toggle_package(self, path, model, image=False):
576 inc = model[path][self.model.COL_INC]
577 # Warn user before removing included packages
578 if inc:
579 # FIXME: due to inpredictability of the removal of packages we are
580 # temporarily disabling this feature
581 return
582 # pn = model[path][self.model.COL_NAME]
583 # revdeps = self.model.find_reverse_depends(pn)
584 # if len(revdeps):
585 # lbl = "<b>Remove %s?</b>\n\nThis action cannot be undone and all packages which depend on this will be removed\nPackages which depend on %s include %s." % (pn, pn, ", ".join(revdeps).rstrip(","))
586 # else:
587 # lbl = "<b>Remove %s?</b>\n\nThis action cannot be undone." % pn
588 # dialog = CrumbsDialog(self, lbl, gtk.STOCK_DIALOG_WARNING)
589 # dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
590 # dialog.add_button("Remove", gtk.RESPONSE_OK)
591 # response = dialog.run()
592 # dialog.destroy()
593 # if response == gtk.RESPONSE_CANCEL:
594 # return
595
596 self.set_busy_cursor()
597 # Convert path to path in original model
598 opath = model.convert_path_to_child_path(path)
599 # This is a potentially length call which can block the
600 # main loop, therefore do the work in an idle func to keep
601 # the UI responsive
602 glib.idle_add(self.toggle_package_idle_cb, opath, image)
603
604 self.dirty = True
605
606 def toggle_include_cb(self, cell, path, tv):
607 model = tv.get_model()
608 self.toggle_package(path, model)
609
610 def toggle_pkg_include_cb(self, cell, path, tv):
611 # there's an extra layer of models in the packages case.
612 sort_model = tv.get_model()
613 cpath = sort_model.convert_path_to_child_path(path)
614 self.toggle_package(cpath, sort_model.get_model())
615
616 def pkgsaz(self):
617 vbox = gtk.VBox(False, 6)
618 vbox.show()
619 self.pkgsaz_tree = gtk.TreeView()
620 self.pkgsaz_tree.set_headers_visible(True)
621 self.pkgsaz_tree.set_headers_clickable(True)
622 self.pkgsaz_tree.set_enable_search(True)
623 self.pkgsaz_tree.set_search_column(0)
624 self.pkgsaz_tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
625
626 col = gtk.TreeViewColumn('Package')
627 col.set_clickable(True)
628 col.set_sort_column_id(self.model.COL_NAME)
629 col.set_min_width(220)
630 col1 = gtk.TreeViewColumn('Description')
631 col1.set_resizable(True)
632 col1.set_min_width(360)
633 col2 = gtk.TreeViewColumn('License')
634 col2.set_resizable(True)
635 col2.set_clickable(True)
636 col2.set_sort_column_id(self.model.COL_LIC)
637 col2.set_min_width(170)
638 col3 = gtk.TreeViewColumn('Group')
639 col3.set_clickable(True)
640 col3.set_sort_column_id(self.model.COL_GROUP)
641 col4 = gtk.TreeViewColumn('Included')
642 col4.set_min_width(80)
643 col4.set_max_width(90)
644 col4.set_sort_column_id(self.model.COL_INC)
645
646 self.pkgsaz_tree.append_column(col)
647 self.pkgsaz_tree.append_column(col1)
648 self.pkgsaz_tree.append_column(col2)
649 self.pkgsaz_tree.append_column(col3)
650 self.pkgsaz_tree.append_column(col4)
651
652 cell = gtk.CellRendererText()
653 cell1 = gtk.CellRendererText()
654 cell1.set_property('width-chars', 20)
655 cell2 = gtk.CellRendererText()
656 cell2.set_property('width-chars', 20)
657 cell3 = gtk.CellRendererText()
658 cell4 = gtk.CellRendererToggle()
659 cell4.set_property('activatable', True)
660 cell4.connect("toggled", self.toggle_pkg_include_cb, self.pkgsaz_tree)
661
662 col.pack_start(cell, True)
663 col1.pack_start(cell1, True)
664 col2.pack_start(cell2, True)
665 col3.pack_start(cell3, True)
666 col4.pack_end(cell4, True)
667
668 col.set_attributes(cell, text=self.model.COL_NAME)
669 col1.set_attributes(cell1, text=self.model.COL_DESC)
670 col2.set_attributes(cell2, text=self.model.COL_LIC)
671 col3.set_attributes(cell3, text=self.model.COL_GROUP)
672 col4.set_attributes(cell4, active=self.model.COL_INC)
673
674 self.pkgsaz_tree.show()
675
676 scroll = gtk.ScrolledWindow()
677 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
678 scroll.set_shadow_type(gtk.SHADOW_IN)
679 scroll.add(self.pkgsaz_tree)
680 vbox.pack_start(scroll, True, True, 0)
681
682 hb = gtk.HBox(False, 0)
683 hb.show()
684 self.search = gtk.Entry()
685 self.search.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, "gtk-clear")
686 self.search.connect("icon-release", self.search_entry_clear_cb)
687 self.search.show()
688 self.pkgsaz_tree.set_search_entry(self.search)
689 hb.pack_end(self.search, False, False, 0)
690 label = gtk.Label("Search packages:")
691 label.show()
692 hb.pack_end(label, False, False, 6)
693 vbox.pack_start(hb, False, False, 0)
694
695 return vbox
696
697 def search_entry_clear_cb(self, entry, icon_pos, event):
698 entry.set_text("")
699
700 def tasks(self):
701 vbox = gtk.VBox(False, 6)
702 vbox.show()
703 self.tasks_tree = gtk.TreeView()
704 self.tasks_tree.set_headers_visible(True)
705 self.tasks_tree.set_headers_clickable(False)
706 self.tasks_tree.set_enable_search(True)
707 self.tasks_tree.set_search_column(0)
708 self.tasks_tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
709
710 col = gtk.TreeViewColumn('Package Collection')
711 col.set_min_width(430)
712 col1 = gtk.TreeViewColumn('Description')
713 col1.set_min_width(430)
714 col2 = gtk.TreeViewColumn('Include')
715 col2.set_min_width(70)
716 col2.set_max_width(80)
717
718 self.tasks_tree.append_column(col)
719 self.tasks_tree.append_column(col1)
720 self.tasks_tree.append_column(col2)
721
722 cell = gtk.CellRendererText()
723 cell1 = gtk.CellRendererText()
724 cell2 = gtk.CellRendererToggle()
725 cell2.set_property('activatable', True)
726 cell2.connect("toggled", self.toggle_include_cb, self.tasks_tree)
727
728 col.pack_start(cell, True)
729 col1.pack_start(cell1, True)
730 col2.pack_end(cell2, True)
731
732 col.set_attributes(cell, text=self.model.COL_NAME)
733 col1.set_attributes(cell1, text=self.model.COL_DESC)
734 col2.set_attributes(cell2, active=self.model.COL_INC)
735
736 self.tasks_tree.show()
737
738 scroll = gtk.ScrolledWindow()
739 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
740 scroll.set_shadow_type(gtk.SHADOW_IN)
741 scroll.add(self.tasks_tree)
742 vbox.pack_start(scroll, True, True, 0)
743
744 hb = gtk.HBox(False, 0)
745 hb.show()
746 search = gtk.Entry()
747 search.show()
748 self.tasks_tree.set_search_entry(search)
749 hb.pack_end(search, False, False, 0)
750 label = gtk.Label("Search collections:")
751 label.show()
752 hb.pack_end(label, False, False, 6)
753 vbox.pack_start(hb, False, False, 0)
754
755 return vbox
756
757 def cancel_build(self, button):
758 if self.stopping:
759 lbl = "<b>Force Stop build?</b>\nYou've already selected Stop once,"
760 lbl = lbl + " would you like to 'Force Stop' the build?\n\n"
761 lbl = lbl + "This will stop the build as quickly as possible but may"
762 lbl = lbl + " well leave your build directory in an unusable state"
763 lbl = lbl + " that requires manual steps to fix.\n"
764 dialog = CrumbsDialog(self, lbl, gtk.STOCK_DIALOG_WARNING)
765 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
766 dialog.add_button("Force Stop", gtk.RESPONSE_YES)
767 else:
768 lbl = "<b>Stop build?</b>\n\nAre you sure you want to stop this"
769 lbl = lbl + " build?\n\n'Force Stop' will stop the build as quickly as"
770 lbl = lbl + " possible but may well leave your build directory in an"
771 lbl = lbl + " unusable state that requires manual steps to fix.\n\n"
772 lbl = lbl + "'Stop' will stop the build as soon as all in"
773 lbl = lbl + " progress build tasks are finished. However if a"
774 lbl = lbl + " lengthy compilation phase is in progress this may take"
775 lbl = lbl + " some time."
776 dialog = CrumbsDialog(self, lbl, gtk.STOCK_DIALOG_WARNING)
777 dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
778 dialog.add_button("Stop", gtk.RESPONSE_OK)
779 dialog.add_button("Force Stop", gtk.RESPONSE_YES)
780 response = dialog.run()
781 dialog.destroy()
782 if response != gtk.RESPONSE_CANCEL:
783 self.stopping = True
784 if response == gtk.RESPONSE_OK:
785 self.handler.cancel_build()
786 elif response == gtk.RESPONSE_YES:
787 self.handler.cancel_build(True)
788
789 def view_build_gui(self):
790 vbox = gtk.VBox(False, 12)
791 vbox.set_border_width(6)
792 vbox.show()
793 build_tv = RunningBuildTreeView(readonly=True)
794 build_tv.show()
795 build_tv.set_model(self.build.model)
796 self.build.model.connect("row-inserted", self.scroll_tv_cb, build_tv)
797 scrolled_view = gtk.ScrolledWindow ()
798 scrolled_view.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
799 scrolled_view.add(build_tv)
800 scrolled_view.show()
801 vbox.pack_start(scrolled_view, expand=True, fill=True)
802 hbox = gtk.HBox(False, 12)
803 hbox.show()
804 vbox.pack_start(hbox, expand=False, fill=False)
805 self.back = gtk.Button("Back")
806 self.back.show()
807 self.back.set_sensitive(False)
808 hbox.pack_start(self.back, expand=False, fill=False)
809 self.cancel = gtk.Button("Stop Build")
810 self.cancel.connect("clicked", self.cancel_build)
811 self.cancel.show()
812 hbox.pack_end(self.cancel, expand=False, fill=False)
813
814 return vbox
815
816 def create_menu(self):
817 menu_items = '''<ui>
818 <menubar name="MenuBar">
819 <menu action="File">
820 <menuitem action="Save"/>
821 <menuitem action="Save As"/>
822 <menuitem action="Open"/>
823 <separator/>
824 <menuitem action="AddLayer" label="Add Layer"/>
825 <separator/>
826 <menuitem action="Quit"/>
827 </menu>
828 <menu action="Edit">
829 <menuitem action="Layers" label="Layers"/>
830 <menuitem action="Preferences"/>
831 </menu>
832 <menu action="Help">
833 <menuitem action="About"/>
834 </menu>
835 </menubar>
836 </ui>'''
837
838 uimanager = gtk.UIManager()
839 accel = uimanager.get_accel_group()
840 self.add_accel_group(accel)
841
842 actions = gtk.ActionGroup('ImageCreator')
843 self.actions = actions
844 actions.add_actions([('Quit', gtk.STOCK_QUIT, None, None, None, self.menu_quit,),
845 ('File', None, '_File'),
846 ('Save', gtk.STOCK_SAVE, None, None, None, self.save_cb),
847 ('Save As', gtk.STOCK_SAVE_AS, None, None, None, self.save_as_cb),
848 ('Edit', None, '_Edit'),
849 ('Help', None, '_Help'),
850 ('About', gtk.STOCK_ABOUT, None, None, None, self.about_cb)])
851
852 self.add_layers_action = gtk.Action('AddLayer', 'Add Layer', None, None)
853 self.add_layers_action.connect("activate", self.add_layer_cb)
854 self.actions.add_action(self.add_layers_action)
855 self.layers_action = gtk.Action('Layers', 'Layers', None, None)
856 self.layers_action.connect("activate", self.layers_cb)
857 self.actions.add_action(self.layers_action)
858 self.prefs_action = gtk.Action('Preferences', 'Preferences', None, None)
859 self.prefs_action.connect("activate", self.preferences_cb)
860 self.actions.add_action(self.prefs_action)
861 self.open_action = gtk.Action('Open', 'Open', None, None)
862 self.open_action.connect("activate", self.open_cb)
863 self.actions.add_action(self.open_action)
864
865 uimanager.insert_action_group(actions, 0)
866 uimanager.add_ui_from_string(menu_items)
867
868 menubar = uimanager.get_widget('/MenuBar')
869 menubar.show_all()
870
871 return menubar
872
873 def info_button_clicked_cb(self, button):
874 info = "We cannot accurately predict the image contents before they are built so instead a best"
875 info = info + " attempt at estimating what the image will contain is listed."
876 dialog = CrumbsDialog(self, info, gtk.STOCK_DIALOG_INFO)
877 dialog.add_buttons(gtk.STOCK_CLOSE, gtk.RESPONSE_OK)
878 resp = dialog.run()
879 dialog.destroy()
880
881 def create_build_gui(self):
882 vbox = gtk.VBox(False, 12)
883 vbox.set_border_width(6)
884 vbox.show()
885
886 hbox = gtk.HBox(False, 12)
887 hbox.show()
888 vbox.pack_start(hbox, expand=False, fill=False)
889
890 label = gtk.Label("Machine:")
891 label.show()
892 hbox.pack_start(label, expand=False, fill=False, padding=6)
893 self.machine_combo = gtk.combo_box_new_text()
894 self.machine_combo.show()
895 self.machine_combo.set_tooltip_text("Selects the architecture of the target board for which you would like to build an image.")
896 hbox.pack_start(self.machine_combo, expand=False, fill=False, padding=6)
897 label = gtk.Label("Base image:")
898 label.show()
899 hbox.pack_start(label, expand=False, fill=False, padding=6)
900 self.image_combo = gtk.ComboBox()
901 self.image_combo.show()
902 self.image_combo.set_tooltip_text("Selects the image on which to base the created image")
903 image_combo_cell = gtk.CellRendererText()
904 self.image_combo.pack_start(image_combo_cell, True)
905 self.image_combo.add_attribute(image_combo_cell, 'text', self.model.COL_NAME)
906 hbox.pack_start(self.image_combo, expand=False, fill=False, padding=6)
907 self.progress = gtk.ProgressBar()
908 self.progress.set_size_request(250, -1)
909 hbox.pack_end(self.progress, expand=False, fill=False, padding=6)
910
911 ins = gtk.Notebook()
912 vbox.pack_start(ins, expand=True, fill=True)
913 ins.set_show_tabs(True)
914 label = gtk.Label("Packages")
915 label.show()
916 ins.append_page(self.pkgsaz(), tab_label=label)
917 label = gtk.Label("Package Collections")
918 label.show()
919 ins.append_page(self.tasks(), tab_label=label)
920 ins.set_current_page(0)
921 ins.show_all()
922
923 hbox = gtk.HBox(False, 1)
924 hbox.show()
925 label = gtk.Label("Estimated image contents:")
926 self.model.connect("contents-changed", self.update_package_count_cb, label)
927 label.set_property("xalign", 0.00)
928 label.show()
929 hbox.pack_start(label, expand=False, fill=False, padding=6)
930 info = gtk.Button("?")
931 info.set_tooltip_text("What does this mean?")
932 info.show()
933 info.connect("clicked", self.info_button_clicked_cb)
934 hbox.pack_start(info, expand=False, fill=False, padding=6)
935 vbox.pack_start(hbox, expand=False, fill=False, padding=6)
936 con = self.contents()
937 con.show()
938 vbox.pack_start(con, expand=True, fill=True)
939
940 bbox = gtk.HButtonBox()
941 bbox.set_spacing(12)
942 bbox.set_layout(gtk.BUTTONBOX_END)
943 bbox.show()
944 vbox.pack_start(bbox, expand=False, fill=False)
945 reset = gtk.Button("Reset")
946 reset.connect("clicked", self.reset_clicked_cb)
947 reset.show()
948 bbox.add(reset)
949 bake = gtk.Button("Bake")
950 bake.connect("clicked", self.bake_clicked_cb)
951 bake.show()
952 bbox.add(bake)
953
954 return vbox
955
956 def update_package_count_cb(self, model, count, label):
957 lbl = "Estimated image contents (%s packages):" % count
958 label.set_text(lbl)
959
960 def contents(self):
961 self.contents_tree = gtk.TreeView()
962 self.contents_tree.set_headers_visible(True)
963 self.contents_tree.get_selection().set_mode(gtk.SELECTION_SINGLE)
964
965 # allow searching in the package column
966 self.contents_tree.set_search_column(0)
967 self.contents_tree.set_enable_search(True)
968
969 col = gtk.TreeViewColumn('Package')
970 col.set_sort_column_id(0)
971 col.set_min_width(430)
972 col1 = gtk.TreeViewColumn('Brought in by')
973 col1.set_resizable(True)
974 col1.set_min_width(430)
975
976 self.contents_tree.append_column(col)
977 self.contents_tree.append_column(col1)
978
979 cell = gtk.CellRendererText()
980 cell1 = gtk.CellRendererText()
981 cell1.set_property('width-chars', 20)
982
983 col.pack_start(cell, True)
984 col1.pack_start(cell1, True)
985
986 col.set_attributes(cell, text=self.model.COL_NAME)
987 col1.set_attributes(cell1, text=self.model.COL_BINB)
988
989 self.contents_tree.show()
990
991 scroll = gtk.ScrolledWindow()
992 scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
993 scroll.set_shadow_type(gtk.SHADOW_IN)
994 scroll.add(self.contents_tree)
995
996 return scroll
997
998def main (server, eventHandler):
999 gobject.threads_init() 59 gobject.threads_init()
1000 60
1001 # NOTE: For now we require that the user run with pre and post files to 61 # That indicates whether the Hob and the bitbake server are
1002 # read and store configuration set in the GUI. 62 # running on different machines
1003 # We hope to adjust this long term as tracked in Yocto Bugzilla #1441 63 # recipe model and package model
1004 # http://bugzilla.pokylinux.org/show_bug.cgi?id=1441 64 recipe_model = RecipeListModel()
1005 reqfiles = 0 65 package_model = PackageListModel()
1006 dep_files = server.runCommand(["getVariable", "__depends"]) or set()
1007 dep_files.union(server.runCommand(["getVariable", "__base_depends"]) or set())
1008 for f in dep_files:
1009 if f[0].endswith("hob-pre.conf"):
1010 reqfiles = reqfiles + 1
1011 elif f[0].endswith("hob-post.conf"):
1012 reqfiles = reqfiles + 1
1013 if reqfiles == 2:
1014 break
1015 if reqfiles < 2:
1016 print("""The hob UI requires a pre file named hob-pre.conf and a post
1017file named hob-post.conf to store and read its configuration from. Please run
1018hob with these files, i.e.\n
1019\bitbake -u hob -r conf/hob-pre.conf -R conf/hob-post.conf""")
1020 return
1021 66
1022 taskmodel = TaskListModel() 67 hobHandler = HobHandler(bitbake_server, server_addr, client_addr, recipe_model, package_model)
1023 configurator = Configurator() 68 if hobHandler.kick() == False:
1024 handler = HobHandler(taskmodel, server)
1025 mach = server.runCommand(["getVariable", "MACHINE"])
1026 sdk_mach = server.runCommand(["getVariable", "SDKMACHINE"])
1027 # If SDKMACHINE not set the default SDK_ARCH is used so we
1028 # should represent that in the GUI
1029 if not sdk_mach:
1030 sdk_mach = server.runCommand(["getVariable", "SDK_ARCH"])
1031 distro = server.runCommand(["getVariable", "DISTRO"])
1032 if not distro:
1033 distro = "defaultsetup"
1034 bbthread = server.runCommand(["getVariable", "BB_NUMBER_THREADS"])
1035 if not bbthread:
1036 bbthread = 1
1037 else:
1038 bbthread = int(bbthread)
1039 pmake = server.runCommand(["getVariable", "PARALLEL_MAKE"])
1040 if not pmake:
1041 pmake = 1
1042 else:
1043 # The PARALLEL_MAKE variable will be of the format: "-j 3" and we only
1044 # want a number for the spinner, so strip everything from the variable
1045 # up to and including the space
1046 pmake = int(pmake.lstrip("-j "))
1047
1048 selected_image_types = server.runCommand(["getVariable", "IMAGE_FSTYPES"])
1049 all_image_types = server.runCommand(["getVariable", "IMAGE_TYPES"])
1050
1051 pclasses = server.runCommand(["getVariable", "PACKAGE_CLASSES"]).split(" ")
1052 # NOTE: we're only supporting one value for PACKAGE_CLASSES being set
1053 # this seems OK because we're using the first package format set in
1054 # PACKAGE_CLASSES and that's the package manager used for the rootfs
1055 pkg, sep, pclass = pclasses[0].rpartition("_")
1056
1057 incompatible = server.runCommand(["getVariable", "INCOMPATIBLE_LICENSE"])
1058 gplv3disabled = False
1059 if incompatible and incompatible.lower().find("gplv3") != -1:
1060 gplv3disabled = True
1061
1062 build_toolchain = bool(server.runCommand(["getVariable", "HOB_BUILD_TOOLCHAIN"]))
1063 handler.toggle_toolchain(build_toolchain)
1064 build_headers = bool(server.runCommand(["getVariable", "HOB_BUILD_TOOLCHAIN_HEADERS"]))
1065 handler.toggle_toolchain_headers(build_headers)
1066
1067 prefs = HobPrefs(configurator, handler, sdk_mach, distro, pclass,
1068 pmake, bbthread, selected_image_types, all_image_types,
1069 gplv3disabled, build_toolchain, build_headers)
1070 layers = LayerEditor(configurator, None)
1071 window = MainWindow(taskmodel, handler, configurator, prefs, layers, mach)
1072 prefs.set_parent_window(window)
1073 layers.set_parent_window(window)
1074 window.show_all ()
1075 handler.connect("machines-updated", window.update_machines)
1076 handler.connect("sdk-machines-updated", prefs.update_sdk_machines)
1077 handler.connect("distros-updated", prefs.update_distros)
1078 handler.connect("package-formats-found", prefs.update_package_formats)
1079 handler.connect("generating-data", window.busy)
1080 handler.connect("data-generated", window.data_generated)
1081 handler.connect("reload-triggered", window.reload_triggered_cb)
1082 configurator.connect("layers-loaded", layers.load_current_layers)
1083 configurator.connect("layers-changed", handler.reload_data)
1084 handler.connect("config-found", configurator.configFound)
1085 handler.connect("fatal-error", window.fatal_error_cb)
1086
1087 try:
1088 # kick the while thing off
1089 handler.current_command = handler.CFG_PATH_LOCAL
1090 server.runCommand(["findConfigFilePath", "local.conf"])
1091 except xmlrpclib.Fault:
1092 print("XMLRPC Fault getting commandline:\n %s" % x)
1093 return 1 69 return 1
70 builder = Builder(hobHandler, recipe_model, package_model)
1094 71
1095 # This timeout function regularly probes the event queue to find out if we 72 # This timeout function regularly probes the event queue to find out if we
1096 # have any messages waiting for us. 73 # have any messages waiting for us.
1097 gobject.timeout_add (100, 74 gobject.timeout_add(10, event_handle_idle_func, eventHandler, hobHandler)
1098 handler.event_handle_idle_func,
1099 eventHandler,
1100 window.build,
1101 window.progress)
1102 75
1103 try: 76 try:
1104 gtk.main() 77 gtk.main()
@@ -1107,5 +80,13 @@ hob with these files, i.e.\n
1107 if ioerror.args[0] == 4: 80 if ioerror.args[0] == 4:
1108 pass 81 pass
1109 finally: 82 finally:
1110 server.runCommand(["stateStop"]) 83 hobHandler.cancel_build(force = True)
1111 84
85if __name__ == "__main__":
86 try:
87 ret = main()
88 except Exception:
89 ret = 1
90 import traceback
91 traceback.print_exc(15)
92 sys.exit(ret)