diff options
Diffstat (limited to 'bitbake-dev/lib/bb/ui/puccho.py')
-rw-r--r-- | bitbake-dev/lib/bb/ui/puccho.py | 426 |
1 files changed, 426 insertions, 0 deletions
diff --git a/bitbake-dev/lib/bb/ui/puccho.py b/bitbake-dev/lib/bb/ui/puccho.py new file mode 100644 index 0000000000..a6a613f1cf --- /dev/null +++ b/bitbake-dev/lib/bb/ui/puccho.py | |||
@@ -0,0 +1,426 @@ | |||
1 | # | ||
2 | # BitBake Graphical GTK User Interface | ||
3 | # | ||
4 | # Copyright (C) 2008 Intel Corporation | ||
5 | # | ||
6 | # Authored by Rob Bradford <rob@linux.intel.com> | ||
7 | # | ||
8 | # 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 | ||
10 | # published by the Free Software Foundation. | ||
11 | # | ||
12 | # This program is distributed in the hope that it will be useful, | ||
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
15 | # GNU General Public License for more details. | ||
16 | # | ||
17 | # You should have received a copy of the GNU General Public License along | ||
18 | # with this program; if not, write to the Free Software Foundation, Inc., | ||
19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||
20 | |||
21 | import gtk | ||
22 | import gobject | ||
23 | import gtk.glade | ||
24 | import threading | ||
25 | import urllib2 | ||
26 | import os | ||
27 | import datetime | ||
28 | |||
29 | from bb.ui.crumbs.buildmanager import BuildManager, BuildConfiguration | ||
30 | from bb.ui.crumbs.buildmanager import BuildManagerTreeView | ||
31 | |||
32 | from bb.ui.crumbs.runningbuild import RunningBuild, RunningBuildTreeView | ||
33 | |||
34 | # The metadata loader is used by the BuildSetupDialog to download the | ||
35 | # available options to populate the dialog | ||
36 | class MetaDataLoader(gobject.GObject): | ||
37 | """ This class provides the mechanism for loading the metadata (the | ||
38 | fetching and parsing) from a given URL. The metadata encompasses details | ||
39 | on what machines are available. The distribution and images available for | ||
40 | the machine and the the uris to use for building the given machine.""" | ||
41 | __gsignals__ = { | ||
42 | 'success' : (gobject.SIGNAL_RUN_LAST, | ||
43 | gobject.TYPE_NONE, | ||
44 | ()), | ||
45 | 'error' : (gobject.SIGNAL_RUN_LAST, | ||
46 | gobject.TYPE_NONE, | ||
47 | (gobject.TYPE_STRING,)) | ||
48 | } | ||
49 | |||
50 | # We use these little helper functions to ensure that we take the gdk lock | ||
51 | # when emitting the signal. These functions are called as idles (so that | ||
52 | # they happen in the gtk / main thread's main loop. | ||
53 | def emit_error_signal (self, remark): | ||
54 | gtk.gdk.threads_enter() | ||
55 | self.emit ("error", remark) | ||
56 | gtk.gdk.threads_leave() | ||
57 | |||
58 | def emit_success_signal (self): | ||
59 | gtk.gdk.threads_enter() | ||
60 | self.emit ("success") | ||
61 | gtk.gdk.threads_leave() | ||
62 | |||
63 | def __init__ (self): | ||
64 | gobject.GObject.__init__ (self) | ||
65 | |||
66 | class LoaderThread(threading.Thread): | ||
67 | """ This class provides an asynchronous loader for the metadata (by | ||
68 | using threads and signals). This is useful since the metadata may be | ||
69 | at a remote URL.""" | ||
70 | class LoaderImportException (Exception): | ||
71 | pass | ||
72 | |||
73 | def __init__(self, loader, url): | ||
74 | threading.Thread.__init__ (self) | ||
75 | self.url = url | ||
76 | self.loader = loader | ||
77 | |||
78 | def run (self): | ||
79 | result = {} | ||
80 | try: | ||
81 | f = urllib2.urlopen (self.url) | ||
82 | |||
83 | # Parse the metadata format. The format is.... | ||
84 | # <machine>;<default distro>|<distro>...;<default image>|<image>...;<type##url>|... | ||
85 | for line in f.readlines(): | ||
86 | components = line.split(";") | ||
87 | if (len (components) < 4): | ||
88 | raise MetaDataLoader.LoaderThread.LoaderImportException | ||
89 | machine = components[0] | ||
90 | distros = components[1].split("|") | ||
91 | images = components[2].split("|") | ||
92 | urls = components[3].split("|") | ||
93 | |||
94 | result[machine] = (distros, images, urls) | ||
95 | |||
96 | # Create an object representing this *potential* | ||
97 | # configuration. It can become concrete if the machine, distro | ||
98 | # and image are all chosen in the UI | ||
99 | configuration = BuildConfiguration() | ||
100 | configuration.metadata_url = self.url | ||
101 | configuration.machine_options = result | ||
102 | self.loader.configuration = configuration | ||
103 | |||
104 | # Emit that we've actually got a configuration | ||
105 | gobject.idle_add (MetaDataLoader.emit_success_signal, | ||
106 | self.loader) | ||
107 | |||
108 | except MetaDataLoader.LoaderThread.LoaderImportException, e: | ||
109 | gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader, | ||
110 | "Repository metadata corrupt") | ||
111 | except Exception, e: | ||
112 | gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader, | ||
113 | "Unable to download repository metadata") | ||
114 | print e | ||
115 | |||
116 | def try_fetch_from_url (self, url): | ||
117 | # Try and download the metadata. Firing a signal if successful | ||
118 | thread = MetaDataLoader.LoaderThread(self, url) | ||
119 | thread.start() | ||
120 | |||
121 | class BuildSetupDialog (gtk.Dialog): | ||
122 | RESPONSE_BUILD = 1 | ||
123 | |||
124 | # A little helper method that just sets the states on the widgets based on | ||
125 | # whether we've got good metadata or not. | ||
126 | def set_configurable (self, configurable): | ||
127 | if (self.configurable == configurable): | ||
128 | return | ||
129 | |||
130 | self.configurable = configurable | ||
131 | for widget in self.conf_widgets: | ||
132 | widget.set_sensitive (configurable) | ||
133 | |||
134 | if not configurable: | ||
135 | self.machine_combo.set_active (-1) | ||
136 | self.distribution_combo.set_active (-1) | ||
137 | self.image_combo.set_active (-1) | ||
138 | |||
139 | # GTK widget callbacks | ||
140 | def refresh_button_clicked (self, button): | ||
141 | # Refresh button clicked. | ||
142 | |||
143 | url = self.location_entry.get_chars (0, -1) | ||
144 | self.loader.try_fetch_from_url(url) | ||
145 | |||
146 | def repository_entry_editable_changed (self, entry): | ||
147 | if (len (entry.get_chars (0, -1)) > 0): | ||
148 | self.refresh_button.set_sensitive (True) | ||
149 | else: | ||
150 | self.refresh_button.set_sensitive (False) | ||
151 | self.clear_status_message() | ||
152 | |||
153 | # If we were previously configurable we are no longer since the | ||
154 | # location entry has been changed | ||
155 | self.set_configurable (False) | ||
156 | |||
157 | def machine_combo_changed (self, combobox): | ||
158 | active_iter = combobox.get_active_iter() | ||
159 | |||
160 | if not active_iter: | ||
161 | return | ||
162 | |||
163 | model = combobox.get_model() | ||
164 | |||
165 | if model: | ||
166 | chosen_machine = model.get (active_iter, 0)[0] | ||
167 | |||
168 | (distros_model, images_model) = \ | ||
169 | self.loader.configuration.get_distro_and_images_models (chosen_machine) | ||
170 | |||
171 | self.distribution_combo.set_model (distros_model) | ||
172 | self.image_combo.set_model (images_model) | ||
173 | |||
174 | # Callbacks from the loader | ||
175 | def loader_success_cb (self, loader): | ||
176 | self.status_image.set_from_icon_name ("info", | ||
177 | gtk.ICON_SIZE_BUTTON) | ||
178 | self.status_image.show() | ||
179 | self.status_label.set_label ("Repository metadata successfully downloaded") | ||
180 | |||
181 | # Set the models on the combo boxes based on the models generated from | ||
182 | # the configuration that the loader has created | ||
183 | |||
184 | # We just need to set the machine here, that then determines the | ||
185 | # distro and image options. Cunning huh? :-) | ||
186 | |||
187 | self.configuration = self.loader.configuration | ||
188 | model = self.configuration.get_machines_model () | ||
189 | self.machine_combo.set_model (model) | ||
190 | |||
191 | self.set_configurable (True) | ||
192 | |||
193 | def loader_error_cb (self, loader, message): | ||
194 | self.status_image.set_from_icon_name ("error", | ||
195 | gtk.ICON_SIZE_BUTTON) | ||
196 | self.status_image.show() | ||
197 | self.status_label.set_text ("Error downloading repository metadata") | ||
198 | for widget in self.conf_widgets: | ||
199 | widget.set_sensitive (False) | ||
200 | |||
201 | def clear_status_message (self): | ||
202 | self.status_image.hide() | ||
203 | self.status_label.set_label ( | ||
204 | """<i>Enter the repository location and press _Refresh</i>""") | ||
205 | |||
206 | def __init__ (self): | ||
207 | gtk.Dialog.__init__ (self) | ||
208 | |||
209 | # Cancel | ||
210 | self.add_button (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) | ||
211 | |||
212 | # Build | ||
213 | button = gtk.Button ("_Build", None, True) | ||
214 | image = gtk.Image () | ||
215 | image.set_from_stock (gtk.STOCK_EXECUTE,gtk.ICON_SIZE_BUTTON) | ||
216 | button.set_image (image) | ||
217 | self.add_action_widget (button, BuildSetupDialog.RESPONSE_BUILD) | ||
218 | button.show_all () | ||
219 | |||
220 | # Pull in *just* the table from the Glade XML data. | ||
221 | gxml = gtk.glade.XML ("bitbake-dev/lib/bb/ui/crumbs/puccho.glade", | ||
222 | root = "build_table") | ||
223 | table = gxml.get_widget ("build_table") | ||
224 | self.vbox.pack_start (table, True, False, 0) | ||
225 | |||
226 | # Grab all the widgets that we need to turn on/off when we refresh... | ||
227 | self.conf_widgets = [] | ||
228 | self.conf_widgets += [gxml.get_widget ("machine_label")] | ||
229 | self.conf_widgets += [gxml.get_widget ("distribution_label")] | ||
230 | self.conf_widgets += [gxml.get_widget ("image_label")] | ||
231 | self.conf_widgets += [gxml.get_widget ("machine_combo")] | ||
232 | self.conf_widgets += [gxml.get_widget ("distribution_combo")] | ||
233 | self.conf_widgets += [gxml.get_widget ("image_combo")] | ||
234 | |||
235 | # Grab the status widgets | ||
236 | self.status_image = gxml.get_widget ("status_image") | ||
237 | self.status_label = gxml.get_widget ("status_label") | ||
238 | |||
239 | # Grab the refresh button and connect to the clicked signal | ||
240 | self.refresh_button = gxml.get_widget ("refresh_button") | ||
241 | self.refresh_button.connect ("clicked", self.refresh_button_clicked) | ||
242 | |||
243 | # Grab the location entry and connect to editable::changed | ||
244 | self.location_entry = gxml.get_widget ("location_entry") | ||
245 | self.location_entry.connect ("changed", | ||
246 | self.repository_entry_editable_changed) | ||
247 | |||
248 | # Grab the machine combo and hook onto the changed signal. This then | ||
249 | # allows us to populate the distro and image combos | ||
250 | self.machine_combo = gxml.get_widget ("machine_combo") | ||
251 | self.machine_combo.connect ("changed", self.machine_combo_changed) | ||
252 | |||
253 | # Setup the combo | ||
254 | cell = gtk.CellRendererText() | ||
255 | self.machine_combo.pack_start(cell, True) | ||
256 | self.machine_combo.add_attribute(cell, 'text', 0) | ||
257 | |||
258 | # Grab the distro and image combos. We need these to populate with | ||
259 | # models once the machine is chosen | ||
260 | self.distribution_combo = gxml.get_widget ("distribution_combo") | ||
261 | cell = gtk.CellRendererText() | ||
262 | self.distribution_combo.pack_start(cell, True) | ||
263 | self.distribution_combo.add_attribute(cell, 'text', 0) | ||
264 | |||
265 | self.image_combo = gxml.get_widget ("image_combo") | ||
266 | cell = gtk.CellRendererText() | ||
267 | self.image_combo.pack_start(cell, True) | ||
268 | self.image_combo.add_attribute(cell, 'text', 0) | ||
269 | |||
270 | # Put the default descriptive text in the status box | ||
271 | self.clear_status_message() | ||
272 | |||
273 | # Mark as non-configurable, this is just greys out the widgets the | ||
274 | # user can't yet use | ||
275 | self.configurable = False | ||
276 | self.set_configurable(False) | ||
277 | |||
278 | # Show the table | ||
279 | table.show_all () | ||
280 | |||
281 | # The loader and some signals connected to it to update the status | ||
282 | # area | ||
283 | self.loader = MetaDataLoader() | ||
284 | self.loader.connect ("success", self.loader_success_cb) | ||
285 | self.loader.connect ("error", self.loader_error_cb) | ||
286 | |||
287 | def update_configuration (self): | ||
288 | """ A poorly named function but it updates the internal configuration | ||
289 | from the widgets. This can make that configuration concrete and can | ||
290 | thus be used for building """ | ||
291 | # Extract the chosen machine from the combo | ||
292 | model = self.machine_combo.get_model() | ||
293 | active_iter = self.machine_combo.get_active_iter() | ||
294 | if (active_iter): | ||
295 | self.configuration.machine = model.get(active_iter, 0)[0] | ||
296 | |||
297 | # Extract the chosen distro from the combo | ||
298 | model = self.distribution_combo.get_model() | ||
299 | active_iter = self.distribution_combo.get_active_iter() | ||
300 | if (active_iter): | ||
301 | self.configuration.distro = model.get(active_iter, 0)[0] | ||
302 | |||
303 | # Extract the chosen image from the combo | ||
304 | model = self.image_combo.get_model() | ||
305 | active_iter = self.image_combo.get_active_iter() | ||
306 | if (active_iter): | ||
307 | self.configuration.image = model.get(active_iter, 0)[0] | ||
308 | |||
309 | # This function operates to pull events out from the event queue and then push | ||
310 | # them into the RunningBuild (which then drives the RunningBuild which then | ||
311 | # pushes through and updates the progress tree view.) | ||
312 | # | ||
313 | # TODO: Should be a method on the RunningBuild class | ||
314 | def event_handle_timeout (eventHandler, build): | ||
315 | # Consume as many messages as we can ... | ||
316 | event = eventHandler.getEvent() | ||
317 | while event: | ||
318 | build.handle_event (event) | ||
319 | event = eventHandler.getEvent() | ||
320 | return True | ||
321 | |||
322 | class MainWindow (gtk.Window): | ||
323 | |||
324 | # Callback that gets fired when the user hits a button in the | ||
325 | # BuildSetupDialog. | ||
326 | def build_dialog_box_response_cb (self, dialog, response_id): | ||
327 | conf = None | ||
328 | if (response_id == BuildSetupDialog.RESPONSE_BUILD): | ||
329 | dialog.update_configuration() | ||
330 | print dialog.configuration.machine, dialog.configuration.distro, \ | ||
331 | dialog.configuration.image | ||
332 | conf = dialog.configuration | ||
333 | |||
334 | dialog.destroy() | ||
335 | |||
336 | if conf: | ||
337 | self.manager.do_build (conf) | ||
338 | |||
339 | def build_button_clicked_cb (self, button): | ||
340 | dialog = BuildSetupDialog () | ||
341 | |||
342 | # For some unknown reason Dialog.run causes nice little deadlocks ... :-( | ||
343 | dialog.connect ("response", self.build_dialog_box_response_cb) | ||
344 | dialog.show() | ||
345 | |||
346 | def __init__ (self): | ||
347 | gtk.Window.__init__ (self) | ||
348 | |||
349 | # Pull in *just* the main vbox from the Glade XML data and then pack | ||
350 | # that inside the window | ||
351 | gxml = gtk.glade.XML ("bitbake-dev/lib/bb/ui/crumbs/puccho.glade", | ||
352 | root = "main_window_vbox") | ||
353 | vbox = gxml.get_widget ("main_window_vbox") | ||
354 | self.add (vbox) | ||
355 | |||
356 | # Create the tree views for the build manager view and the progress view | ||
357 | self.build_manager_view = BuildManagerTreeView() | ||
358 | self.running_build_view = RunningBuildTreeView() | ||
359 | |||
360 | # Grab the scrolled windows that we put the tree views into | ||
361 | self.results_scrolledwindow = gxml.get_widget ("results_scrolledwindow") | ||
362 | self.progress_scrolledwindow = gxml.get_widget ("progress_scrolledwindow") | ||
363 | |||
364 | # Put the tree views inside ... | ||
365 | self.results_scrolledwindow.add (self.build_manager_view) | ||
366 | self.progress_scrolledwindow.add (self.running_build_view) | ||
367 | |||
368 | # Hook up the build button... | ||
369 | self.build_button = gxml.get_widget ("main_toolbutton_build") | ||
370 | self.build_button.connect ("clicked", self.build_button_clicked_cb) | ||
371 | |||
372 | # I'm not very happy about the current ownership of the RunningBuild. I have | ||
373 | # my suspicions that this object should be held by the BuildManager since we | ||
374 | # care about the signals in the manager | ||
375 | |||
376 | def running_build_succeeded_cb (running_build, manager): | ||
377 | # Notify the manager that a build has succeeded. This is necessary as part | ||
378 | # of the 'hack' that we use for making the row in the model / view | ||
379 | # representing the ongoing build change into a row representing the | ||
380 | # completed build. Since we know only one build can be running a time then | ||
381 | # we can handle this. | ||
382 | |||
383 | # FIXME: Refactor all this so that the RunningBuild is owned by the | ||
384 | # BuildManager. It can then hook onto the signals directly and drive | ||
385 | # interesting things it cares about. | ||
386 | manager.notify_build_succeeded () | ||
387 | print "build succeeded" | ||
388 | |||
389 | def running_build_failed_cb (running_build, manager): | ||
390 | # As above | ||
391 | print "build failed" | ||
392 | manager.notify_build_failed () | ||
393 | |||
394 | def init (server, eventHandler): | ||
395 | # Initialise threading... | ||
396 | gobject.threads_init() | ||
397 | gtk.gdk.threads_init() | ||
398 | |||
399 | main_window = MainWindow () | ||
400 | main_window.show_all () | ||
401 | |||
402 | # Set up the build manager stuff in general | ||
403 | builds_dir = os.path.join (os.getcwd(), "results") | ||
404 | manager = BuildManager (server, builds_dir) | ||
405 | main_window.build_manager_view.set_model (manager.model) | ||
406 | |||
407 | # Do the running build setup | ||
408 | running_build = RunningBuild () | ||
409 | main_window.running_build_view.set_model (running_build.model) | ||
410 | running_build.connect ("build-succeeded", running_build_succeeded_cb, | ||
411 | manager) | ||
412 | running_build.connect ("build-failed", running_build_failed_cb, manager) | ||
413 | |||
414 | # We need to save the manager into the MainWindow so that the toolbar | ||
415 | # button can use it. | ||
416 | # FIXME: Refactor ? | ||
417 | main_window.manager = manager | ||
418 | |||
419 | # Use a timeout function for probing the event queue to find out if we | ||
420 | # have a message waiting for us. | ||
421 | gobject.timeout_add (200, | ||
422 | event_handle_timeout, | ||
423 | eventHandler, | ||
424 | running_build) | ||
425 | |||
426 | gtk.main() | ||