summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/bb/ui/puccho.py
blob: 3ce4590c1621ff369ac8ea7896d1341e1baa4059 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#
# BitBake Graphical GTK User Interface
#
# Copyright (C) 2008        Intel Corporation
#
# Authored by Rob Bradford <rob@linux.intel.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import gtk
import gobject
import gtk.glade
import threading
import urllib2
import os
import contextlib

from bb.ui.crumbs.buildmanager import BuildManager, BuildConfiguration
from bb.ui.crumbs.buildmanager import BuildManagerTreeView

from bb.ui.crumbs.runningbuild import RunningBuild, RunningBuildTreeView

# The metadata loader is used by the BuildSetupDialog to download the
# available options to populate the dialog
class MetaDataLoader(gobject.GObject):
    """ This class provides the mechanism for loading the metadata (the
    fetching and parsing) from a given URL. The metadata encompasses details
    on what machines are available. The distribution and images available for
    the machine and the the uris to use for building the given machine."""
    __gsignals__ = {
        'success' : (gobject.SIGNAL_RUN_LAST,
                     gobject.TYPE_NONE,
                     ()),
        'error' : (gobject.SIGNAL_RUN_LAST,
                   gobject.TYPE_NONE,
                   (gobject.TYPE_STRING,))
        }

    # We use these little helper functions to ensure that we take the gdk lock
    # when emitting the signal. These functions are called as idles (so that
    # they happen in the gtk / main thread's main loop.
    def emit_error_signal (self, remark):
        gtk.gdk.threads_enter()
        self.emit ("error", remark)
        gtk.gdk.threads_leave()

    def emit_success_signal (self):
        gtk.gdk.threads_enter()
        self.emit ("success")
        gtk.gdk.threads_leave()

    def __init__ (self):
        gobject.GObject.__init__ (self)

    class LoaderThread(threading.Thread):
        """ This class provides an asynchronous loader for the metadata (by
        using threads and signals). This is useful since the metadata may be
        at a remote URL."""
        class LoaderImportException (Exception):
            pass

        def __init__(self, loader, url):
            threading.Thread.__init__ (self)
            self.url = url
            self.loader = loader

        def run (self):
            result = {}
            try:
                with contextlib.closing (urllib2.urlopen (self.url)) as f:
                    # Parse the metadata format. The format is....
                    # <machine>;<default distro>|<distro>...;<default image>|<image>...;<type##url>|...
                    for line in f:
                        components = line.split(";")
                        if (len (components) < 4):
                            raise MetaDataLoader.LoaderThread.LoaderImportException
                        machine = components[0]
                        distros = components[1].split("|")
                        images = components[2].split("|")
                        urls = components[3].split("|")

                        result[machine] = (distros, images, urls)

                # Create an object representing this *potential*
                # configuration. It can become concrete if the machine, distro
                # and image are all chosen in the UI
                configuration = BuildConfiguration()
                configuration.metadata_url = self.url
                configuration.machine_options = result
                self.loader.configuration = configuration

                # Emit that we've actually got a configuration
                gobject.idle_add (MetaDataLoader.emit_success_signal,
                    self.loader)

            except MetaDataLoader.LoaderThread.LoaderImportException as e:
                gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader,
                    "Repository metadata corrupt")
            except Exception as e:
                gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader,
                    "Unable to download repository metadata")
                print(e)

    def try_fetch_from_url (self, url):
        # Try and download the metadata. Firing a signal if successful
        thread = MetaDataLoader.LoaderThread(self, url)
        thread.start()

class BuildSetupDialog (gtk.Dialog):
    RESPONSE_BUILD = 1

    # A little helper method that just sets the states on the widgets based on
    # whether we've got good metadata or not.
    def set_configurable (self, configurable):
        if (self.configurable == configurable):
            return

        self.configurable = configurable
        for widget in self.conf_widgets:
            widget.set_sensitive (configurable)

        if not configurable:
            self.machine_combo.set_active (-1)
            self.distribution_combo.set_active (-1)
            self.image_combo.set_active (-1)

    # GTK widget callbacks
    def refresh_button_clicked (self, button):
        # Refresh button clicked.

        url = self.location_entry.get_chars (0, -1)
        self.loader.try_fetch_from_url(url)

    def repository_entry_editable_changed (self, entry):
        if (len (entry.get_chars (0, -1)) > 0):
            self.refresh_button.set_sensitive (True)
        else:
            self.refresh_button.set_sensitive (False)
            self.clear_status_message()

        # If we were previously configurable we are no longer since the
        # location entry has been changed
        self.set_configurable (False)

    def machine_combo_changed (self, combobox):
        active_iter = combobox.get_active_iter()

        if not active_iter:
            return

        model = combobox.get_model()

        if model:
            chosen_machine = model.get (active_iter, 0)[0]

        (distros_model, images_model) = \
            self.loader.configuration.get_distro_and_images_models (chosen_machine)

        self.distribution_combo.set_model (distros_model)
        self.image_combo.set_model (images_model)

    # Callbacks from the loader
    def loader_success_cb (self, loader):
        self.status_image.set_from_icon_name ("info",
            gtk.ICON_SIZE_BUTTON)
        self.status_image.show()
        self.status_label.set_label ("Repository metadata successfully downloaded")

        # Set the models on the combo boxes based on the models generated from
        # the configuration that the loader has created

        # We just need to set the machine here, that then determines the
        # distro and image options. Cunning huh? :-)

        self.configuration = self.loader.configuration
        model = self.configuration.get_machines_model ()
        self.machine_combo.set_model (model)

        self.set_configurable (True)

    def loader_error_cb (self, loader, message):
        self.status_image.set_from_icon_name ("error",
            gtk.ICON_SIZE_BUTTON)
        self.status_image.show()
        self.status_label.set_text ("Error downloading repository metadata")
        for widget in self.conf_widgets:
            widget.set_sensitive (False)

    def clear_status_message (self):
        self.status_image.hide()
        self.status_label.set_label (
            """<i>Enter the repository location and press _Refresh</i>""")

    def __init__ (self):
        gtk.Dialog.__init__ (self)

        # Cancel
        self.add_button (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)

        # Build
        button = gtk.Button ("_Build", None, True)
        image = gtk.Image ()
        image.set_from_stock (gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
        button.set_image (image)
        self.add_action_widget (button, BuildSetupDialog.RESPONSE_BUILD)
        button.show_all ()

        # Pull in *just* the table from the Glade XML data.
        gxml = gtk.glade.XML (os.path.dirname(__file__) + "/crumbs/puccho.glade",
            root = "build_table")
        table = gxml.get_widget ("build_table")
        self.vbox.pack_start (table, True, False, 0)

        # Grab all the widgets that we need to turn on/off when we refresh...
        self.conf_widgets = []
        self.conf_widgets += [gxml.get_widget ("machine_label")]
        self.conf_widgets += [gxml.get_widget ("distribution_label")]
        self.conf_widgets += [gxml.get_widget ("image_label")]
        self.conf_widgets += [gxml.get_widget ("machine_combo")]
        self.conf_widgets += [gxml.get_widget ("distribution_combo")]
        self.conf_widgets += [gxml.get_widget ("image_combo")]

        # Grab the status widgets
        self.status_image = gxml.get_widget ("status_image")
        self.status_label = gxml.get_widget ("status_label")

        # Grab the refresh button and connect to the clicked signal
        self.refresh_button = gxml.get_widget ("refresh_button")
        self.refresh_button.connect ("clicked", self.refresh_button_clicked)

        # Grab the location entry and connect to editable::changed
        self.location_entry = gxml.get_widget ("location_entry")
        self.location_entry.connect ("changed",
            self.repository_entry_editable_changed)

        # Grab the machine combo and hook onto the changed signal. This then
        # allows us to populate the distro and image combos
        self.machine_combo = gxml.get_widget ("machine_combo")
        self.machine_combo.connect ("changed", self.machine_combo_changed)

        # Setup the combo
        cell = gtk.CellRendererText()
        self.machine_combo.pack_start(cell, True)
        self.machine_combo.add_attribute(cell, 'text', 0)

        # Grab the distro and image combos. We need these to populate with
        # models once the machine is chosen
        self.distribution_combo = gxml.get_widget ("distribution_combo")
        cell = gtk.CellRendererText()
        self.distribution_combo.pack_start(cell, True)
        self.distribution_combo.add_attribute(cell, 'text', 0)

        self.image_combo = gxml.get_widget ("image_combo")
        cell = gtk.CellRendererText()
        self.image_combo.pack_start(cell, True)
        self.image_combo.add_attribute(cell, 'text', 0)

        # Put the default descriptive text in the status box
        self.clear_status_message()

        # Mark as non-configurable, this is just greys out the widgets the
        # user can't yet use
        self.configurable = False
        self.set_configurable(False)

        # Show the table
        table.show_all ()

        # The loader and some signals connected to it to update the status
        # area
        self.loader = MetaDataLoader()
        self.loader.connect ("success", self.loader_success_cb)
        self.loader.connect ("error", self.loader_error_cb)

    def update_configuration (self):
        """ A poorly named function but it updates the internal configuration
        from the widgets. This can make that configuration concrete and can
        thus be used for building """
        # Extract the chosen machine from the combo
        model = self.machine_combo.get_model()
        active_iter = self.machine_combo.get_active_iter()
        if (active_iter):
            self.configuration.machine = model.get(active_iter, 0)[0]

        # Extract the chosen distro from the combo
        model = self.distribution_combo.get_model()
        active_iter = self.distribution_combo.get_active_iter()
        if (active_iter):
            self.configuration.distro = model.get(active_iter, 0)[0]

        # Extract the chosen image from the combo
        model = self.image_combo.get_model()
        active_iter = self.image_combo.get_active_iter()
        if (active_iter):
            self.configuration.image = model.get(active_iter, 0)[0]

# This function operates to pull events out from the event queue and then push
# them into the RunningBuild (which then drives the RunningBuild which then
# pushes through and updates the progress tree view.)
#
# TODO: Should be a method on the RunningBuild class
def event_handle_timeout (eventHandler, build):
    # Consume as many messages as we can ...
    event = eventHandler.getEvent()
    while event:
        build.handle_event (event)
        event = eventHandler.getEvent()
    return True

class MainWindow (gtk.Window):

    # Callback that gets fired when the user hits a button in the
    # BuildSetupDialog.
    def build_dialog_box_response_cb (self, dialog, response_id):
        conf = None
        if (response_id == BuildSetupDialog.RESPONSE_BUILD):
            dialog.update_configuration()
            print(dialog.configuration.machine, dialog.configuration.distro, \
                dialog.configuration.image)
            conf = dialog.configuration

        dialog.destroy()

        if conf:
            self.manager.do_build (conf)

    def build_button_clicked_cb (self, button):
        dialog = BuildSetupDialog ()

        # For some unknown reason Dialog.run causes nice little deadlocks ... :-(
        dialog.connect ("response", self.build_dialog_box_response_cb)
        dialog.show()

    def __init__ (self):
        gtk.Window.__init__ (self)

        # Pull in *just* the main vbox from the Glade XML data and then pack
        # that inside the window
        gxml = gtk.glade.XML (os.path.dirname(__file__) + "/crumbs/puccho.glade",
            root = "main_window_vbox")
        vbox = gxml.get_widget ("main_window_vbox")
        self.add (vbox)

        # Create the tree views for the build manager view and the progress view
        self.build_manager_view = BuildManagerTreeView()
        self.running_build_view = RunningBuildTreeView()

        # Grab the scrolled windows that we put the tree views into
        self.results_scrolledwindow = gxml.get_widget ("results_scrolledwindow")
        self.progress_scrolledwindow = gxml.get_widget ("progress_scrolledwindow")

        # Put the tree views inside ...
        self.results_scrolledwindow.add (self.build_manager_view)
        self.progress_scrolledwindow.add (self.running_build_view)

        # Hook up the build button...
        self.build_button = gxml.get_widget ("main_toolbutton_build")
        self.build_button.connect ("clicked", self.build_button_clicked_cb)

# I'm not very happy about the current ownership of the RunningBuild. I have
# my suspicions that this object should be held by the BuildManager since we
# care about the signals in the manager

def running_build_succeeded_cb (running_build, manager):
    # Notify the manager that a build has succeeded. This is necessary as part
    # of the 'hack' that we use for making the row in the model / view
    # representing the ongoing build change into a row representing the
    # completed build. Since we know only one build can be running a time then
    # we can handle this.

    # FIXME: Refactor all this so that the RunningBuild is owned by the
    # BuildManager. It can then hook onto the signals directly and drive
    # interesting things it cares about.
    manager.notify_build_succeeded ()
    print("build succeeded")

def running_build_failed_cb (running_build, manager):
    # As above
    print("build failed")
    manager.notify_build_failed ()

def main (server, eventHandler):
    # Initialise threading...
    gobject.threads_init()
    gtk.gdk.threads_init()

    main_window = MainWindow ()
    main_window.show_all ()

    # Set up the build manager stuff in general
    builds_dir = os.path.join (os.getcwd(),  "results")
    manager = BuildManager (server, builds_dir)
    main_window.build_manager_view.set_model (manager.model)

    # Do the running build setup
    running_build = RunningBuild ()
    main_window.running_build_view.set_model (running_build.model)
    running_build.connect ("build-succeeded", running_build_succeeded_cb,
        manager)
    running_build.connect ("build-failed", running_build_failed_cb, manager)

    # We need to save the manager into the MainWindow so that the toolbar
    # button can use it.
    # FIXME: Refactor ?
    main_window.manager = manager

    # Use a timeout function for probing the event queue to find out if we
    # have a message waiting for us.
    gobject.timeout_add (200,
                         event_handle_timeout,
                         eventHandler,
                         running_build)

    gtk.main()