| # |
| # 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 threading |
| import os |
| import datetime |
| import time |
| |
| class BuildConfiguration: |
| """ Represents a potential *or* historic *or* concrete build. It |
| encompasses all the things that we need to tell bitbake to do to make it |
| build what we want it to build. |
| |
| It also stored the metadata URL and the set of possible machines (and the |
| distros / images / uris for these. Apart from the metdata URL these are |
| not serialised to file (since they may be transient). In some ways this |
| functionality might be shifted to the loader class.""" |
| |
| def __init__ (self): |
| self.metadata_url = None |
| |
| # Tuple of (distros, image, urls) |
| self.machine_options = {} |
| |
| self.machine = None |
| self.distro = None |
| self.image = None |
| self.urls = [] |
| self.extra_urls = [] |
| self.extra_pkgs = [] |
| |
| def get_machines_model (self): |
| model = gtk.ListStore (gobject.TYPE_STRING) |
| for machine in self.machine_options.keys(): |
| model.append ([machine]) |
| |
| return model |
| |
| def get_distro_and_images_models (self, machine): |
| distro_model = gtk.ListStore (gobject.TYPE_STRING) |
| |
| for distro in self.machine_options[machine][0]: |
| distro_model.append ([distro]) |
| |
| image_model = gtk.ListStore (gobject.TYPE_STRING) |
| |
| for image in self.machine_options[machine][1]: |
| image_model.append ([image]) |
| |
| return (distro_model, image_model) |
| |
| def get_repos (self): |
| self.urls = self.machine_options[self.machine][2] |
| return self.urls |
| |
| # It might be a lot lot better if we stored these in like, bitbake conf |
| # file format. |
| @staticmethod |
| def load_from_file (filename): |
| |
| conf = BuildConfiguration() |
| with open(filename, "r") as f: |
| for line in f: |
| data = line.split (";")[1] |
| if (line.startswith ("metadata-url;")): |
| conf.metadata_url = data.strip() |
| continue |
| if (line.startswith ("url;")): |
| conf.urls += [data.strip()] |
| continue |
| if (line.startswith ("extra-url;")): |
| conf.extra_urls += [data.strip()] |
| continue |
| if (line.startswith ("machine;")): |
| conf.machine = data.strip() |
| continue |
| if (line.startswith ("distribution;")): |
| conf.distro = data.strip() |
| continue |
| if (line.startswith ("image;")): |
| conf.image = data.strip() |
| continue |
| |
| return conf |
| |
| # Serialise to a file. This is part of the build process and we use this |
| # to be able to repeat a given build (using the same set of parameters) |
| # but also so that we can include the details of the image / machine / |
| # distro in the build manager tree view. |
| def write_to_file (self, filename): |
| f = open (filename, "w") |
| |
| lines = [] |
| |
| if (self.metadata_url): |
| lines += ["metadata-url;%s\n" % (self.metadata_url)] |
| |
| for url in self.urls: |
| lines += ["url;%s\n" % (url)] |
| |
| for url in self.extra_urls: |
| lines += ["extra-url;%s\n" % (url)] |
| |
| if (self.machine): |
| lines += ["machine;%s\n" % (self.machine)] |
| |
| if (self.distro): |
| lines += ["distribution;%s\n" % (self.distro)] |
| |
| if (self.image): |
| lines += ["image;%s\n" % (self.image)] |
| |
| f.writelines (lines) |
| f.close () |
| |
| class BuildResult(gobject.GObject): |
| """ Represents an historic build. Perhaps not successful. But it includes |
| things such as the files that are in the directory (the output from the |
| build) as well as a deserialised BuildConfiguration file that is stored in |
| ".conf" in the directory for the build. |
| |
| This is GObject so that it can be included in the TreeStore.""" |
| |
| (STATE_COMPLETE, STATE_FAILED, STATE_ONGOING) = \ |
| (0, 1, 2) |
| |
| def __init__ (self, parent, identifier): |
| gobject.GObject.__init__ (self) |
| self.date = None |
| |
| self.files = [] |
| self.status = None |
| self.identifier = identifier |
| self.path = os.path.join (parent, identifier) |
| |
| # Extract the date, since the directory name is of the |
| # format build-<year><month><day>-<ordinal> we can easily |
| # pull it out. |
| # TODO: Better to stat a file? |
| (_, date, revision) = identifier.split ("-") |
| print(date) |
| |
| year = int (date[0:4]) |
| month = int (date[4:6]) |
| day = int (date[6:8]) |
| |
| self.date = datetime.date (year, month, day) |
| |
| self.conf = None |
| |
| # By default builds are STATE_FAILED unless we find a "complete" file |
| # in which case they are STATE_COMPLETE |
| self.state = BuildResult.STATE_FAILED |
| for file in os.listdir (self.path): |
| if (file.startswith (".conf")): |
| conffile = os.path.join (self.path, file) |
| self.conf = BuildConfiguration.load_from_file (conffile) |
| elif (file.startswith ("complete")): |
| self.state = BuildResult.STATE_COMPLETE |
| else: |
| self.add_file (file) |
| |
| def add_file (self, file): |
| # Just add the file for now. Don't care about the type. |
| self.files += [(file, None)] |
| |
| class BuildManagerModel (gtk.TreeStore): |
| """ Model for the BuildManagerTreeView. This derives from gtk.TreeStore |
| but it abstracts nicely what the columns mean and the setup of the columns |
| in the model. """ |
| |
| (COL_IDENT, COL_DESC, COL_MACHINE, COL_DISTRO, COL_BUILD_RESULT, COL_DATE, COL_STATE) = \ |
| (0, 1, 2, 3, 4, 5, 6) |
| |
| def __init__ (self): |
| gtk.TreeStore.__init__ (self, |
| gobject.TYPE_STRING, |
| gobject.TYPE_STRING, |
| gobject.TYPE_STRING, |
| gobject.TYPE_STRING, |
| gobject.TYPE_OBJECT, |
| gobject.TYPE_INT64, |
| gobject.TYPE_INT) |
| |
| class BuildManager (gobject.GObject): |
| """ This class manages the historic builds that have been found in the |
| "results" directory but is also used for starting a new build.""" |
| |
| __gsignals__ = { |
| 'population-finished' : (gobject.SIGNAL_RUN_LAST, |
| gobject.TYPE_NONE, |
| ()), |
| 'populate-error' : (gobject.SIGNAL_RUN_LAST, |
| gobject.TYPE_NONE, |
| ()) |
| } |
| |
| def update_build_result (self, result, iter): |
| # Convert the date into something we can sort by. |
| date = long (time.mktime (result.date.timetuple())) |
| |
| # Add a top level entry for the build |
| |
| self.model.set (iter, |
| BuildManagerModel.COL_IDENT, result.identifier, |
| BuildManagerModel.COL_DESC, result.conf.image, |
| BuildManagerModel.COL_MACHINE, result.conf.machine, |
| BuildManagerModel.COL_DISTRO, result.conf.distro, |
| BuildManagerModel.COL_BUILD_RESULT, result, |
| BuildManagerModel.COL_DATE, date, |
| BuildManagerModel.COL_STATE, result.state) |
| |
| # And then we use the files in the directory as the children for the |
| # top level iter. |
| for file in result.files: |
| self.model.append (iter, (None, file[0], None, None, None, date, -1)) |
| |
| # This function is called as an idle by the BuildManagerPopulaterThread |
| def add_build_result (self, result): |
| gtk.gdk.threads_enter() |
| self.known_builds += [result] |
| |
| self.update_build_result (result, self.model.append (None)) |
| |
| gtk.gdk.threads_leave() |
| |
| def notify_build_finished (self): |
| # This is a bit of a hack. If we have a running build running then we |
| # will have a row in the model in STATE_ONGOING. Find it and make it |
| # as if it was a proper historic build (well, it is completed now....) |
| |
| # We need to use the iters here rather than the Python iterator |
| # interface to the model since we need to pass it into |
| # update_build_result |
| |
| iter = self.model.get_iter_first() |
| |
| while (iter): |
| (ident, state) = self.model.get(iter, |
| BuildManagerModel.COL_IDENT, |
| BuildManagerModel.COL_STATE) |
| |
| if state == BuildResult.STATE_ONGOING: |
| result = BuildResult (self.results_directory, ident) |
| self.update_build_result (result, iter) |
| iter = self.model.iter_next(iter) |
| |
| def notify_build_succeeded (self): |
| # Write the "complete" file so that when we create the BuildResult |
| # object we put into the model |
| |
| complete_file_path = os.path.join (self.cur_build_directory, "complete") |
| f = file (complete_file_path, "w") |
| f.close() |
| self.notify_build_finished() |
| |
| def notify_build_failed (self): |
| # Without a "complete" file then this will mark the build as failed: |
| self.notify_build_finished() |
| |
| # This function is called as an idle |
| def emit_population_finished_signal (self): |
| gtk.gdk.threads_enter() |
| self.emit ("population-finished") |
| gtk.gdk.threads_leave() |
| |
| class BuildManagerPopulaterThread (threading.Thread): |
| def __init__ (self, manager, directory): |
| threading.Thread.__init__ (self) |
| self.manager = manager |
| self.directory = directory |
| |
| def run (self): |
| # For each of the "build-<...>" directories .. |
| |
| if os.path.exists (self.directory): |
| for directory in os.listdir (self.directory): |
| |
| if not directory.startswith ("build-"): |
| continue |
| |
| build_result = BuildResult (self.directory, directory) |
| self.manager.add_build_result (build_result) |
| |
| gobject.idle_add (BuildManager.emit_population_finished_signal, |
| self.manager) |
| |
| def __init__ (self, server, results_directory): |
| gobject.GObject.__init__ (self) |
| |
| # The builds that we've found from walking the result directory |
| self.known_builds = [] |
| |
| # Save out the bitbake server, we need this for issuing commands to |
| # the cooker: |
| self.server = server |
| |
| # The TreeStore that we use |
| self.model = BuildManagerModel () |
| |
| # The results directory is where we create (and look for) the |
| # build-<xyz>-<n> directories. We need to populate ourselves from |
| # directory |
| self.results_directory = results_directory |
| self.populate_from_directory (self.results_directory) |
| |
| def populate_from_directory (self, directory): |
| thread = BuildManager.BuildManagerPopulaterThread (self, directory) |
| thread.start() |
| |
| # Come up with the name for the next build ident by combining "build-" |
| # with the date formatted as yyyymmdd and then an ordinal. We do this by |
| # an optimistic algorithm incrementing the ordinal if we find that it |
| # already exists. |
| def get_next_build_ident (self): |
| today = datetime.date.today () |
| datestr = str (today.year) + str (today.month) + str (today.day) |
| |
| revision = 0 |
| test_name = "build-%s-%d" % (datestr, revision) |
| test_path = os.path.join (self.results_directory, test_name) |
| |
| while (os.path.exists (test_path)): |
| revision += 1 |
| test_name = "build-%s-%d" % (datestr, revision) |
| test_path = os.path.join (self.results_directory, test_name) |
| |
| return test_name |
| |
| # Take a BuildConfiguration and then try and build it based on the |
| # parameters of that configuration. S |
| def do_build (self, conf): |
| server = self.server |
| |
| # Work out the build directory. Note we actually create the |
| # directories here since we need to write the ".conf" file. Otherwise |
| # we could have relied on bitbake's builder thread to actually make |
| # the directories as it proceeds with the build. |
| ident = self.get_next_build_ident () |
| build_directory = os.path.join (self.results_directory, |
| ident) |
| self.cur_build_directory = build_directory |
| os.makedirs (build_directory) |
| |
| conffile = os.path.join (build_directory, ".conf") |
| conf.write_to_file (conffile) |
| |
| # Add a row to the model representing this ongoing build. It's kinda a |
| # fake entry. If this build completes or fails then this gets updated |
| # with the real stuff like the historic builds |
| date = long (time.time()) |
| self.model.append (None, (ident, conf.image, conf.machine, conf.distro, |
| None, date, BuildResult.STATE_ONGOING)) |
| try: |
| server.runCommand(["setVariable", "BUILD_IMAGES_FROM_FEEDS", 1]) |
| server.runCommand(["setVariable", "MACHINE", conf.machine]) |
| server.runCommand(["setVariable", "DISTRO", conf.distro]) |
| server.runCommand(["setVariable", "PACKAGE_CLASSES", "package_ipk"]) |
| server.runCommand(["setVariable", "BBFILES", \ |
| """${OEROOT}/meta/packages/*/*.bb ${OEROOT}/meta-moblin/packages/*/*.bb"""]) |
| server.runCommand(["setVariable", "TMPDIR", "${OEROOT}/build/tmp"]) |
| server.runCommand(["setVariable", "IPK_FEED_URIS", \ |
| " ".join(conf.get_repos())]) |
| server.runCommand(["setVariable", "DEPLOY_DIR_IMAGE", |
| build_directory]) |
| server.runCommand(["buildTargets", [conf.image], "rootfs"]) |
| |
| except Exception as e: |
| print(e) |
| |
| class BuildManagerTreeView (gtk.TreeView): |
| """ The tree view for the build manager. This shows the historic builds |
| and so forth. """ |
| |
| # We use this function to control what goes in the cell since we store |
| # the date in the model as seconds since the epoch (for sorting) and so we |
| # need to make it human readable. |
| def date_format_custom_cell_data_func (self, col, cell, model, iter): |
| date = model.get (iter, BuildManagerModel.COL_DATE)[0] |
| datestr = time.strftime("%A %d %B %Y", time.localtime(date)) |
| cell.set_property ("text", datestr) |
| |
| # This format function controls what goes in the cell. We use this to map |
| # the integer state to a string and also to colourise the text |
| def state_format_custom_cell_data_fun (self, col, cell, model, iter): |
| state = model.get (iter, BuildManagerModel.COL_STATE)[0] |
| |
| if (state == BuildResult.STATE_ONGOING): |
| cell.set_property ("text", "Active") |
| cell.set_property ("foreground", "#000000") |
| elif (state == BuildResult.STATE_FAILED): |
| cell.set_property ("text", "Failed") |
| cell.set_property ("foreground", "#ff0000") |
| elif (state == BuildResult.STATE_COMPLETE): |
| cell.set_property ("text", "Complete") |
| cell.set_property ("foreground", "#00ff00") |
| else: |
| cell.set_property ("text", "") |
| |
| def __init__ (self): |
| gtk.TreeView.__init__(self) |
| |
| # Misc descriptiony thing |
| renderer = gtk.CellRendererText () |
| col = gtk.TreeViewColumn (None, renderer, |
| text=BuildManagerModel.COL_DESC) |
| self.append_column (col) |
| |
| # Machine |
| renderer = gtk.CellRendererText () |
| col = gtk.TreeViewColumn ("Machine", renderer, |
| text=BuildManagerModel.COL_MACHINE) |
| self.append_column (col) |
| |
| # distro |
| renderer = gtk.CellRendererText () |
| col = gtk.TreeViewColumn ("Distribution", renderer, |
| text=BuildManagerModel.COL_DISTRO) |
| self.append_column (col) |
| |
| # date (using a custom function for formatting the cell contents it |
| # takes epoch -> human readable string) |
| renderer = gtk.CellRendererText () |
| col = gtk.TreeViewColumn ("Date", renderer, |
| text=BuildManagerModel.COL_DATE) |
| self.append_column (col) |
| col.set_cell_data_func (renderer, |
| self.date_format_custom_cell_data_func) |
| |
| # For status. |
| renderer = gtk.CellRendererText () |
| col = gtk.TreeViewColumn ("Status", renderer, |
| text = BuildManagerModel.COL_STATE) |
| self.append_column (col) |
| col.set_cell_data_func (renderer, |
| self.state_format_custom_cell_data_fun) |